Skip to content

docs(self-host): document the attachment limit env vars - #5647

Merged
mmabrouk merged 1 commit into
release/v0.107.0from
docs/attachment-env-reference
Aug 2, 2026
Merged

docs(self-host): document the attachment limit env vars#5647
mmabrouk merged 1 commit into
release/v0.107.0from
docs/attachment-env-reference

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 2, 2026

Copy link
Copy Markdown
Member

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

@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 2, 2026 10:10am

Request Review

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. documentation Improvements or additions to documentation labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added durable session attachments with upload, download, referencing, validation, quotas, previews, retries, and cleanup.
    • Added multimodal agent support for images, audio, documents, workspace copies, replay, and capability-aware delivery.
    • Enabled file uploads by default and improved drag-and-drop support for nested files.
    • Default agents now include Pi’s read, bash, edit, and write tools.
  • Bug Fixes
    • Improved approval resumption, attachment persistence, protected mount handling, and structured run errors.
  • Documentation
    • Added configuration guidance and comprehensive multimodality and built-in tool documentation.

Walkthrough

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

Changes

Session attachment resource

Layer / File(s) Summary
Storage and lifecycle
api/oss/src/core/sessions/attachments/*, api/oss/src/dbs/postgres/sessions/attachments/*, api/oss/databases/postgres/migrations/*
Adds attachment DTOs, media classification, quotas, idempotent pending and ready states, PostgreSQL persistence, protected original storage, and cleanup sweeps.
API and configuration
api/entrypoints/routers.py, api/oss/src/apis/fastapi/sessions/*, api/oss/src/utils/env.py, hosting/*
Adds upload, download, and reference routes with authorization, bounded reads, error mapping, limits, configuration, and deployment settings.
Runner delivery and replay
services/runner/src/protocol.ts, services/runner/src/server.ts, services/runner/src/engines/sandbox_agent/*, services/runner/src/sessions/*
Adds attachment references, current-turn parsing, capability-gated native delivery, workspace materialization, history restoration, delivery events, and attachment-aware approval resumes.
SDK and frontend integration
sdks/python/agenta/sdk/agents/*, web/oss/src/components/AgentChatSlice/*, web/packages/agenta-ui/*
Adds attachment wire models, model modalities, typed run failures, upload retry handling, authenticated rendering, replay, send blocking, and accessibility updates.
Drive uploads and validation
web/oss/src/components/Drives/*, api/oss/tests/*, services/runner/tests/*, web/oss/tests/*
Preserves dropped-file paths, limits upload concurrency, adds attachment and protected-mount tests, and adds end-to-end upload and reload coverage.

Default Pi built-ins

Layer / File(s) Summary
Default grants and harness behavior
sdks/python/agenta/sdk/agents/pi_builtins.py, sdks/python/agenta/sdk/utils/types.py, services/oss/src/agent/config.py, services/runner/config/agent.json, .agents/skills/agent-release-gate/resources/*
Defines Pi’s default read, bash, edit, and write tools, preserves explicit empty-tool behavior, and applies harness-specific release-gate defaults.
Wire, UI, and parity coverage
sdks/python/oss/tests/*, services/oss/tests/*, services/runner/tests/*, web/packages/agenta-entity-ui/*, web/packages/agenta-playground/*
Validates default-tool propagation, cross-language parity, Claude warning behavior, tool naming, overlay merging, and permission gating.
Documentation
docs/design/agent-workflows/documentation/*, docs/design/agent-workflows/interfaces/*, docs/design/agent-workflows/projects/default-agent-builtins/*
Documents default-tool semantics, harness behavior, empty-list behavior, parity requirements, and saved-agent limitations.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

  • Agenta-AI/agenta#5607: Adds the session attachment resource, protected mounts, DAO/service wiring, migrations, sweeps, and tests.
  • Agenta-AI/agenta#5615: Adds the runner current-user-turn and attachment-delivery changes extended here.
  • Agenta-AI/agenta#5597: Adds the Pi default built-in tools included and extended here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.76% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: documenting attachment limit environment variables for self-hosted deployments.
Description check ✅ Passed The description directly explains the documented attachment environment variables, configuration reference, and Docker Compose examples.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/attachment-env-reference

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk
mmabrouk changed the base branch from main to release/v0.107.0 August 2, 2026 10:10
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-08-02T10:14:13.745Z

@mmabrouk
mmabrouk merged commit 32fd38e into release/v0.107.0 Aug 2, 2026
44 of 46 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Add the missing runner golden fixture.

loadGolden("run_request.attachment.json") reads from sdks/python/oss/tests/pytest/unit/agents/golden/, but this test is under services/runner/tests/unit/wire-contract.test.ts and 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 win

Use default_tools(cell["harness"]) in the template() fallback.

j1_chat and j6_warm call template(cell) without explicit tools, so Pi harnesses continue with "tools": []. That still gates release on a tool-less configuration, which default_tools() says should be avoided.

api/oss/src/apis/fastapi/mounts/router.py-323-323 (1)

323-323: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep PublicMountCreate aligned with MountCreate.

MountCreateRequest.mount uses PublicMountCreate, but the router converts it to MountCreate(**body.mount.model_dump()). If PublicMountCreate adds fields that MountCreate does not declare, Pydantic will reject the request; if MountCreate requires fields that PublicMountCreate does 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 win

Align 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 win

Update the stale current-state descriptions.

Lines 54-58 state that uploads are off by default and paste or drag paths are not gated. isAgentFileUploadsEnabled now defaults to enabled, and the attachment hook gates these paths.

Lines 840-857 state a five-file limit, omit other files, and say no uploader exists. The current defaults allow 100 files, enable other, and call uploadAttachment.

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 win

Require 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_result followed 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 win

Restore 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 in finally.

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 win

Document the upload enablement setting.

The reference lists only limit variables. hosting/docker-compose/oss/env.oss.dev.example Line 123 enables NEXT_PUBLIC_AGENT_FILE_UPLOADS separately. 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 win

Document 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 win

Do not report the client default as the server limit.

AGENTA_ATTACHMENTS_MAX_*_BYTES can change the API cap. This branch always formats DEFAULT_ATTACHMENT_LIMITS, so a 413 from 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 win

Align the requireNative reason code with the computed modality state.

modelModalityState returns only "supported" or "unknown". Line 464 documents why: a catalog modality list is a positive declaration, so absence is not a negative capability. The requireNative branch at line 515 then reports model_modality_unsupported for that same "unknown" state.

The emitted reason code therefore asserts a fact the gate never established. An operator reading model_modality_unsupported in 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 as model_modality_required_unknown.

Note that services/runner/tests/unit/attachment-capability-gate.test.ts line 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 win

Consider deleting the object before dropping the pending row on a write failure.

The quota path at Lines 148-159 keeps the PENDING row 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. If write_attachment_original fails 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,
+                )
             raise
api/oss/tests/pytest/acceptance/conftest.py (1)

10-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider 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 integration acceptance test, and CI stays green with no database coverage. Add an env flag (for example AGENTA_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 value

Derive the caps from configuration instead of hardcoding them.

_AUDIO_CAP_BYTES and the 10 MiB image payload duplicate the defaults in api/oss/src/utils/env.py. If a deployment sets AGENTA_ATTACHMENTS_MAX_AUDIO_BYTES or AGENTA_ATTACHMENTS_MAX_IMAGE_BYTES, these tests fail for the wrong reason. Read the values from the shared env object.

♻️ 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 tradeoff

The fake dispatches on rendered SQL text, which couples the test to query shape.

FakeMarkReadySession.execute branches on substrings such as "FOR UPDATE" and "count(session_attachments.id)". A benign DAO query change, for example adding a count() to the locked read or renaming the aggregate, silently routes to the wrong branch or raises AssertionError. Consider dispatching on the statement's structural form, or move this coverage to the real DAO integration test that already exists in api/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 value

Size the fixture to the maximum attachments one message can carry.

The test uses 5 attachments. The configured limit max_pending_per_session defaults 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 lift

Add coverage for useAttachmentUploads's core upload/retry/abort flow.

This file tests only removeUploadFile and isUploadRetryReady. The hook itself — enqueue, run, retry, canRetry, abort, and the remount-resumption and unmount-cleanup effects — has no test here. This logic manages AbortController lifecycles, 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 renderHook tests, using fake timers and a mock AttachmentUploader, for at least: a successful upload patches status: "done" with the response; a failed upload with retryAfterSeconds blocks retry until the deadline and then runs; abort cancels the in-flight controller and clears pending retry state.

web/oss/src/components/Drives/dropEntries.ts (1)

66-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize child reads in collectDropEntry.

The for...of loop awaits each child sequentially. Each child is an independent async read (entryFile or a nested readAllEntries call), 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.all to 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 value

Derive the symlink-check timeout from the configured restore timeout.

Line 345 hardcodes timeoutMs: 15_000. attachmentRestoreTimeoutMs() already exposes the same default through AGENTA_ATTACHMENTS_RESTORE_TIMEOUT_MS. rejectDaytonaSymlinks issues one runProcess call 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 value

Abort the attachment fetch when withTimeout fires.

withTimeout only clears the timer, not fetchAttachment; fetchAttachment creates its own AbortSignal.timeout(), so a restore timeout can leave the download running past the worker failure. Pass a shared AbortSignal into fetchAttachment and 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 value

Use a template literal to match the adjacent helper.

toolDeliveryUnsupportedMessage at lines 51-61 uses a template literal with literal ' characters. This function uses \x27 escapes 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 value

Clarify the .png fixture, and strengthen the assertion it feeds.

Line 180 names the pre-existing working copy `.png`. The value is a valid basename, so validatedFilename accepts 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-disposition at line 201 returns the same .png filename, 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 value

Use sep instead of a hardcoded /.

attachments.ts line 227 builds the prefix with sep from node:path, so the production check is platform-independent. This assertion hardcodes "/" and would fail on a Windows runner. Line 66 can keep /, because relative is built with posix.join by 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 value

This duplicates coverage in attachment-capability-gate.test.ts.

services/runner/tests/unit/attachment-capability-gate.test.ts lines 24-37 already assert a native outcome for both claude and pi with 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 win

Reuse the shared approvalReplyMessage fixture 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 value

Drop the redundant double cast.

ContentBlock already declares type, attachmentId, filename, mimeType, and size. The as unknown as ContentBlock cast 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 win

Expose disabledReason to 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 disabled and therefore out of the tab order, and the span is not focusable. Enter-to-send is blocked in SubmitPlugin at 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-live region 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 win

Remove the committed test-results artifact.

This file is a generated Vitest JUnit report (testsuites/testcase output), 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 win

Add cleanup for the attachmentBlobQueryFamily atom cache.

attachmentBlobQueryFamily is a plain atomFamily. Jotai's own documentation states that internally an atom family is just a Map, and "Unless you explicitly remove unused params, this leads to memory leaks." Only the query data has gcTime: 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 call attachmentBlobQueryFamily.remove(param) when a component unmounts, to bound the cache size.


66-98: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize onError in useAttachmentMediaSrc.

onError: () => setMode("blob") is a new function on every render. AgentMessage.tsx's AttachmentFilePart (lines 296-297) passes this value directly into imageProps={{onError: source.onError}} and videoProps={{onError: source.onError}}, creating a new object each render regardless of whether mode, sessionId, or attachmentId changed. This defeats any memoization on FileCard or the underlying <img>/<video> elements.

Wrap onError with useCallback so its identity stays stable across renders that do not change mode.

♻️ 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 win

Show a loading state for audio attachments during blob fallback.

For kind === "image", "video", and "file", FileCard receives loading={attachmentId ? source.isPending : false}. For kind === "audio", no equivalent state reaches AudioPlayer; src ?? "" renders an empty src on the <audio> element while source.isPending is true (blob fallback in flight). An empty src can trigger an unwanted load/error cycle on some browsers and gives the user no feedback that content is loading.

Add a loading prop to AudioPlayer (or gate the audio player behind a placeholder) while source.isPending is true.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f184346-0b21-496d-b8ec-145fba9cc2c0

📥 Commits

Reviewing files that changed from the base of the PR and between 7c395db and 7e55e72.

⛔ Files ignored due to path filters (21)
  • api/uv.lock is excluded by !**/*.lock
  • clients/python/uv.lock is excluded by !**/*.lock
  • sdks/python/uv.lock is excluded by !**/*.lock
  • services/uv.lock is excluded by !**/*.lock
  • web/packages/agenta-api-client/src/generated/api/resources/mounts/client/requests/MountCreateRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/CreateSessionAttachmentRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/DownloadSessionAttachmentContentRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionAttachmentReferenceRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionInteractionTransitionRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/index.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/Mount.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/PublicMountCreate.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionAttachment.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionAttachmentResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionAttachmentsResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionInteractionData.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionInteractionRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionInteractionResolution.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionMount.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/index.ts is 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.py
  • api/entrypoints/routers.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000020_add_session_attachments.py
  • api/oss/src/apis/fastapi/mounts/models.py
  • api/oss/src/apis/fastapi/mounts/router.py
  • api/oss/src/apis/fastapi/sessions/models.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/core/mounts/dtos.py
  • api/oss/src/core/mounts/service.py
  • api/oss/src/core/mounts/types.py
  • api/oss/src/core/sessions/attachments/__init__.py
  • api/oss/src/core/sessions/attachments/dtos.py
  • api/oss/src/core/sessions/attachments/interfaces.py
  • api/oss/src/core/sessions/attachments/media.py
  • api/oss/src/core/sessions/attachments/service.py
  • api/oss/src/core/sessions/attachments/types.py
  • api/oss/src/core/sessions/interactions/dtos.py
  • api/oss/src/dbs/postgres/mounts/dao.py
  • api/oss/src/dbs/postgres/mounts/dbas.py
  • api/oss/src/dbs/postgres/mounts/mappings.py
  • api/oss/src/dbs/postgres/sessions/attachments/__init__.py
  • api/oss/src/dbs/postgres/sessions/attachments/dao.py
  • api/oss/src/dbs/postgres/sessions/attachments/dbas.py
  • api/oss/src/dbs/postgres/sessions/attachments/dbes.py
  • api/oss/src/dbs/postgres/sessions/attachments/mappings.py
  • api/oss/src/tasks/asyncio/sessions/attachment_sweep.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/acceptance/conftest.py
  • api/oss/tests/pytest/acceptance/mounts/test_attachments_mount_hidden.py
  • api/oss/tests/pytest/acceptance/sessions/test_session_attachment_teardown.py
  • api/oss/tests/pytest/acceptance/sessions/test_session_attachments.py
  • api/oss/tests/pytest/unit/mounts/test_agent_mounts.py
  • api/oss/tests/pytest/unit/mounts/test_protected_mount_policy.py
  • api/oss/tests/pytest/unit/resources/test_workflow_catalog.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_dao_integration.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_filename.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_media.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_quotas.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_state_machine.py
  • api/oss/tests/pytest/unit/sessions/test_attachment_sweep.py
  • api/oss/tests/pytest/unit/sessions/test_interaction_request_tool_call_id.py
  • api/oss/tests/pytest/unit/sessions/test_records_truncation.py
  • api/oss/tests/pytest/unit/sessions/test_wp5_dao_fanout.py
  • api/pyproject.toml
  • clients/python/pyproject.toml
  • docs/design/agent-workflows/documentation/agent-configuration.md
  • docs/design/agent-workflows/documentation/tools.md
  • docs/design/agent-workflows/interfaces/README.md
  • docs/design/agent-workflows/interfaces/in-service/harness-adapters.md
  • docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md
  • docs/design/agent-workflows/projects/agent-multi-modality/README.md
  • docs/design/agent-workflows/projects/agent-multi-modality/context.md
  • docs/design/agent-workflows/projects/agent-multi-modality/decisions.md
  • docs/design/agent-workflows/projects/agent-multi-modality/design.md
  • docs/design/agent-workflows/projects/agent-multi-modality/plan.md
  • docs/design/agent-workflows/projects/agent-multi-modality/plans/stage-1-implementation.md
  • docs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-0.md
  • docs/design/agent-workflows/projects/agent-multi-modality/protocols/stage-1.md
  • docs/design/agent-workflows/projects/agent-multi-modality/research.md
  • docs/design/agent-workflows/projects/agent-multi-modality/scope.md
  • docs/design/agent-workflows/projects/agent-multi-modality/status.md
  • docs/design/agent-workflows/projects/default-agent-builtins/README.md
  • docs/design/agent-workflows/projects/default-agent-builtins/context.md
  • docs/design/agent-workflows/projects/default-agent-builtins/design.md
  • docs/design/agent-workflows/projects/default-agent-builtins/open-questions.md
  • docs/design/agent-workflows/projects/default-agent-builtins/plan.md
  • docs/design/agent-workflows/projects/default-agent-builtins/research.md
  • docs/design/agent-workflows/projects/default-agent-builtins/status.md
  • docs/design/agent-workflows/projects/default-agent-builtins/testing.md
  • docs/docs/self-host/reference/01-configuration.mdx
  • hosting/docker-compose/ee/env.ee.dev.example
  • hosting/docker-compose/ee/env.ee.gh.example
  • hosting/docker-compose/oss/env.oss.dev.example
  • hosting/docker-compose/oss/env.oss.gh.example
  • hosting/docker-compose/oss/nginx/nginx.conf
  • hosting/kubernetes/helm/Chart.yaml
  • sdks/python/agenta/sdk/agents/__init__.py
  • sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py
  • sdks/python/agenta/sdk/agents/adapters/harnesses.py
  • sdks/python/agenta/sdk/agents/adapters/vercel/messages.py
  • sdks/python/agenta/sdk/agents/adapters/vercel/stream.py
  • sdks/python/agenta/sdk/agents/connections/models.py
  • sdks/python/agenta/sdk/agents/connections/resolver.py
  • sdks/python/agenta/sdk/agents/dtos.py
  • sdks/python/agenta/sdk/agents/errors.py
  • sdks/python/agenta/sdk/agents/model_catalog.py
  • sdks/python/agenta/sdk/agents/pi_builtins.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/agenta/sdk/agents/utils/wire.py
  • sdks/python/agenta/sdk/agents/wire_models.py
  • sdks/python/agenta/sdk/utils/types.py
  • sdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_messages_roundtrip.py
  • sdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_conformance.py
  • sdks/python/oss/tests/pytest/unit/agents/adapters/test_vercel_stream_multimodality.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_models.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_resolver.py
  • sdks/python/oss/tests/pytest/unit/agents/golden/pi_default_active_builtins.json
  • sdks/python/oss/tests/pytest/unit/agents/golden/run_request.attachment.json
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_content_blocks.py
  • sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py
  • sdks/python/oss/tests/pytest/unit/agents/test_pi_builtins_parity.py
  • sdks/python/oss/tests/pytest/unit/agents/test_result_multimodality.py
  • sdks/python/oss/tests/pytest/unit/agents/test_ui_messages.py
  • sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py
  • sdks/python/oss/tests/pytest/unit/agents/test_wire_models.py
  • sdks/python/pyproject.toml
  • services/oss/src/agent/config.py
  • services/oss/tests/pytest/unit/agent/test_config_template_fallback.py
  • services/oss/tests/pytest/unit/agent/test_default_agent_template.py
  • services/pyproject.toml
  • services/runner/config/agent.json
  • services/runner/src/engines/sandbox_agent/acp-interactions.ts
  • services/runner/src/engines/sandbox_agent/attachments.ts
  • services/runner/src/engines/sandbox_agent/capabilities.ts
  • services/runner/src/engines/sandbox_agent/engine.ts
  • services/runner/src/engines/sandbox_agent/reconstruct-history.ts
  • services/runner/src/engines/sandbox_agent/run-plan.ts
  • services/runner/src/engines/sandbox_agent/run-turn.ts
  • services/runner/src/engines/sandbox_agent/runtime-contracts.ts
  • services/runner/src/engines/sandbox_agent/session-identity.ts
  • services/runner/src/engines/sandbox_agent/session-pool.ts
  • services/runner/src/engines/sandbox_agent/transcript.ts
  • services/runner/src/protocol.ts
  • services/runner/src/server.ts
  • services/runner/src/sessions/attachments.ts
  • services/runner/src/sessions/interactions.ts
  • services/runner/src/sessions/reconstruct.ts
  • services/runner/tests/unit/attachment-capability-gate.test.ts
  • services/runner/tests/unit/attachment-client.test.ts
  • services/runner/tests/unit/attachment-delivery-events.test.ts
  • services/runner/tests/unit/attachment-materialize.test.ts
  • services/runner/tests/unit/attachment-path-safety.test.ts
  • services/runner/tests/unit/attachment-prompt-blocks.test.ts
  • services/runner/tests/unit/current-user-turn.test.ts
  • services/runner/tests/unit/pi-default-builtins-parity.test.ts
  • services/runner/tests/unit/reconstruct-resume-nonfatal.test.ts
  • services/runner/tests/unit/sandbox-agent-acp-interactions.test.ts
  • services/runner/tests/unit/sandbox-agent-capabilities.test.ts
  • services/runner/tests/unit/sandbox-agent-orchestration.test.ts
  • services/runner/tests/unit/sandbox-agent-run-plan.test.ts
  • services/runner/tests/unit/server.test.ts
  • services/runner/tests/unit/session-keepalive-approval.test.ts
  • services/runner/tests/unit/session-persist.test.ts
  • services/runner/tests/unit/session-pool.test.ts
  • services/runner/tests/unit/session-reconstruct-history.test.ts
  • services/runner/tests/unit/session-reconstruct.test.ts
  • services/runner/tests/unit/transcript.test.ts
  • services/runner/tests/unit/wire-contract.test.ts
  • web/ee/package.json
  • web/oss/package.json
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx
  • web/oss/src/components/AgentChatSlice/assets/attachmentMedia.ts
  • web/oss/src/components/AgentChatSlice/assets/attachmentTransport.test.ts
  • web/oss/src/components/AgentChatSlice/assets/attachmentTransport.ts
  • web/oss/src/components/AgentChatSlice/assets/attachments.test.ts
  • web/oss/src/components/AgentChatSlice/assets/attachments.ts
  • web/oss/src/components/AgentChatSlice/assets/constants.ts
  • web/oss/src/components/AgentChatSlice/assets/files.test.ts
  • web/oss/src/components/AgentChatSlice/assets/files.ts
  • web/oss/src/components/AgentChatSlice/assets/inFlightSubmit.test.ts
  • web/oss/src/components/AgentChatSlice/assets/inFlightSubmit.ts
  • web/oss/src/components/AgentChatSlice/assets/messageParts.ts
  • web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.test.ts
  • web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.ts
  • web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentTurn.tsx
  • web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx
  • web/oss/src/components/AgentChatSlice/components/AudioPlayer.tsx
  • web/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsx
  • web/oss/src/components/AgentChatSlice/components/ToolActivity.tsx
  • web/oss/src/components/AgentChatSlice/components/TranscriptPlaceholder.tsx
  • web/oss/src/components/AgentChatSlice/components/clientTools/meta.test.ts
  • web/oss/src/components/AgentChatSlice/components/clientTools/meta.ts
  • web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.test.ts
  • web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.ts
  • web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts
  • web/oss/src/components/AgentChatSlice/state/sessions.ts
  • web/oss/src/components/Drives/ContextRail.tsx
  • web/oss/src/components/Drives/DriveExplorer.tsx
  • web/oss/src/components/Drives/FilesDrawer.tsx
  • web/oss/src/components/Drives/SessionFilesDrawer.tsx
  • web/oss/src/components/Drives/chatFileRefs.tsx
  • web/oss/src/components/Drives/configDrive.ts
  • web/oss/src/components/Drives/driveMedia.ts
  • web/oss/src/components/Drives/dropEntries.test.ts
  • web/oss/src/components/Drives/dropEntries.ts
  • web/oss/src/components/Drives/useDriveDrop.ts
  • web/oss/src/components/Drives/useDriveUploads.ts
  • web/oss/src/components/Drives/useMountUpload.ts
  • web/oss/src/components/Drives/useSessionDrive.ts
  • web/oss/test-results/junit.xml
  • web/oss/tests/playwright/acceptance/agent-chat/attach-send-render-reload.spec.ts
  • web/package.json
  • web/packages/agenta-api-client/package.json
  • web/packages/agenta-entities/src/session/core/mountSelection.ts
  • web/packages/agenta-entities/src/session/index.ts
  • web/packages/agenta-entities/tests/unit/create-ephemeral-app-from-template.test.ts
  • web/packages/agenta-entities/tests/unit/session-pick-cwd-mount.test.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/PiSettingsControl.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/itemDescriptors.tsx
  • web/packages/agenta-entity-ui/tests/unit/itemDescriptors.test.ts
  • web/packages/agenta-playground/src/state/execution/agentApprovalResume.ts
  • web/packages/agenta-playground/tests/unit/agentApprovalResume.test.ts
  • web/packages/agenta-playground/tests/unit/agentMessageQueue.test.ts
  • web/packages/agenta-playground/tests/unit/agentRequest.test.ts
  • web/packages/agenta-ui/src/RichChatInput/RichChatInput.tsx
  • web/packages/agenta-ui/src/RichChatInput/plugins/SendButton.tsx
  • web/packages/agenta-ui/src/RichChatInput/plugins/SubmitPlugin.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 -S

Repository: 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.py

Repository: 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:


🏁 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' || true

Repository: Agenta-AI/agenta

Length of output: 9675


Make SessionAttachment.created_at optional or populate missing row values.

Lifecycle.created_at is nullable in LifecycleDBA (nullbale=True) and in the SDK DTO (Optional[datetime] = None/created_at=attachment_dbe.created_at). SessionAttachment.created_at is required, so a ready attachment loaded with created_at=None makes _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 use async with request.form() as form:, Starlette parses the request body and returns a FormData object [1][3]. If the form contains file uploads, these are represented as UploadFile instances, which use SpooledTemporaryFile internally to store data [1][3][4]. Using the context manager ensures that the close() 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 call await request.form() and then subsequently ensure await 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:


🏁 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' || true

Repository: 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)
PY

Repository: 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:


Close the parsed form when handling attachments.

multipart/form-data uploads are represented by UploadFile, which uses a spooled temporary file for large payloads. Use the form as an async context manager, or call await form.close() after reading file, 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.md and plans/stage-1-implementation.md use 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/tests

Repository: 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:


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 uses statFs/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 failed represents only a contract violation. It also lists fetch_failed as a reason code and requires missing, authorization, and timeout failures to produce structured errors. Specify whether these cases emit outcome: "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 addFiles below the NEXT_PUBLIC_AGENT_FILE_UPLOADS guard. Stage 2 enables NEXT_PUBLIC_AGENT_VOICE_INPUT separately. 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 mkdirFs and writeFsFile. rejectDaytonaSymlinks runs only before mkdirFs, when path.directory normally does not exist yet, so test -L exits 1 and the check passes without proving anything about the directory that mkdirFs then creates. The local path is protected by O_EXCL through linkSync and writeFileSync(..., {flag:"wx"}); the Daytona path has no equivalent, so writeFsFile follows whatever path.directory resolves 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-run rejectDaytonaSymlinks on path.directory and path.absolute after await sandbox.mkdirFs({ path: path.directory }) returns, before the statFs existence probe and the writeFsFile call.
  • services/runner/tests/unit/attachment-materialize.test.ts#L138-L143: extend the expected operations array so it includes the post-mkdir symlink 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.

restoreReferencedWorkingCopies wraps every fetchAttachment call in withTimeout(..., attachmentRestoreTimeoutMs()) and runs a bounded worker pool. This path does neither. Line 806 awaits fetchAttachment with no deadline, inside a strictly sequential for loop.

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 withTimeout budget here. Treat a timeout as fetch_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/hooks

Repository: 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.tsx

Repository: 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])
PY

Repository: 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)
}
JS

Repository: 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)
}
JS

Repository: 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)
}
JS

Repository: 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 300

Repository: Agenta-AI/agenta

Length of output: 9064


Catch incomplete tray uploads in the upload send path.

uploadExtraFiles() returns null on failure and stores the failed entry in the tray, but filesToParts() still throws when a tray entry has no response?.attachment. Since handleSubmit is passed directly to onSendVoiceMessage, onStart, and onSubmit, convert filesToParts() failures into attachments.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 || true

Repository: 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"
fi

Repository: Agenta-AI/agenta

Length of output: 25681


Move attachment upload behind a Fern resource accessor.

uploadAttachment still calls Axios directly and uses Axios params for session_id. Add an attachment upload method to wps/packages/agenta-sdk/src/resources.ts/SessionsClient, keep upload progress on that accessor, and call it from uploadAttachment.

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=false for extra files.

uploadExtraFiles calls attachmentUploader even when uploadsEnabled is false. A voice take that uses this bypass-tray path can therefore create an attachment after uploads were disabled.

Return the completed staged entries before Promise.allSettled when uploadsEnabled is 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)
}
JS

Repository: 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:


🏁 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"
fi

Repository: 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.ts

Repository: 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:


Do not hard-code the multipart Content-Type for FormData.

At line 190, uploadMountFile passes a browser FormData body with headers: {"Content-Type": "multipart/form-data"}. That omits the boundary parameter the browser needs to compose multipart payload parsing. Axios 1.x should let the browser set Content-Type for FormData; 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant