docs(self-host): document the attachment limit env vars - #5647
Conversation
…em in the env examples
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR adds session-scoped attachment storage, validation, quotas, protected mounts, API routes, cleanup, runner delivery, SDK wire support, frontend uploads, rendering, and replay. It also enables Pi’s four default built-in tools and updates approval, drive-upload, configuration, and documentation behavior. ChangesSession attachment resource
Default Pi built-ins
Estimated code review effort: 5 (Critical) | ~180 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Railway Preview Environment
Updated at 2026-08-02T10:14:13.745Z |
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
services/runner/tests/unit/wire-contract.test.ts (1)
72-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd the missing runner golden fixture.
loadGolden("run_request.attachment.json")reads fromsdks/python/oss/tests/pytest/unit/agents/golden/, but this test is underservices/runner/tests/unit/wire-contract.test.tsand the TS golden directory does not contain that file. Add the fixture at that resolver’s path or point the runner contract test at the Python golden directory before relying on this shared file.Source: Path instructions
🟡 Minor comments (10)
.agents/skills/agent-release-gate/resources/qa_product.py-241-253 (1)
241-253: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse
default_tools(cell["harness"])in thetemplate()fallback.
j1_chatandj6_warmcalltemplate(cell)without explicit tools, so Pi harnesses continue with"tools": []. That still gates release on a tool-less configuration, whichdefault_tools()says should be avoided.api/oss/src/apis/fastapi/mounts/router.py-323-323 (1)
323-323: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winKeep
PublicMountCreatealigned withMountCreate.
MountCreateRequest.mountusesPublicMountCreate, but the router converts it toMountCreate(**body.mount.model_dump()). IfPublicMountCreateadds fields thatMountCreatedoes not declare, Pydantic will reject the request; ifMountCreaterequires fields thatPublicMountCreatedoes not define, the created mount will miss required data. Use a shared mount create model or make the conversion explicit.docs/design/agent-workflows/projects/agent-multi-modality/plan.md-313-317 (1)
313-317: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAlign the retention test with the release stage.
This plan places the attachment teardown assertion in Stage 3. The Stage 1 implementation plan requires it in WP1 before enablement. Keep it in Stage 1 if retention is a release guarantee, or state that Stage 3 adds only extra coverage.
docs/design/agent-workflows/projects/agent-multi-modality/research.md-26-58 (1)
26-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the stale current-state descriptions.
Lines 54-58 state that uploads are off by default and paste or drag paths are not gated.
isAgentFileUploadsEnablednow defaults to enabled, and the attachment hook gates these paths.Lines 840-857 state a five-file limit, omit
otherfiles, and say no uploader exists. The current defaults allow 100 files, enableother, and calluploadAttachment.Mark these as a dated pre-Stage-1 baseline or update them to the shipped behavior.
Also applies to: 834-857
services/runner/src/engines/sandbox_agent/run-plan.ts-331-336 (1)
331-336: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRequire the approval reply to be the terminal message.
carriesApprovalReplyOnly()searches backward until it finds a tool envelope. It skips later messages that have no envelope.A request with an earlier approved
tool_resultfollowed by an empty user message passes this check with no prompt, attachment, or inline media. Restrict the helper to the terminal message, and add this trailing-empty-message case to the run-plan tests.services/runner/tests/unit/sandbox-agent-run-plan.test.ts-97-127 (1)
97-127: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRestore the original environment value.
Line 99 overwrites a process-wide environment variable. Line 126 always deletes it.
If the test process already defines
AGENTA_ATTACHMENTS_MAX_PER_TURN, later tests run with a different configuration. Save the original value and restore it infinally.Proposed fix
+ const originalMaxPerTurn = process.env.AGENTA_ATTACHMENTS_MAX_PER_TURN; process.env.AGENTA_ATTACHMENTS_MAX_PER_TURN = "2"; ... } finally { - delete process.env.AGENTA_ATTACHMENTS_MAX_PER_TURN; + if (originalMaxPerTurn === undefined) { + delete process.env.AGENTA_ATTACHMENTS_MAX_PER_TURN; + } else { + process.env.AGENTA_ATTACHMENTS_MAX_PER_TURN = originalMaxPerTurn; + } }docs/docs/self-host/reference/01-configuration.mdx-605-614 (1)
605-614: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDocument the upload enablement setting.
The reference lists only limit variables.
hosting/docker-compose/oss/env.oss.dev.exampleLine 123 enablesNEXT_PUBLIC_AGENT_FILE_UPLOADSseparately. A deployment can configure every listed limit but still not expose uploads. Document the flag, its default, and its Compose and Helm configuration location.hosting/docker-compose/ee/env.ee.dev.example-127-145 (1)
127-145: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDocument the runner hard limit in every compose example.
The API per-file caps can be raised above 16 MB. The runner rejects every single attachment body above 16 MB. State this constraint beside the API cap variables.
hosting/docker-compose/ee/env.ee.dev.example#L127-L145: add the 16 MB runner ceiling warning.hosting/docker-compose/ee/env.ee.gh.example#L129-L147: add the 16 MB runner ceiling warning.hosting/docker-compose/oss/env.oss.dev.example#L129-L147: add the 16 MB runner ceiling warning.web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts-53-59 (1)
53-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not report the client default as the server limit.
AGENTA_ATTACHMENTS_MAX_*_BYTEScan change the API cap. This branch always formatsDEFAULT_ATTACHMENT_LIMITS, so a413from a customized deployment can show a false limit. Return the applied limit from the API, or use a generic size-limit message.Proposed safe fallback
- const kind = kindForType(file.type || "application/octet-stream") - const limit = formatBytes(DEFAULT_ATTACHMENT_LIMITS.maxBytes[kind]) - const label = kind === "other" ? "file" : kind - return new AttachmentUploadError(`This file exceeds the ${limit} ${label} limit.`, { + return new AttachmentUploadError("This file exceeds the upload size limit.", { retryable: false, })services/runner/src/engines/sandbox_agent/attachments.ts-507-523 (1)
507-523: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAlign the
requireNativereason code with the computed modality state.
modelModalityStatereturns only"supported"or"unknown". Line 464 documents why: a catalog modality list is a positive declaration, so absence is not a negative capability. TherequireNativebranch at line 515 then reportsmodel_modality_unsupportedfor that same"unknown"state.The emitted reason code therefore asserts a fact the gate never established. An operator reading
model_modality_unsupportedin a delivery event concludes the model rejects images, when the catalog only lacked an entry. Report the unknown state in both branches, or add a distinct code such asmodel_modality_required_unknown.Note that
services/runner/tests/unit/attachment-capability-gate.test.tsline 121 pins the current string, so update that assertion together with the code.
🧹 Nitpick comments (20)
api/oss/src/core/sessions/attachments/service.py (1)
128-140: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider deleting the object before dropping the pending row on a write failure.
The quota path at Lines 148-159 keeps the
PENDINGrow when object deletion fails, so the sweep can reclaim both. This path does the opposite: it deletes the pending row and never attempts an object delete. Ifwrite_attachment_originalfails after the store persisted bytes, for example on a retried or partially applied multipart write, the object survives with no row that points at it, and the sweep cannot reclaim it.If the store guarantees that a failed write leaves no object, keep this code and record that guarantee in the comment.
♻️ Optional alignment with the quota compensation order
except Exception: - await self._delete_pending_best_effort( - project_id=project_id, - attachment_id=reservation.attachment.id, - ) + if await self._delete_original_best_effort( + project_id=project_id, + attachment=reservation.attachment, + ): + await self._delete_pending_best_effort( + project_id=project_id, + attachment_id=reservation.attachment.id, + ) raiseapi/oss/tests/pytest/acceptance/conftest.py (1)
10-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider failing instead of skipping when the stack is expected.
The probe result is cached for the whole test session. One transient TCP failure of 0.5 s then silently skips every
integrationacceptance test, and CI stays green with no database coverage. Add an env flag (for exampleAGENTA_TESTS_REQUIRE_POSTGRES) that forces a failure instead of a skip in CI.♻️ Proposed change
`@pytest.fixture`(autouse=True) def _skip_db_adjacent_when_postgres_unreachable(request): if request.node.get_closest_marker("integration") and not _postgres_reachable(): + if (os.getenv("AGENTA_TESTS_REQUIRE_POSTGRES") or "").lower() in {"1", "true"}: + pytest.fail("Postgres unreachable but required for integration tests") pytest.skip( "Postgres not reachable — skipping database-adjacent acceptance tests" )api/oss/tests/pytest/acceptance/sessions/test_session_attachments.py (1)
15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDerive the caps from configuration instead of hardcoding them.
_AUDIO_CAP_BYTESand the 10 MiB image payload duplicate the defaults inapi/oss/src/utils/env.py. If a deployment setsAGENTA_ATTACHMENTS_MAX_AUDIO_BYTESorAGENTA_ATTACHMENTS_MAX_IMAGE_BYTES, these tests fail for the wrong reason. Read the values from the sharedenvobject.♻️ Proposed change
-_AUDIO_CAP_BYTES = 15 * 1024 * 1024 +_AUDIO_CAP_BYTES = env.sessions.attachments.max_audio_bytes +_IMAGE_CAP_BYTES = env.sessions.attachments.max_image_bytes- data=b"\x89PNG\r\n\x1a\n" + b"x" * (10 * 1024 * 1024), + data=b"\x89PNG\r\n\x1a\n" + b"x" * _IMAGE_CAP_BYTES,Also applies to: 125-145
api/oss/tests/pytest/unit/sessions/test_attachment_quotas.py (1)
143-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffThe fake dispatches on rendered SQL text, which couples the test to query shape.
FakeMarkReadySession.executebranches on substrings such as"FOR UPDATE"and"count(session_attachments.id)". A benign DAO query change, for example adding acount()to the locked read or renaming the aggregate, silently routes to the wrong branch or raisesAssertionError. Consider dispatching on the statement's structural form, or move this coverage to the real DAO integration test that already exists inapi/oss/tests/pytest/unit/sessions/test_attachment_dao_integration.py.api/oss/tests/pytest/unit/sessions/test_records_truncation.py (1)
87-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSize the fixture to the maximum attachments one message can carry.
The test uses 5 attachments. The configured limit
max_pending_per_sessiondefaults to 20, so a single message can plausibly carry more than 5 references. Use that upper bound to make the budget assertion meaningful.♻️ Proposed change
- for index in range(5) + for index in range(20)web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.test.ts (1)
1-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd coverage for
useAttachmentUploads's core upload/retry/abort flow.This file tests only
removeUploadFileandisUploadRetryReady. The hook itself —enqueue,run,retry,canRetry,abort, and the remount-resumption and unmount-cleanup effects — has no test here. This logic managesAbortControllerlifecycles, retry timers keyed off a server-provided delay, and re-queuing after remount. A subtle race in any of these (for example, a stale controller reference surviving a retry, or a timer firing after abort) would not surface from static review alone.Add
renderHooktests, using fake timers and a mockAttachmentUploader, for at least: a successful upload patchesstatus: "done"with the response; a failed upload withretryAfterSecondsblocksretryuntil the deadline and then runs;abortcancels the in-flight controller and clears pending retry state.web/oss/src/components/Drives/dropEntries.ts (1)
66-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize child reads in
collectDropEntry.The
for...ofloop awaits each child sequentially. Each child is an independent async read (entryFileor a nestedreadAllEntriescall), so serializing them adds up for a folder drop with many files or deep nesting. Order does not matter here — the caller (readDroppedFiles) already flattens the result with.flat().Use
Promise.allto read every child concurrently.⚡ Proposed fix to parallelize directory traversal
if (!entry.isDirectory || !entry.createReader) return [] const dirPrefix = join(prefix, entry.name) const children = await readAllEntries(entry.createReader()) - const collected: DroppedFile[] = [] - for (const child of children) collected.push(...(await collectDropEntry(child, dirPrefix))) - return collected + const results = await Promise.all(children.map((child) => collectDropEntry(child, dirPrefix))) + return results.flat()services/runner/src/engines/sandbox_agent/attachments.ts (2)
341-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDerive the symlink-check timeout from the configured restore timeout.
Line 345 hardcodes
timeoutMs: 15_000.attachmentRestoreTimeoutMs()already exposes the same default throughAGENTA_ATTACHMENTS_RESTORE_TIMEOUT_MS.rejectDaytonaSymlinksissues onerunProcesscall per path component, so three checks at 15 s each can outlast the caller's own restore timeout when an operator lowers it. Extract the value to a named constant, or pass the caller's budget in.
593-611: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueAbort the attachment fetch when
withTimeoutfires.
withTimeoutonly clears the timer, notfetchAttachment;fetchAttachmentcreates its ownAbortSignal.timeout(), so a restore timeout can leave the download running past the worker failure. Pass a sharedAbortSignalintofetchAttachmentand cancel it on rejection to release the HTTP request.services/runner/src/engines/sandbox_agent/capabilities.ts (1)
63-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse a template literal to match the adjacent helper.
toolDeliveryUnsupportedMessageat lines 51-61 uses a template literal with literal'characters. This function uses\x27escapes and string concatenation for the same purpose. The escapes are not required inside a double-quoted string and make the message harder to read.♻️ Proposed refactor
export function attachmentDeliveryUnsupportedMessage( harness: string, kind: string, missing: string, ): string { - return ( - "harness \x27" + - harness + - "\x27 cannot receive a native " + - kind + - " attachment (" + - missing + - "); refusing the turn instead of dropping it." - ); + return ( + `harness '${harness}' cannot receive a native ${kind} attachment ` + + `(${missing}); refusing the turn instead of dropping it.` + ); }services/runner/tests/unit/attachment-materialize.test.ts (1)
180-180: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClarify the
.pngfixture, and strengthen the assertion it feeds.Line 180 names the pre-existing working copy
`.png`. The value is a valid basename, sovalidatedFilenameaccepts it, but a bare extension in a template literal reads as a typo rather than as a deliberate fixture.The assertion at lines 232-237 is also weaker than it appears. The stubbed
content-dispositionat line 201 returns the same.pngfilename, so the check passes whether or not the skip-fetch branch preserved the record-sourced value. Line 226 already proves the fetch was skipped. Give the existing copy a distinct filename so line 232 discriminates between the two code paths.Also applies to: 232-237
services/runner/tests/unit/attachment-path-safety.test.ts (1)
63-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
sepinstead of a hardcoded/.
attachments.tsline 227 builds the prefix withsepfromnode:path, so the production check is platform-independent. This assertion hardcodes"/"and would fail on a Windows runner. Line 66 can keep/, becauserelativeis built withposix.joinby design.♻️ Proposed refactor
-import { join } from "node:path"; +import { join, sep } from "node:path";- assert.equal(path.absolute.startsWith(path.root + "/"), true); + assert.equal(path.absolute.startsWith(path.root + sep), true);services/runner/tests/unit/sandbox-agent-capabilities.test.ts (1)
203-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThis duplicates coverage in
attachment-capability-gate.test.ts.
services/runner/tests/unit/attachment-capability-gate.test.tslines 24-37 already assert anativeoutcome for bothclaudeandpiwith an image attachment, and it also sets the provider. If the block is intended as a pinned-adapter regression guard rather than gate coverage, add a short comment stating the pin. Otherwise remove it and keep the gate test as the single owner.services/runner/tests/unit/session-reconstruct-history.test.ts (1)
204-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse the shared
approvalReplyMessagefixture instead of duplicating it.The object at Lines 204-215 duplicates
approvalReplyMessage(Lines 29-40) field for field. Use the shared fixture. This keeps the approval-reply shape defined once, so a future change to the envelope does not silently diverge between the two tests.♻️ Proposed fix
- const approvalReply = { - role: "assistant", - content: [ - { type: "tool_call", toolCallId: "toolu_1", toolName: "Write" }, - { - type: "tool_result", - toolCallId: "toolu_1", - toolName: "Write", - output: { approved: true, interactionToken: "tok-1" }, - }, - ], - }; + const approvalReply = approvalReplyMessage;services/runner/tests/unit/current-user-turn.test.ts (1)
10-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the redundant double cast.
ContentBlockalready declarestype,attachmentId,filename,mimeType, andsize. Theas unknown as ContentBlockcast is not needed and it hides type errors if the block shape drifts.♻️ Proposed change
- return { - type: "attachment", - attachmentId, - filename, - mimeType: "image/png", - size: 123, - } as unknown as ContentBlock; + return { + type: "attachment", + attachmentId, + filename, + mimeType: "image/png", + size: 123, + };web/packages/agenta-ui/src/RichChatInput/plugins/SendButton.tsx (1)
88-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose
disabledReasonto keyboard and screen-reader users.The wrapper span makes the tooltip reachable by pointer, because a disabled Ant Design button swallows pointer events. It does not help keyboard users: the button is
disabledand therefore out of the tab order, and the span is not focusable. Enter-to-send is blocked inSubmitPluginat the same time, so a keyboard user gets no feedback and no explanation.Render the reason in an element the assistive technology can reach. One option is a visually rendered hint next to the composer; another is an
aria-liveregion owned by the caller.♻️ Minimal accessible fallback
return ( <Tooltip title={disabledReason}> - <span className="inline-flex">{button}</span> + <span className="inline-flex" role="note" aria-label={typeof disabledReason === "string" ? disabledReason : undefined}> + {button} + </span> </Tooltip> )web/oss/test-results/junit.xml (1)
1-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the committed test-results artifact.
This file is a generated Vitest JUnit report (
testsuites/testcaseoutput), not source code. Committing it causes a noisy diff on every test run and untracked churn. Remove it from version control and add the results path to.gitignore.web/oss/src/components/AgentChatSlice/assets/attachmentMedia.ts (2)
35-46: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAdd cleanup for the
attachmentBlobQueryFamilyatom cache.
attachmentBlobQueryFamilyis a plainatomFamily. Jotai's own documentation states that internally an atom family is just aMap, and "Unless you explicitly remove unused params, this leads to memory leaks." Only the query data hasgcTime: 0; the family's cached atom config for each{sessionId, attachmentId}pair stays in memory for the lifetime of the app. In a long-running session, or across many sessions viewed in one tab, this grows without bound.Call
attachmentBlobQueryFamily.setShouldRemove(...)on an interval, or callattachmentBlobQueryFamily.remove(param)when a component unmounts, to bound the cache size.
66-98: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize
onErrorinuseAttachmentMediaSrc.
onError: () => setMode("blob")is a new function on every render.AgentMessage.tsx'sAttachmentFilePart(lines 296-297) passes this value directly intoimageProps={{onError: source.onError}}andvideoProps={{onError: source.onError}}, creating a new object each render regardless of whethermode,sessionId, orattachmentIdchanged. This defeats any memoization onFileCardor the underlying<img>/<video>elements.Wrap
onErrorwithuseCallbackso its identity stays stable across renders that do not changemode.♻️ Proposed fix
return { src: mode === "direct" ? directUrl : blobUrl, isPending: mode === "blob" && blobQuery.isPending, failed: mode === "blob" && !blobQuery.isPending && !blob, - onError: () => setMode("blob"), + onError: useCallback(() => setMode("blob"), []), }As per coding guidelines, "Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects, especially in lists."
Source: Coding guidelines
web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx (1)
277-286: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winShow a loading state for audio attachments during blob fallback.
For
kind === "image","video", and"file",FileCardreceivesloading={attachmentId ? source.isPending : false}. Forkind === "audio", no equivalent state reachesAudioPlayer;src ?? ""renders an emptysrcon the<audio>element whilesource.isPendingis true (blob fallback in flight). An emptysrccan trigger an unwanted load/error cycle on some browsers and gives the user no feedback that content is loading.Add a
loadingprop toAudioPlayer(or gate the audio player behind a placeholder) whilesource.isPendingis true.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4f184346-0b21-496d-b8ec-145fba9cc2c0
⛔ Files ignored due to path filters (21)
api/uv.lockis excluded by!**/*.lockclients/python/uv.lockis excluded by!**/*.locksdks/python/uv.lockis excluded by!**/*.lockservices/uv.lockis excluded by!**/*.lockweb/packages/agenta-api-client/src/generated/api/resources/mounts/client/requests/MountCreateRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/CreateSessionAttachmentRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/DownloadSessionAttachmentContentRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionAttachmentReferenceRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionInteractionTransitionRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/index.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/Mount.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/PublicMountCreate.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionAttachment.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionAttachmentResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionAttachmentsResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionInteractionData.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionInteractionRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionInteractionResolution.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionMount.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/index.tsis excluded by!**/generated/**
📒 Files selected for processing (212)
.agents/skills/agent-release-gate/resources/qa_probe.py.agents/skills/agent-release-gate/resources/qa_product.pyapi/entrypoints/routers.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000020_add_session_attachments.pyapi/oss/src/apis/fastapi/mounts/models.pyapi/oss/src/apis/fastapi/mounts/router.pyapi/oss/src/apis/fastapi/sessions/models.pyapi/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/mounts/dtos.pyapi/oss/src/core/mounts/service.pyapi/oss/src/core/mounts/types.pyapi/oss/src/core/sessions/attachments/__init__.pyapi/oss/src/core/sessions/attachments/dtos.pyapi/oss/src/core/sessions/attachments/interfaces.pyapi/oss/src/core/sessions/attachments/media.pyapi/oss/src/core/sessions/attachments/service.pyapi/oss/src/core/sessions/attachments/types.pyapi/oss/src/core/sessions/interactions/dtos.pyapi/oss/src/dbs/postgres/mounts/dao.pyapi/oss/src/dbs/postgres/mounts/dbas.pyapi/oss/src/dbs/postgres/mounts/mappings.pyapi/oss/src/dbs/postgres/sessions/attachments/__init__.pyapi/oss/src/dbs/postgres/sessions/attachments/dao.pyapi/oss/src/dbs/postgres/sessions/attachments/dbas.pyapi/oss/src/dbs/postgres/sessions/attachments/dbes.pyapi/oss/src/dbs/postgres/sessions/attachments/mappings.pyapi/oss/src/tasks/asyncio/sessions/attachment_sweep.pyapi/oss/src/utils/env.pyapi/oss/tests/pytest/acceptance/conftest.pyapi/oss/tests/pytest/acceptance/mounts/test_attachments_mount_hidden.pyapi/oss/tests/pytest/acceptance/sessions/test_session_attachment_teardown.pyapi/oss/tests/pytest/acceptance/sessions/test_session_attachments.pyapi/oss/tests/pytest/unit/mounts/test_agent_mounts.pyapi/oss/tests/pytest/unit/mounts/test_protected_mount_policy.pyapi/oss/tests/pytest/unit/resources/test_workflow_catalog.pyapi/oss/tests/pytest/unit/sessions/test_attachment_dao_integration.pyapi/oss/tests/pytest/unit/sessions/test_attachment_filename.pyapi/oss/tests/pytest/unit/sessions/test_attachment_media.pyapi/oss/tests/pytest/unit/sessions/test_attachment_quotas.pyapi/oss/tests/pytest/unit/sessions/test_attachment_state_machine.pyapi/oss/tests/pytest/unit/sessions/test_attachment_sweep.pyapi/oss/tests/pytest/unit/sessions/test_interaction_request_tool_call_id.pyapi/oss/tests/pytest/unit/sessions/test_records_truncation.pyapi/oss/tests/pytest/unit/sessions/test_wp5_dao_fanout.pyapi/pyproject.tomlclients/python/pyproject.tomldocs/design/agent-workflows/documentation/agent-configuration.mddocs/design/agent-workflows/documentation/tools.mddocs/design/agent-workflows/interfaces/README.mddocs/design/agent-workflows/interfaces/in-service/harness-adapters.mddocs/design/agent-workflows/interfaces/public-edge/agent-config-schema.mddocs/design/agent-workflows/projects/agent-multi-modality/README.mddocs/design/agent-workflows/projects/agent-multi-modality/context.mddocs/design/agent-workflows/projects/agent-multi-modality/decisions.mddocs/design/agent-workflows/projects/agent-multi-modality/design.mddocs/design/agent-workflows/projects/agent-multi-modality/plan.mddocs/design/agent-workflows/projects/agent-multi-modality/plans/stage-1-implementation.mddocs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-0.mddocs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-1.mddocs/design/agent-workflows/projects/agent-multi-modality/research.mddocs/design/agent-workflows/projects/agent-multi-modality/scope.mddocs/design/agent-workflows/projects/agent-multi-modality/status.mddocs/design/agent-workflows/projects/default-agent-builtins/README.mddocs/design/agent-workflows/projects/default-agent-builtins/context.mddocs/design/agent-workflows/projects/default-agent-builtins/design.mddocs/design/agent-workflows/projects/default-agent-builtins/open-questions.mddocs/design/agent-workflows/projects/default-agent-builtins/plan.mddocs/design/agent-workflows/projects/default-agent-builtins/research.mddocs/design/agent-workflows/projects/default-agent-builtins/status.mddocs/design/agent-workflows/projects/default-agent-builtins/testing.mddocs/docs/self-host/reference/01-configuration.mdxhosting/docker-compose/ee/env.ee.dev.examplehosting/docker-compose/ee/env.ee.gh.examplehosting/docker-compose/oss/env.oss.dev.examplehosting/docker-compose/oss/env.oss.gh.examplehosting/docker-compose/oss/nginx/nginx.confhosting/kubernetes/helm/Chart.yamlsdks/python/agenta/sdk/agents/__init__.pysdks/python/agenta/sdk/agents/adapters/agenta_builtins.pysdks/python/agenta/sdk/agents/adapters/harnesses.pysdks/python/agenta/sdk/agents/adapters/vercel/messages.pysdks/python/agenta/sdk/agents/adapters/vercel/stream.pysdks/python/agenta/sdk/agents/connections/models.pysdks/python/agenta/sdk/agents/connections/resolver.pysdks/python/agenta/sdk/agents/dtos.pysdks/python/agenta/sdk/agents/errors.pysdks/python/agenta/sdk/agents/model_catalog.pysdks/python/agenta/sdk/agents/pi_builtins.pysdks/python/agenta/sdk/agents/platform/connections.pysdks/python/agenta/sdk/agents/utils/wire.pysdks/python/agenta/sdk/agents/wire_models.pysdks/python/agenta/sdk/utils/types.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_messages_roundtrip.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_conformance.pysdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_multimodality.pysdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.pysdks/python/oss/tests/pytest/unit/agents/connections/test_models.pysdks/python/oss/tests/pytest/unit/agents/connections/test_resolver.pysdks/python/oss/tests/pytest/unit/agents/golden/pi_default_active_builtins.jsonsdks/python/oss/tests/pytest/unit/agents/golden/run_request.attachment.jsonsdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.pysdks/python/oss/tests/pytest/unit/agents/test_dtos_content_blocks.pysdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.pysdks/python/oss/tests/pytest/unit/agents/test_pi_builtins_parity.pysdks/python/oss/tests/pytest/unit/agents/test_result_multimodality.pysdks/python/oss/tests/pytest/unit/agents/test_ui_messages.pysdks/python/oss/tests/pytest/unit/agents/test_wire_contract.pysdks/python/oss/tests/pytest/unit/agents/test_wire_models.pysdks/python/pyproject.tomlservices/oss/src/agent/config.pyservices/oss/tests/pytest/unit/agent/test_config_template_fallback.pyservices/oss/tests/pytest/unit/agent/test_default_agent_template.pyservices/pyproject.tomlservices/runner/config/agent.jsonservices/runner/src/engines/sandbox_agent/acp-interactions.tsservices/runner/src/engines/sandbox_agent/attachments.tsservices/runner/src/engines/sandbox_agent/capabilities.tsservices/runner/src/engines/sandbox_agent/engine.tsservices/runner/src/engines/sandbox_agent/reconstruct-history.tsservices/runner/src/engines/sandbox_agent/run-plan.tsservices/runner/src/engines/sandbox_agent/run-turn.tsservices/runner/src/engines/sandbox_agent/runtime-contracts.tsservices/runner/src/engines/sandbox_agent/session-identity.tsservices/runner/src/engines/sandbox_agent/session-pool.tsservices/runner/src/engines/sandbox_agent/transcript.tsservices/runner/src/protocol.tsservices/runner/src/server.tsservices/runner/src/sessions/attachments.tsservices/runner/src/sessions/interactions.tsservices/runner/src/sessions/reconstruct.tsservices/runner/tests/unit/attachment-capability-gate.test.tsservices/runner/tests/unit/attachment-client.test.tsservices/runner/tests/unit/attachment-delivery-events.test.tsservices/runner/tests/unit/attachment-materialize.test.tsservices/runner/tests/unit/attachment-path-safety.test.tsservices/runner/tests/unit/attachment-prompt-blocks.test.tsservices/runner/tests/unit/current-user-turn.test.tsservices/runner/tests/unit/pi-default-builtins-parity.test.tsservices/runner/tests/unit/reconstruct-resume-nonfatal.test.tsservices/runner/tests/unit/sandbox-agent-acp-interactions.test.tsservices/runner/tests/unit/sandbox-agent-capabilities.test.tsservices/runner/tests/unit/sandbox-agent-orchestration.test.tsservices/runner/tests/unit/sandbox-agent-run-plan.test.tsservices/runner/tests/unit/server.test.tsservices/runner/tests/unit/session-keepalive-approval.test.tsservices/runner/tests/unit/session-persist.test.tsservices/runner/tests/unit/session-pool.test.tsservices/runner/tests/unit/session-reconstruct-history.test.tsservices/runner/tests/unit/session-reconstruct.test.tsservices/runner/tests/unit/transcript.test.tsservices/runner/tests/unit/wire-contract.test.tsweb/ee/package.jsonweb/oss/package.jsonweb/oss/src/components/AgentChatSlice/AgentConversation.tsxweb/oss/src/components/AgentChatSlice/assets/attachmentMedia.tsweb/oss/src/components/AgentChatSlice/assets/attachmentTransport.test.tsweb/oss/src/components/AgentChatSlice/assets/attachmentTransport.tsweb/oss/src/components/AgentChatSlice/assets/attachments.test.tsweb/oss/src/components/AgentChatSlice/assets/attachments.tsweb/oss/src/components/AgentChatSlice/assets/constants.tsweb/oss/src/components/AgentChatSlice/assets/files.test.tsweb/oss/src/components/AgentChatSlice/assets/files.tsweb/oss/src/components/AgentChatSlice/assets/inFlightSubmit.test.tsweb/oss/src/components/AgentChatSlice/assets/inFlightSubmit.tsweb/oss/src/components/AgentChatSlice/assets/messageParts.tsweb/oss/src/components/AgentChatSlice/assets/transcriptToMessages.test.tsweb/oss/src/components/AgentChatSlice/assets/transcriptToMessages.tsweb/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsxweb/oss/src/components/AgentChatSlice/components/AgentMessage.tsxweb/oss/src/components/AgentChatSlice/components/AgentTurn.tsxweb/oss/src/components/AgentChatSlice/components/ApprovalDock.tsxweb/oss/src/components/AgentChatSlice/components/AudioPlayer.tsxweb/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsxweb/oss/src/components/AgentChatSlice/components/ToolActivity.tsxweb/oss/src/components/AgentChatSlice/components/TranscriptPlaceholder.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/meta.test.tsweb/oss/src/components/AgentChatSlice/components/clientTools/meta.tsweb/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.test.tsweb/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.tsweb/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.tsweb/oss/src/components/AgentChatSlice/state/sessions.tsweb/oss/src/components/Drives/ContextRail.tsxweb/oss/src/components/Drives/DriveExplorer.tsxweb/oss/src/components/Drives/FilesDrawer.tsxweb/oss/src/components/Drives/SessionFilesDrawer.tsxweb/oss/src/components/Drives/chatFileRefs.tsxweb/oss/src/components/Drives/configDrive.tsweb/oss/src/components/Drives/driveMedia.tsweb/oss/src/components/Drives/dropEntries.test.tsweb/oss/src/components/Drives/dropEntries.tsweb/oss/src/components/Drives/useDriveDrop.tsweb/oss/src/components/Drives/useDriveUploads.tsweb/oss/src/components/Drives/useMountUpload.tsweb/oss/src/components/Drives/useSessionDrive.tsweb/oss/test-results/junit.xmlweb/oss/tests/playwright/acceptance/agent-chat/attach-send-render-reload.spec.tsweb/package.jsonweb/packages/agenta-api-client/package.jsonweb/packages/agenta-entities/src/session/core/mountSelection.tsweb/packages/agenta-entities/src/session/index.tsweb/packages/agenta-entities/tests/unit/create-ephemeral-app-from-template.test.tsweb/packages/agenta-entities/tests/unit/session-pick-cwd-mount.test.tsweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/PiSettingsControl.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/itemDescriptors.tsxweb/packages/agenta-entity-ui/tests/unit/itemDescriptors.test.tsweb/packages/agenta-playground/src/state/execution/agentApprovalResume.tsweb/packages/agenta-playground/tests/unit/agentApprovalResume.test.tsweb/packages/agenta-playground/tests/unit/agentMessageQueue.test.tsweb/packages/agenta-playground/tests/unit/agentRequest.test.tsweb/packages/agenta-ui/src/RichChatInput/RichChatInput.tsxweb/packages/agenta-ui/src/RichChatInput/plugins/SendButton.tsxweb/packages/agenta-ui/src/RichChatInput/plugins/SubmitPlugin.tsx
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
🛑 Comments failed to post (16)
api/oss/src/apis/fastapi/sessions/router.py (2)
257-264: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Check the Attachment DTO field type and how the DAO populates created_at. fd -t f 'dtos.py' api/oss/src/core/sessions/attachments --exec cat -n {} fd -t f 'mappings.py' api/oss/src/dbs/postgres/sessions/attachments --exec cat -n {}Repository: Agenta-AI/agenta
Length of output: 6469
🏁 Script executed:
#!/bin/bash set -eu echo "== candidate files ==" fd -t f 'models.py|session.*|attachment.*|dbes.py' api/oss/src || true echo echo "== session attachment models and route helper ==" sed -n '1,360p' api/oss/src/apis/fastapi/sessions/router.py echo echo "== DBE definitions and lifecycle model ==" fd -t f 'dbes.py' api/oss/src/dbs/postgres/sessions/attachments --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {} fd -t f 'dtos.py' api/oss/src/core/shared --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}Repository: Agenta-AI/agenta
Length of output: 19461
🏁 Script executed:
#!/bin/bash set -eu echo "== session API models ==" sed -n '1,260p' api/oss/src/apis/fastapi/sessions/models.py echo echo "== lifecycle in agenta sdk ==" rg -n "class Lifecycle|created_at|updated_at|deleted_at" -g '*.py' vendor agenta api/oss/src/core/shared -S || true echo echo "== session attachment DBO schema ==" sed -n '1,260p' api/oss/src/dbs/postgres/sessions/attachments/dbas.py echo echo "== repository-wide Attachment.created_at usages ==" rg -n "created_at=None|created_at\.\s*is\s+None|created_at is None|attachment\.created_at|SessionAttachment\(" api/oss/src -SRepository: Agenta-AI/agenta
Length of output: 9805
🏁 Script executed:
#!/bin/bash set -eu echo "== agenta sdk lifecycle definitions ==" python3 - <<'PY' import inspect try: from agenta.sdk.models.shared import Lifecycle print("Lifecycle=", Lifecycle) print(inspect.getsource(Lifecycle)) except Exception as exc: print(type(exc).__name__, str(exc)) try: from agenta.sdk.models.shared import LifecycleDBA, LifecycleDTO print("LifecycleDBA=", LifecycleDBA) print(inspect.getsource(LifecycleDBA)) except Exception as exc: print(type(exc).__name__, str(exc)) try: from agenta.sdk.models.shared import LifecycleDBE print("LifecycleDBE=", LifecycleDBE) print(inspect.getsource(LifecycleDBE)) except Exception as exc: print(type(exc).__name__, str(exc)) PY echo echo "== relevant DAO/service slices ==" sed -n '1,230p' api/oss/src/dbs/postgres/sessions/attachments/dao.py sed -n '180,260p' api/oss/src/core/sessions/attachments/service.pyRepository: Agenta-AI/agenta
Length of output: 10958
🌐 Web query:
agenta sdk models shared Lifecycle created_at💡 Result:
In the Agenta platform and SDK architecture, Lifecycle refers to a standardized set of metadata fields used across Agenta resources (such as applications, variants, and revisions) to track their existence and history [1][2]. Key aspects of the Lifecycle and created_at implementation include: Standardized Lifecycle Fields: Most Agenta resources are required to implement a consistent set of Lifecycle columns [1][2]. These typically include created_at, updated_at, and often fields related to soft-deletion (such as deleted_at) [1][2][3][4]. The created_at field stores a date-time timestamp indicating when a specific resource or entity was first initialized or committed to the system [2][3]. Versioning and Timestamps: Agenta employs a versioning model similar to Git, where resources consist of artifacts, variants, and immutable revisions [3]. The created_at timestamp is critical for these entities: - Revision Created_at: Represents the commit date of the specific revision [5]. - Environment Timestamps: Environments themselves have created_at and updated_at timestamps, which reflect the creation and modification of the environment artifact (e.g., its name or description) rather than the date of the specific revision currently deployed to that environment [5][6]. Platform Usage: - Query Pattern: The platform's query API (e.g., POST /{resource}/query) uses the created_at field for sorting and filtering results [4]. Requests can include specific parameters such as newest and oldest to constrain results based on these creation timestamps [4]. - Database Integrity: Recent internal updates to the Agenta platform have enforced these Lifecycle conventions across database models, ensuring that tables have fully nullable lifecycle columns and standard server defaults for created_at to maintain consistency [1]. While the Agenta SDK allows users to manage configurations using custom Pydantic models (such as ModelConfig for LLM parameters), the Lifecycle metadata is primarily managed by the backend platform and API layer rather than being defined by the user in the SDK's configuration models [7][8][9][10].
Citations:
- 1: #4676
- 2: https://apis.apievangelist.com/store/agenta-testsets-api/
- 3: https://agenta.ai/docs/reference/api-guide/versioning
- 4: https://agenta.ai/docs/reference/api-guide/query-pattern
- 5: #4593
- 6: #4592
- 7: https://github.com/Agenta-AI/agenta/blob/main/docs/docs/reference/sdk/01-configuration-management.mdx
- 8: https://agenta.ai/docs/custom-workflows/configuration-types
- 9: https://agenta.ai/docs/reference/sdk/custom-workflow
- 10: https://agenta.ai/docs/reference/sdk/configuration-management
🏁 Script executed:
#!/bin/bash set -eu echo "== SQL migrations for session_attachments ==" fd -t f '\.(sql|py)$' . -d 4 | xargs -r rg -n "session_attachments|created_at" -S --glob '*migrat*' --glob '*.sql' --glob '*.py' || true echo echo "== lifecycle DBAs with created_at declarations ==" fd -t f 'dbas.py' api/oss/src/dbs/postgres/api/oss/src/dbs/postgres/shared api/oss/src/dbs/postgres/shared --exec sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}Repository: Agenta-AI/agenta
Length of output: 5257
🏁 Script executed:
#!/bin/bash set -eu echo "== locate lifecycle pydantic sources ==" git ls-files | xargs -r rg -n "class Lifecycle\(|created_at: .*datetime|created_at: Optional\[datetime\]|default_factory=lambda: datetime" -S || true echo echo "== exact agenta package paths in repo ==" git ls-files | rg 'agenta/.*shared' || trueRepository: Agenta-AI/agenta
Length of output: 9675
Make
SessionAttachment.created_atoptional or populate missing row values.
Lifecycle.created_atis nullable inLifecycleDBA(nullbale=True) and in the SDK DTO (Optional[datetime] = None/created_at=attachment_dbe.created_at).SessionAttachment.created_atis required, so areadyattachment loaded withcreated_at=Nonemakes_to_session_attachment()raise during response serialization for the attachment routes.
1045-1058: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Starlette FormData close async context manager request.form💡 Result:
In Starlette,
request.form()can be used as an asynchronous context manager to automatically handle the lifecycle of uploaded files, ensuring they are properly closed and associated resources are cleaned up [1][2][3]. When you useasync with request.form() as form:, Starlette parses the request body and returns aFormDataobject [1][3]. If the form contains file uploads, these are represented asUploadFileinstances, which useSpooledTemporaryFileinternally to store data [1][3][4]. Using the context manager ensures that theclose()method is called on these file objects once the block exits, preventing resource leaks [2][4]. Prior to the introduction of the context manager pattern, developers were required to manually callawait request.form()and then subsequently ensureawait form.close()was called to clean up any temporary files [2][4]. The context manager approach is now the recommended best practice for handling form data to avoid common issues where files remain open [2]. Example usage: async with request.form as form: filename = form["upload_file"].filename contents = await form["upload_file"].read # File is automatically closed after this block exits [2]Citations:
- 1: https://starlette.dev/requests/
- 2: Kludex/starlette#1903
- 3: https://www.starlette.io/requests/
- 4: https://deepwiki.com/encode/starlette/4.2-form-parsing
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== locate router ==" fd -a 'router.py' . | sed 's#^\./##' | rg 'api/oss/src/apis/fastapi/sessions/router.py|sessions/router.py' || true echo "== relevant source slice ==" sed -n '1020,1075p' api/oss/src/apis/fastapi/sessions/router.py echo "== dependency/source for FormData context manager ==" python3 - <<'PY' import inspect try: from starlette.datastructures import FormData import starlette print("starlette version:", starlette.__version__) print(inspect.getsource(FileFormDataFile if (FileFormDataFile := getattr(inspect.getmodule(FormData), "FileFormDataFile", None)) else "__missing__")) print("FormData close source:", inspect.getsource(FormData.close)) # Probe class methods/mro without importing repo code. for cls in [FormData]: print(cls.__name__, cls.__bases__) print("__aenter__", hasattr(cls, "__aenter__"), cls.__aenter__ if hasattr(cls, "__aenter__") else None) print("__aexit__", hasattr(cls, "__aexit__"), cls.__aexit__ if hasattr(cls, "__aexit__") else None) except Exception as exc: print("ERROR", type(exc).__name__, exc) PY echo "== existing form close/context usage ==" rg -n "request\.form\(|FormData|async with .*form|form\.close\(" api/oss/src/apis/fastapi -g '*.py' || trueRepository: Agenta-AI/agenta
Length of output: 3282
🏁 Script executed:
#!/bin/bash set -euo pipefail python3 - <<'PY' import sys, warnings, asyncio from pathlib import Path try: import starlette from starlette.datastructures import FormData, UploadFile from starlette.testclient import TestClient from starlette.requests import Request from starlette.responses import JSONResponse except Exception as exc: print("IMPORT_ERROR", type(exc).__name__, exc, file=sys.stderr) sys.exit(0) print("starlette_version", starlette.__version__) print("FormData_has_aenter", FormData.__aenter__, FormData.__aexit__, UploadFile.__aenter__, UploadFile.__aexit__) MAX_MEM = 256 * 1024 # 256 kB payload = b"x" * (MAX_MEM + 64 * 1024) class App: def __init__(self, handler): self.handler = handler async def __call__(self, scope, receive, send): req = Request(scope, receive) try: out = await self.handler(req) finally: out.close() def plain_file_endpoint(): async def handler(request: Request): form = await request.form() assert form["file"].filename == "upload.bin" f = form["file"] data = await f.read() await f.close() # only close after the endpoint manually handles it return JSONResponse({"size": sys.getsizeof(data)}) return App(handler) def context_file_endpoint(): async def handler(request: Request): async with await request.form() as form: f = form["file"] data = await f.read() assert form["file"].filename == "upload.bin" return JSONResponse({"size": sys.getsizeof(data)}) return App(handler) client = TestClient(App(lambda r: JSONResponse({}))) for _, endpoint in [ ("plain", plain_file_endpoint()), ("context", context_file_endpoint()), ]: resp = client.post("/test-file", files={"file": ("upload.bin", payload)}) print("endpoint", endpoint.__class__) print("resp_status", resp.status_code, "resp_json", resp.json()) print("fd_count_before_import", None) PYRepository: Agenta-AI/agenta
Length of output: 215
🌐 Web query:
Starlette source FormData __aenter__ __aexit__ close UploadFile File spooled💡 Result:
In Starlette, UploadFile objects are used to handle file uploads during multipart/form-data requests [1][2]. These objects leverage a SpooledTemporaryFile internally to manage data, automatically switching between memory and disk storage based on a size threshold [1][3]. Regarding the requested methods and context management: 1. UploadFile does not implement aenter or aexit directly. Instead, Starlette provides a helper, AwaitableOrContextManagerWrapper, in its request.form method [4][5]. This allows you to use the form as an asynchronous context manager: async with request.form as form: [1][2]. 2. The close method is defined on both UploadFile and FormData [6][3]. When you use the async context manager with request.form, the underlying FormData.close method is called upon exit [4][5]. This method iterates through all items in the form and explicitly calls close on any UploadFile instances, ensuring that the underlying SpooledTemporaryFile is properly closed and resources are released [6][3][7]. 3. Recent developments have improved this process. While older implementations relied on run_in_threadpool to perform synchronous file operations asynchronously [3][7], newer versions (e.g., following PR
#2925) have integrated anyio.SpooledTemporaryFile, which provides native asynchronous support for file operations, simplifying the internal handling [8]. Using the async context manager (async with request.form) is the recommended practice, as it guarantees that all UploadFile objects created during the request are correctly closed, avoiding potential resource warnings associated with unclosed temporary files [1][9].Citations:
- 1: https://starlette.dev/requests/
- 2: https://www.starlette.io/requests/
- 3: https://github.com/encode/starlette/blob/212fa46b23be0701a5963cdeff14f05ed352e22a/starlette/datastructures.py
- 4: https://github.com/encode/starlette/blob/master/starlette/_utils.py
- 5: https://github.com/Kludex/starlette/blob/118c7c71c9dd053291100176113338eb62071547/starlette/requests.py
- 6: https://github.com/encode/starlette/blob/master/starlette/datastructures.py
- 7: https://github.com/encode/starlette/blob/4b37cf89ec80f8eee74eefa8bdce6384ae7af5c3/starlette/datastructures.py
- 8: Kludex/starlette#2925
- 9: Kludex/starlette#1967
Close the parsed form when handling attachments.
multipart/form-datauploads are represented byUploadFile, which uses a spooled temporary file for large payloads. Use the form as an async context manager, or callawait form.close()after readingfile, so the temporary file closes when the request finishes.🔒️ Proposed fix using the form context manager
- form = await request.form() - file = form.get("file") - idempotency_key = form.get("idempotency_key") - if not isinstance(file, UploadFile): - raise AttachmentRequestInvalid("A file upload is required.") - if not isinstance(idempotency_key, str): - raise AttachmentRequestInvalid("An idempotency_key form field is required.") - # The key is part of a composite btree index, which rejects oversized values at the DAO. - if len(idempotency_key) > _MAX_IDEMPOTENCY_KEY_CHARACTERS: - raise AttachmentRequestInvalid( - f"idempotency_key must be at most {_MAX_IDEMPOTENCY_KEY_CHARACTERS} characters." - ) - - data = await self._read_bounded(file=file) + async with await request.form() as form: + file = form.get("file") + idempotency_key = form.get("idempotency_key") + if not isinstance(file, UploadFile): + raise AttachmentRequestInvalid("A file upload is required.") + if not isinstance(idempotency_key, str): + raise AttachmentRequestInvalid("An idempotency_key form field is required.") + # The key is part of a composite btree index, which rejects oversized values at the DAO. + if len(idempotency_key) > _MAX_IDEMPOTENCY_KEY_CHARACTERS: + raise AttachmentRequestInvalid( + f"idempotency_key must be at most {_MAX_IDEMPOTENCY_KEY_CHARACTERS} characters." + ) + + data = await self._read_bounded(file=file)docs/design/agent-workflows/projects/agent-multi-modality/design.md (1)
381-401: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Resolve the attachment-limit contract before implementation.
The matrix defines 100 attachments per turn and 1,000 stored attachments per session. Line [453] says the count is 5.
plan.mdandplans/stage-1-implementation.mduse 100 for the session quota, while the runner config uses 5 per turn. The frontend courtesy limit remains 100.Select one value for each limit. Update the decision log, design, plans, configuration, and tests. If the runner cap is 5, the frontend must prevent uploads above 5 instead of allowing turns that the runner rejects.
Also applies to: 453-453
docs/design/agent-workflows/projects/agent-multi-modality/plan.md (1)
146-149: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Assign record-reference persistence to one work package.
This plan places the record-schema extension in the API-first package. The Stage 1 implementation plan says the JSONB record needs no migration and assigns the change entirely to WP2. Update the package scope and acceptance criteria so one package owns this contract.
docs/design/agent-workflows/projects/agent-multi-modality/plans/stage-1-implementation.md (5)
372-378: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Make quota admission atomic with row creation.
The create flow checks count, bytes, and pending uploads, then inserts the pending row. The plan does not require a per-session lock or an atomic reservation. Concurrent uploads can pass the same check and exceed the server-side quotas. Add a serialization mechanism and a concurrency test.
522-531: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Retain retry state when object deletion fails.
The reaper and sweep delete the object “best-effort” and then delete the database row. If object deletion fails, later sweeps cannot retry it because the row is gone. This can leave orphaned originals permanently. Keep a deletion-pending record, or delete the row only after confirmed object deletion.
712-715: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not make attachment claiming non-fatal after persisting the record.
The runner persists the user record before claiming the attachments. A failed claim still allows the turn to run. The TTL sweep can then delete the unclaimed original after 24 hours even though the record references it. This breaks the retention and findability promises.
Make claim state recoverable and exclude persisted references from cleanup, or make the persistence and claim path atomic.
Also applies to: 762-764
730-741: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail rg -n -C 4 'linkSync|writeFsFile|statFs|EEXIST|mutex|lock|serialize' \ services/runner/src services/runner/testsRepository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "Plan target lines:" sed -n '700,760p' docs/design/agent-workflows/projects/agent-multi-modality/plans/stage-1-implementation.md echo sed -n '1135,1165p' docs/design/agent-workflows/projects/agent-multi-modality/plans/stage-1-implementation.md echo echo "Relevant implementation sections:" sed -n '270,345p' services/runner/src/engines/sandbox_agent/attachments.ts sed -n '355,395p' services/runner/src/engines/sandbox_agent/attachments.ts echo echo "Lock/serialize mentions in runner source:" rg -n '\b(mutex|lock|serialize|serialize|acquire|shared|concurrent|materializeWorkingCopy|writeWorkingCopy|restore|working-copy)\b' services/runner/src -g '*.ts' -C 3 \ | sed -n '1,260p' echo echo "Test mentions for attachment/materialize/working copy/Daytona/geesefs/local exclusive:" rg -n 'attachment|materialize|working.?copy|daytona|geesefs|local|exclusive|linkSync|writeFsFile|statFs|EEXIST|overwrite|restore' services/runner/tests -g '*.ts' -C 2 \ | sed -n '1,260p'Repository: Agenta-AI/agenta
Length of output: 50372
🌐 Web query:
geesefs link symlink EOPNOTSUPP behavior Node.js linkSync fs.linkSync💡 Result:
The error EOPNOTSUPP (Operation not supported) occurs when an application attempts a file system operation—such as creating a hard link—that is not supported by the underlying file system or mount, including FUSE-based systems like GeeseFS [1][2][3]. GeeseFS explicitly does not support hard links [1][4]. When using Node.js fs.linkSync to attempt a hard link operation on a GeeseFS mount, the underlying system call will fail, and Node.js will surface this as an error [5]. Because GeeseFS and other non-POSIX-compliant or restricted file systems do not implement the necessary hard link primitives, the kernel returns EOPNOTSUPP, which Node.js propagates to your JavaScript code [2][5][6]. Regarding symlinks, GeeseFS does support symbolic links [1][4]. If you are encountering an EOPNOTSUPP error specifically when working with symlinks, it is likely due to an attempt to perform an unsupported attribute modification on the symlink itself (such as changing its permissions) or an interaction with specific mount-time limitations [7][8]. Key takeaways: - Hard links are not supported by GeeseFS [1][4]. Attempting to create one via fs.linkSync will result in an operation not supported error [5][6]. - Symlinks are supported by GeeseFS [1][4]. If you receive EOPNOTSUPP while working with symlinks, it usually indicates that the operation being attempted (like changing file mode/permissions on the link) is not permitted by the kernel for symlink inodes or the underlying storage configuration [7][8]. - Node.js fs.linkSync directly maps to the underlying system call (link(2)). It does not implement special handling for these errors; it simply throws the error received from the operating system [5][9]. If your application requires hard link-like behavior on S3-backed storage, you may need to use a different strategy, such as symlinks (if supported) or by storing redundant copies, as hard links fundamentally require the file system to manage shared inode references that simple object storage does not natively provide [1][10].
Citations:
- 1: https://github.com/yandex-cloud/geesefs
- 2: https://lists.jedsoft.org/lists/jed-users/2007/0000308.html
- 3: https://reviews.freebsd.org/rGdd9e59beebbb346f329dfd1de26a84da8afdcb1d
- 4: https://github.com/yandex-cloud/docs/blob/master/en/storage/tools/geesefs.md
- 5: https://nodejs.org/api/fs.html
- 6: nodejs/node#11663
- 7: solid-eureka/visionfive-linux-mitigations@5d1f903
- 8: google/gvisor@1a02bb2
- 9: nodejs/node@81f15274e2
- 10: https://stackoverflow.com/questions/79279657/why-does-fs-link-fail-with-eperm-whre-fs-rename-succeeds
Use an atomic, exclusive create primitive for every working-copy materialization.
The documented local path relies on
linkSync, but geesefs does not support hard links, so that path cannot provide the required local-filesystem atomicity. The Daytona path usesstatFs/writeFsFile, so concurrent restores can overwrite edited working copies despite the never-overwrite guarantee. Add a per-attachment or per-session lock, or use exclusive-create writes for every sandbox; block release until geesefs and Daytona integration tests pass.
783-796: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Define the outcome for fetch failures.
The plan says
failedrepresents only a contract violation. It also listsfetch_failedas a reason code and requires missing, authorization, and timeout failures to produce structured errors. Specify whether these cases emitoutcome: "failed"and continue, or fail the turn. Update D6, the event contract, and tests together.docs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-0.md (1)
45-51: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Gate recorder completion with the attachment feature.
The recorder calls
addFilesbelow theNEXT_PUBLIC_AGENT_FILE_UPLOADSguard. Stage 2 enablesNEXT_PUBLIC_AGENT_VOICE_INPUTseparately. If voice is enabled while file uploads remain disabled, a recording enters an unavailable attachment pipeline.Require both flags for recorder completion, or document the dependency in the Stage 2 rollout contract.
services/runner/src/engines/sandbox_agent/attachments.ts (2)
372-389: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Daytona materialization leaves a symlink window between
mkdirFsandwriteFsFile.rejectDaytonaSymlinksruns only beforemkdirFs, whenpath.directorynormally does not exist yet, sotest -Lexits 1 and the check passes without proving anything about the directory thatmkdirFsthen creates. The local path is protected byO_EXCLthroughlinkSyncandwriteFileSync(..., {flag:"wx"}); the Daytona path has no equivalent, sowriteFsFilefollows whateverpath.directoryresolves to at write time. The unit test pins the current three-check-then-mkdir sequence, so it must change together with the fix.
services/runner/src/engines/sandbox_agent/attachments.ts#L372-L389: re-runrejectDaytonaSymlinksonpath.directoryandpath.absoluteafterawait sandbox.mkdirFs({ path: path.directory })returns, before thestatFsexistence probe and thewriteFsFilecall.services/runner/tests/unit/attachment-materialize.test.ts#L138-L143: extend the expectedoperationsarray so it includes the post-mkdirsymlink checks in the new order.📍 Affects 2 files
services/runner/src/engines/sandbox_agent/attachments.ts#L372-L389(this comment)services/runner/tests/unit/attachment-materialize.test.ts#L138-L143
792-810: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bound the current-turn attachment fetch with a timeout.
restoreReferencedWorkingCopieswraps everyfetchAttachmentcall inwithTimeout(..., attachmentRestoreTimeoutMs())and runs a bounded worker pool. This path does neither. Line 806 awaitsfetchAttachmentwith no deadline, inside a strictly sequentialforloop.A slow or hung attachment endpoint therefore blocks the current turn for as long as the socket stays open, with no per-attachment ceiling. The exposure multiplies by the attachment count, because each fetch waits for the previous one to settle.
Apply the same
withTimeoutbudget here. Treat a timeout asfetch_failed, which already degrades to a workspace mention instead of failing the turn. Running the loop through the same bounded pool would also remove the serial latency.🛡️ Proposed fix to bound each fetch
- const fetched = await fetchAttachment( - input.sessionId, - ref.attachmentId, - input.auth, - ); + let fetched: FetchedAttachment | null = null; + try { + fetched = await withTimeout( + fetchAttachment(input.sessionId, ref.attachmentId, input.auth), + attachmentRestoreTimeoutMs(), + ); + } catch { + // A timed-out fetch is indistinguishable from a missing one for delivery purposes. + fetched = null; + } if (!fetched) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.for (const ref of refs) { try { assertCanonicalAttachmentId(ref.attachmentId); } catch { input.emit({ type: "attachment_delivery", attachmentId: ref.attachmentId, outcome: "failed", reasonCode: "contract_violation", }); failure ??= new Error("Attachment reference is invalid."); continue; } let fetched: FetchedAttachment | null = null; try { fetched = await withTimeout( fetchAttachment(input.sessionId, ref.attachmentId, input.auth), attachmentRestoreTimeoutMs(), ); } catch { // A timed-out fetch is indistinguishable from a missing one for delivery purposes. fetched = null; }web/oss/src/components/AgentChatSlice/AgentConversation.tsx (1)
421-469: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Description: Inspect uploadExtraFiles' failure contract to confirm whether it can return an empty array on failure. ast-grep run --pattern 'uploadExtraFiles = $_' --lang typescript web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.ts web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts 2>/dev/null rg -n -A 15 'uploadExtraFiles' web/oss/src/components/AgentChatSlice/hooksRepository: Agenta-AI/agenta
Length of output: 2754
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== filesToParts and attachment response handling ==" rg -n -A 40 -B 10 'function filesToParts|const filesToParts|filesToParts' web/oss/src -g '*.ts' -g '*.tsx' rg -n -A 80 -B 10 'file\.response\?\.attachment|attachment' web/oss/src/components/AgentChatSlice -g '*.ts' -g '*.tsx' | head -n 260 echo echo "== useComposerAttachments uploadExtraFiles implementation ==" sed -n '90,180p' web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts echo echo "== AgentConversation submission call sites around handleSubmit ==" sed -n '390,490p' web/oss/src/components/AgentChatSlice/AgentConversation.tsx rg -n "handleSubmit|onSendVoiceMessage|onSubmit|onStart" web/oss/src/components/AgentChatSlice/AgentConversation.tsxRepository: Agenta-AI/agenta
Length of output: 50372
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== useComposerAttachments uploadExtraFiles full implementation ==" sed -n '100,160p' web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts echo echo "== AgentConversation handleSubmitRef.current assignment and direct call sites ==" rg -n -C 8 "handleSubmitRef\.current|onSendVoiceMessage:|onStart={handleSubmit}|onSubmit={handleSubmit}" web/oss/src/components/AgentChatSlice/AgentConversation.tsx echo echo "== filesToParts implementation ==" sed -n '20,36p' web/oss/src/components/AgentChatSlice/assets/files.ts echo echo "== structural search for catch around AgentConversation handleSubmitRef.current current invocations ==" python3 - <<'PY' from pathlib import Path p = Path('web/oss/src/components/AgentChatSlice/AgentConversation.tsx') s = p.read_text() idx = s.find('handleSubmitRef.current = handleSubmit') print('handleSubmitRef.current index', idx) print(s[max(0, idx-1200):idx+1200]) PYRepository: Agenta-AI/agenta
Length of output: 3742
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== AgentConversation handleSubmitRef and call sites ==" rg -n -C 8 "handleSubmitRef\.current|onSendVoiceMessage:|onStart={handleSubmit}|onSubmit={handleSubmit}" 'web/oss/src/components/AgentChatSlice/AgentConversation.tsx' echo echo "== filesToParts implementation ==" sed -n '20,36p' web/oss/src/components/AgentChatSlice/assets/files.ts echo echo "== deterministic behavioral probe for filesToParts missing attachment ==" node - <<'JS' function filesToParts(files, sessionId) { return files.map((file) => { const attachment = file.response?.attachment if (!attachment) throw new Error(`Attachment upload is incomplete: ${file.name}`) return { type: "file", mediaType: attachment.media_type, filename: attachment.filename, url: "url", providerMetadata: {agenta: {attachmentId: attachment.attachment_id, size: attachment.size}}, } }) } const payload = {type: "audio", mediaType: "audio/pcm", data: Buffer.from([1,2,3]), name: "takes.wav"} try { filesToParts([payload], "session-1") console.log("unexpected success") } catch (e) { console.log("throws:", e.message) } JSRepository: Agenta-AI/agenta
Length of output: 448
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== AgentConversation handleSubmitRef and call sites ==" rg -n -C 8 "handleSubmitRef.current|onSendVoiceMessage:|onStart={handleSubmit}|onSubmit={handleSubmit}" web/oss/src echo echo "== AgentConversation relevant section ==" sed -n '390,475p' web/oss/src echo echo "== filesToParts implementation ==" sed -n '20,36p' web/oss/src echo echo "== deterministic behavioral probe for filesToParts missing attachment ==" node - <<'JS' function filesToParts(files) { return files.map((file) => { const attachment = file.response?.attachment if (!attachment) throw new Error(`Attachment upload is incomplete: ${file.name}`) return { type: "file", mediaType: attachment.media_type, filename: attachment.filename, url: "url", providerMetadata: {agenta: {attachmentId: attachment.attachment_id, size: attachment.size}}, } }) } const payload = {type: "audio", mediaType: "audio/pcm", data: Buffer.from([1,2,3]), name: "takes.wav"} try { filesToParts([payload]) console.log("unexpected success") } catch (e) { console.log("throws:", e.message) } JSRepository: Agenta-AI/agenta
Length of output: 446
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== AgentConversation handleSubmitRef and call sites ==" rg -n -C 8 'handleSubmitRef\.current|onSendVoiceMessage:|onStart=\{handleSubmit\}|onSubmit=\{handleSubmit\}' web/oss/src echo echo "== AgentConversation relevant section ==" sed -n '390,475p' web/oss/src/components/AgentChatSlice/AgentConversation.tsx echo echo "== filesToParts implementation ==" sed -n '20,36p' web/oss/src/components/AgentChatSlice/assets/files.ts echo echo "== deterministic behavioral probe for filesToParts missing attachment ==" node - <<'JS' function filesToParts(files) { return files.map((file) => { const attachment = file.response?.attachment if (!attachment) throw new Error(`Attachment upload is incomplete: ${file.name}`) return { type: "file", mediaType: attachment.media_type, filename: attachment.filename, url: "url", providerMetadata: {agenta: {attachmentId: attachment.attachment_id, size: attachment.size}}, } }) } const payload = {type: "audio", mediaType: "audio/pcm", data: Buffer.from([1,2,3]), name: "takes.wav"} try { filesToParts([payload]) console.log("unexpected success") } catch (e) { console.log("throws:", e.message) } JSRepository: Agenta-AI/agenta
Length of output: 23202
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== Candidate AgentChatSubmit/Sender Submit interfaces ==" rg -n -C 6 'interface .*Submit|type .*Submit|onSubmit|onSendVoiceMessage|AgentChatSubmit|Sender' web/oss/src/components/Player web/oss/src/components/Composer web/oss/src/components/AgentChatSlice/components web/oss/src/components/AgentChatSlice/hooks useVoiceComposer.ts | head -n 240 echo echo "== locate submit handlers in AgentChatSlice ==" rg -n -C 6 'onSubmit|onSendVoiceMessage|AgentChatSubmit|useVoiceComposer' web/oss/src/components/AgentChatSlice -g '*.tsx' -g '*.ts' | head -n 300Repository: Agenta-AI/agenta
Length of output: 9064
Catch incomplete tray uploads in the upload send path.
uploadExtraFiles()returnsnullon failure and stores the failed entry in the tray, butfilesToParts()still throws when a tray entry has noresponse?.attachment. SincehandleSubmitis passed directly toonSendVoiceMessage,onStart, andonSubmit, convertfilesToParts()failures intoattachments.setRejections(...), or reject incomplete tray entries before creating file parts.web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts (1)
105-112: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail resources_file="$(fd -a '^resources\.ts$' web/packages/agenta-sdk/src | head -n 1)" test -n "$resources_file" ast-grep outline "$resources_file" --items all rg -n -C 3 'attachment|session' "$resources_file"Repository: Agenta-AI/agenta
Length of output: 3918
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== resources.ts ==" sed -n '1,180p' web/packages/agenta-sdk/src/resources.ts echo "== attachmentTransport.ts outline ===" ast-grep outline web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts --items all || true echo "== attachmentTransport.ts snippet ==" sed -n '1,170p' web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts echo "== session accessor candidates ==" fd -a '^sessions\.ts$' web/packages/agenta-sdk/src web/packages/agenta-sdk/src 2>/dev/null || true files=$(fd -a '_sessions.*|sessions.*|attachments.*|attachment.*' web/web | head -n 50 || true) echo "$files" rg -n -C 4 'attachments|session_id|createAttachment|Attachment|upload.*progress|onUploadProgress' web/packages/ web/oss/src/components/AgentChatSlice || trueRepository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== resources.ts relevant symbols ==" sed -n '1,120p' web/packages/agenta-sdk/src/resources.ts echo "== file size + attachmentTransport relevant lines ==" wc -l web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts sed -n '1,140p' web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts echo "== SDK package exports/imports ==" sed -n '1,160p' web/packages/agenta-sdk/src/package.json 2>/dev/null || cat web/packages/agenta-sdk/package.json sed -n '1,100p' web/packages/agenta-sdk/src/index.ts 2>/dev/null || true echo "== targeted SDK attachment/session method names ==" rg -n -C 4 'getSessionsClient|getLowPrioritySessionsClient|Session|attachments|attachment|upload|progress|FormData' web/packages/agenta-sdk/src/resources.ts web/packages/agenta-sdk/src config -g '!node_modules' -g '!dist' -g '!build' || true echo "== package client session resource snippet ==" fd -a 'sessions.*\.ts$|sessions\.ts$' web/packages web/oss | head -n 20 file="$(fd -a 'sessions.*\.ts$|sessions\.ts$' web/packages web/oss | head -n 1)" if [ -n "${file:-}" ]; then wc -l "$file" sed -n '1,220p' "$file" fiRepository: Agenta-AI/agenta
Length of output: 25681
Move attachment upload behind a Fern resource accessor.
uploadAttachmentstill calls Axios directly and uses Axiosparamsforsession_id. Add an attachment upload method towps/packages/agenta-sdk/src/resources.ts/SessionsClient, keep upload progress on that accessor, and call it fromuploadAttachment.Source: Coding guidelines
web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts (1)
113-124: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Honor
NEXT_PUBLIC_AGENT_FILE_UPLOADS=falsefor extra files.
uploadExtraFilescallsattachmentUploadereven whenuploadsEnabledis false. A voice take that uses this bypass-tray path can therefore create an attachment after uploads were disabled.Return the completed
stagedentries beforePromise.allSettledwhenuploadsEnabledis false.Proposed fix
const uploadExtraFiles = async (extraFiles: File[]): Promise<StagedFile[] | null> => { const staged = extraFiles.map((file) => toUploadFile(file, uploadsEnabled)) + if (!uploadsEnabled) return staged + const results = await Promise.allSettled(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const uploadExtraFiles = async (extraFiles: File[]): Promise<StagedFile[] | null> => { const staged = extraFiles.map((file) => toUploadFile(file, uploadsEnabled)) if (!uploadsEnabled) return staged const results = await Promise.allSettled( staged.map(async (entry) => { const response = await attachmentUploader(entry.originFileObj as File, { uid: entry.uid, onProgress: () => undefined, signal: new AbortController().signal, }) return {...entry, status: "done" as const, percent: 100, response} }), )web/oss/src/components/Drives/driveMedia.ts (1)
187-196: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== package config ==" for f in package.json web/package.json package-lock.json web/package-lock.json pnpm-lock.yaml web/pnpm-lock.yaml; do if [ -f "$f" ]; then echo "--- $f" case "$f" in *package.json) jq '.dependencies, .devDependencies' "$f" | rg '"axios"|"`@axios`|axios' -C 2 || true ;; pnpm-lock.yaml|pnpm-lock.yaml) rg -n "axios(@|:)" "$f" | head -80 || true ;; esac fi done echo echo "== locate relevant source ==" git ls-files | rg '(^web/oss/src/components/Drives/driveMedia\.ts$|axios|api|shared.*axios|axios.*instance)' || true echo echo "== target lines ==" if [ -f web/oss/src/components/Drives/driveMedia.ts ]; then wc -l web/oss/src/components/Drives/driveMedia.ts sed -n '150,220p' web/oss/src/components/Drives/driveMedia.ts | nl -ba -v150 fi echo echo "== axios usages and instances ==" rg -n "new Axios|axios|create\\(" web/oss/src web/shared web/packages web/src 2>/dev/null | rg -i 'axios|instance|headers|headers:' | head -160 || true echo echo "== behavioral probe with installed axios if available (no repo code exec) ==" node - <<'JS' try { const axios = require('axios') const pkg = require('axios/package.json') console.log('axios version via require:', pkg.version) console.log('axios config type:', Object.prototype.toString.call(axios), typeof axios) // This does not execute request; Axios is read as a module class. } catch (e) { console.log('axios package not resolvable from /tmp scratch:', e.name + ': ' + e.message) } JSRepository: Agenta-AI/agenta
Length of output: 50375
🌐 Web query:
axios browser FormData Content-Type multipart/form-data boundary documentation💡 Result:
When using Axios in the browser, you should generally NOT manually set the Content-Type header for FormData objects [1][2][3]. The browser runtime automatically sets the correct Content-Type, including the required multipart boundary, when it detects a FormData instance [1][3][4]. Manually overriding this header can prevent the browser from appending the necessary boundary, which will cause your request to fail or be misinterpreted by the server [1][5]. Key takeaways: 1. Browser Behavior: Pass your FormData instance directly to the Axios request. Axios will allow the browser to handle the Content-Type header and boundary generation automatically [1][3][4]. 2. Automatic Serialization: Since version 0.27.0, if you wish to have Axios automatically convert a plain JavaScript object into FormData, you should set the Content-Type header to multipart/form-data [1][6][7]. Axios will then handle the serialization and boundary generation [6][4]. 3. Shortcut Methods: You can use the Axios shortcut methods postForm, putForm, or patchForm, which automatically set the Content-Type to multipart/form-data for you, simplifying the process [1][6][3]. 4. Node.js Environment: While the browser handles this automatically, note that in Node.js, Axios relies on the FormData object (often from the form-data package) to provide headers [1][2][3]. In cases where you must manually intervene (e.g., specific library limitations), the common workaround involves appending the boundary generated by the FormData instance itself (e.g., FormData#getBoundary) to the Content-Type header [5]. However, for most standard use cases in modern Axios versions, this is handled by the library [1][8][3].
Citations:
- 1: https://github.com/axios/axios/blob/e435384f/docs/pages/advanced/multipart-form-data-format.md
- 2: https://axios.rest/pages/advanced/multipart-form-data-format
- 3: https://github.com/axios/axios
- 4: https://www.mintlify.com/axios/axios/advanced/form-data
- 5: axios/axios#5379
- 6: https://github.com/axios/axios-docs/blob/master/posts/en/multipart.md
- 7: https://github.com/axios/axios/blob/main/README.md
- 8: axios/axios#6998
🏁 Script executed:
#!/bin/bash set -u echo "== target file ==" if [ -f web/oss/src/components/Drives/driveMedia.ts ]; then wc -l web/oss/src/components/Drives/driveMedia.ts sed -n '1,240p' web/oss/src/components/Drives/driveMedia.ts | nl -ba fi echo echo "== focused dependency lock/manifest entries ==" rg -n '"axios"|"axios' pnpm-lock.yaml web/pnpm-lock.yaml package.json web/package.json --glob 'pnpm-lock.yaml' --glob 'package.json' | head -200 || true echo echo "== focused axios usages ==" rg -n "import .*axios|from ['\"][^'\"]*axios|axios\.post|axios\.create|Content-Type|FormData|uploads|files/upload" web --glob '*.{ts,tsx,js,jsx}' | head -240 || true echo echo "== read-only behavioral probe: axios module source snippets if node_modules exists ==" if [ -f node_modules/axios/index.js ]; then echo "axios node_modules present" node - <<'JS' const axios = require('axios') const pkg = require('axios/package.json') console.log('package axios:', pkg.version) const source = require('fs').readFileSync(require.resolve('axios'), 'utf8') const keywords = ['_toFormData', 'isFormData', 'resolveConfig', 'dispatchRequest', 'platform', 'setContentTypeIfUnset', 'Content-Type', 'boundary'] for (const k of keywords) { const idx = source.indexOf(k) console.log(k, idx >= 0 ? idx : 'not found') } JS else echo "axios node_modules absent" fiRepository: Agenta-AI/agenta
Length of output: 27717
🏁 Script executed:
#!/bin/bash set -u echo "== target source ==" if [ -f web/oss/src/components/Drives/driveMedia.ts ]; then sed -n '1,240p' web/oss/src/components/Drives/driveMedia.ts fi echo echo "== shared axios config ==" if [ -f web/packages/agenta-shared/src/api/axios.ts ]; then sed -n '1,140p' web/packages/agenta-shared/src/api/axios.ts fi echo echo "== existing upload tests around FormData and headers ==" sed -n '298,345p' web/packages/agenta-entities/src/testset/api/mutations.ts sed -n '125,135p' web/packages/agenta-shared/src/api/axios.tsRepository: Agenta-AI/agenta
Length of output: 15880
🌐 Web query:
axios 1.8 formData browser setContentTypeIfUnset Content-Type undefined💡 Result:
When using Axios to send FormData in the browser, you should generally not manually set the Content-Type header [1][2]. When a FormData object is passed as the request payload, Axios recognizes it and allows the browser to automatically set the Content-Type header to multipart/form-data, including the required boundary [3][4]. If you manually set the Content-Type header to multipart/form-data yourself, the browser will be unable to append the dynamically calculated boundary parameter required for the multipart request, which often results in server-side parsing errors [5]. Regarding the behavior of setContentTypeIfUnset and undefined headers: 1. Automatic Handling: Axios is designed to automatically manage headers based on the data type [6][7]. When data is an object, Axios defaults to application/json; when it detects FormData, it handles it as multipart [8]. 2. The setContentTypeIfUnset Function: This internal helper function is used by Axios to ensure that a default Content-Type is applied only if the user has not explicitly provided one [9][7]. If you explicitly set Content-Type to undefined in your headers config, it may not behave as an "unset" state in some versions or configurations, potentially leading to conflicts where Axios falls back to its default (application/x-www-form-urlencoded) instead of properly detecting the FormData [5][9][7]. 3. Recommended Approach: - To send FormData, pass the FormData instance directly in the data property and remove any manual Content-Type header [1][4]. - If you require a multipart request for a plain JavaScript object, ensure the Content-Type header is set to multipart/form-data [1][10]. Axios will then automatically serialize the object into FormData [2][11]. - If using interceptors, ensure they do not unintentionally overwrite or enforce a default Content-Type (like application/x-www-form-urlencoded) for requests containing FormData [5][6][9]. - Use the built-in shortcut methods like axios.postForm, axios.putForm, or axios.patchForm, which are designed to handle this automatically [1][4]. If you are experiencing issues with incorrect Content-Type headers, verify if your interceptors or custom axios instances are forcing a header value, effectively bypassing the logic that would otherwise correctly detect your payload [5][6].
Citations:
- 1: https://axios.rest/pages/advanced/multipart-form-data-format
- 2: https://github.com/axios/axios/blob/e435384f/docs/pages/advanced/multipart-form-data-format.md
- 3: https://npmx.dev/package/axios/v/1.8.3
- 4: https://www.mintlify.com/axios/axios/advanced/form-data
- 5: axios/axios#3857
- 6: https://stackoverflow.com/questions/70947934/using-axios-transformrequest-results-in-415-unsupported-media-type
- 7: https://stackoverflow.com/questions/63184723/why-does-axios-send-my-post-request-with-content-type-application-x-www-form-url
- 8: axios/axios@73e3bdb
- 9: https://stackoverflow.com/questions/62594185/axios-in-react-app-posting-image-with-form-data-sends-request-with-empty-body
- 10: https://github.com/axios/axios/blob/main/README.md
- 11: https://github.com/axios/axios-docs/blob/master/posts/en/multipart.md
Do not hard-code the multipart
Content-TypeforFormData.At line 190,
uploadMountFilepasses a browserFormDatabody withheaders: {"Content-Type": "multipart/form-data"}. That omits theboundaryparameter the browser needs to compose multipart payload parsing. Axios 1.x should let the browser setContent-TypeforFormData; if this header overrides that, uploads can fail because the backend cannot split the request into parts. Set the header only to break JSON serialization, and use the boundary-free form only if the HTTP adapter/runtime guarantees browser multipart generation survives it.
The v0.107.0 release added the
AGENTA_ATTACHMENTS_*family (per-file size caps, per-session quotas, lifecycle TTLs, plus the runner's per-turn cap and fetch timeout). All twelve have code defaults and none is required or secret, but nothing documented them.This adds a reference section to the self-hosting configuration page (variable, env.py path, what it bounds, default, with the runner-side pair listed separately and the runner's hard 16 MB per-attachment ceiling stated as a constraint) and mirrors the family as commented-out blocks in all four docker-compose env example files, following each file's existing style.
Found while writing, left for follow-ups: the env examples point at a docs slug that does not exist (
/self-host/agent-execution/runner-configuration), and the three session-storage flags in the examples are also absent from the configuration page.https://claude.ai/code/session_01McMogkcDRV7UpSAjfd8VKG