Skip to content

Support file attachments in Sessions - #1630

Merged
gjkim42 merged 1 commit into
mainfrom
agent/session-attachments
Aug 10, 2026
Merged

gjkim42 merged 1 commit into
mainfrom
agent/session-attachments

Conversation

@gjkim42

@gjkim42 gjkim42 commented Aug 10, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind feature

What this PR does / why we need it:

Adds persisted input attachments to interactive Sessions so users can provide images and other local files from either client.

  • stores validated attachments in the Session state directory with per-file, per-message, and per-Session limits
  • journals attachment metadata and restores it with queued and retained messages
  • maps raster images to native Codex image inputs, OpenCode attachments to file parts, and exposes local paths to every provider
  • adds authenticated multipart upload and download endpoints to the Session web server
  • adds file selection and drag-and-drop to the web composer
  • adds /attach PATH to terminal chat and recognizes bracketed-paste file drops in the interactive terminal UI
  • adds end-to-end coverage for web transfer, Pod recovery, history, reset cleanup, and terminal submission

Which issue(s) this PR is related to:

N/A

Special notes for your reviewer:

Attachments use the existing Session state volume. They survive Pod replacement only for Sessions configured with spec.volumeClaimTemplate; reset and deletion follow the existing workspace lifecycle. The current limits are 10 MiB per file, 8 files per message, and 100 MiB or 128 files per Session.

Verification completed with:

  • make verify
  • make test
  • make build

Does this PR introduce a user-facing change?

Sessions now support image and file attachments from the web composer and `kelos session connect`, including drag-and-drop in both interactive interfaces.

Summary by cubic

Add file attachments to Sessions across web and terminal. Users can attach images and other files; attachments are stored in Session state, included in turns and history, and persist across Pod replacement when using spec.volumeClaimTemplate.

  • New Features

    • Web composer: attach button and drag-and-drop, pending attachment chips, and inline image previews/downloads.
    • Terminal: /attach PATH, bracketed-paste drag-and-drop in the interactive UI, and /send to submit files without text.
    • Storage and limits: 10 MiB per file, up to 8 files per message, and up to 100 MiB or 128 files per Session. Attachments live in the Session state volume and follow the workspace lifecycle.
    • API and runtime: authenticated multipart upload POST /api/sessions/{ns}/{name}/attachments and download GET /api/sessions/{ns}/{name}/attachments/{id}; kelos-session-runtime attachment put|get; new internal/sessionattachment client using Pod exec.
    • Provider integration: raster images become native Codex local images; OpenCode receives file parts via file:// URLs; all providers receive a prompt that includes an attachment summary.
  • Migration

    • Providers: Provider.RunTurn now accepts TurnInput (text + attachments) instead of a string prompt. Update custom providers accordingly.
    • Socket clients: include attachmentIds on message requests if sending attachments.

Written for commit aec44d0. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added kind/feature Categorizes issue or PR as related to a new feature needs-triage needs-priority needs-actor release-note labels Aug 10, 2026
@gjkim42
gjkim42 marked this pull request as ready for review August 10, 2026 10:34
@gjkim42

gjkim42 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

/kelos claude-review

@kelos-bot

kelos-bot Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-claude-reviewer-issue-comment-a6529eab3d3d has succeeded. ✅

@kelos-bot

kelos-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Claude Reviewer Agent @gjkim42

Review Summary

Verdict: APPROVE
Overall correctness: patch is correct
Scope: Adds persisted input attachments to interactive Sessions (storage/quota, journaling, provider mapping, authenticated upload/download endpoints, web + terminal composers).

Findings Overview

Priority Count File:Line Summary
P0 0 — none
P1 0 — none
P2 1 internal/sessionserver/web/app.js:3283 submitComposer has no re-entrancy guard; double-submit during upload
P3 2 internal/cli/session_terminal.go:459 Plain terminal lacks the 8-file cap the TUI enforces; excess /attach uploads orphan quota
P3 internal/sessionruntime/attachment.go:925 No test covers the session-level count/byte quota

Findings

Correctness

  • [P2] internal/sessionserver/web/app.js:3283 — submitComposer is now async and awaits uploadAttachment for each staged file, but nothing guards against re-entry. During the upload the message textarea stays enabled (only #send-message and #attach-files are disabled via state.sendingMessage), and its keydown handler calls elements.composer.requestSubmit(), which fires the submit event regardless of the disabled send button. A second Enter while a (multi-MiB, exec-streamed) upload is in flight re-enters submitComposer before clearPromptDraft/attachmentDrafts.delete run, so the same files are uploaded again and the message is sent twice — producing duplicate turns and consuming the 128-file / 100 MiB session quota twice. Add an early if (state.sendingMessage) return; at the top of submitComposer.

Tests

  • [P3] internal/sessionruntime/attachment.go:925 — checkQuota and the storedBytes+written > maxSessionAttachmentBytes branch enforce the per-Session file-count and byte limits, but no test exercises either limit. attachment_test.go covers per-file size, name validation, and identifier rejection only. A test that fills the store to maxSessionAttachmentCount (or the byte cap) and asserts ErrAttachmentQuotaExceeded would pin this behavior.

Suggestions (optional)

  • [P3] internal/cli/session_terminal.go:459 — In the plain terminal, /attach immediately uploads and appends to pendingAttachments with no cap, unlike the TUI's attachFile (session_terminal_tui.go:401) which rejects once len(pendingAttachments) >= MaxAttachmentsPerMessage. Staging more than eight files in the plain terminal uploads (and stores, consuming quota) all of them, then the server rejects the send with "a Session message supports at most 8 attachments" and pendingAttachments is cleared, losing the staging. Enforcing the same cap client-side before upload would keep the two terminals consistent and avoid orphaned uploads.
  • [P3] The web composer hardcodes 10 * 1024 * 1024 and 8 in stageAttachments (app.js:3241, :3245) to mirror MaxAttachmentBytes / MaxAttachmentsPerMessage. These will silently drift if the server constants change; consider surfacing them from a served config or a shared constant.
  • [P3] When Promise.all in submitComposer rejects on a later file, earlier files have already been uploaded and stored but are never referenced by a message; they persist against the session quota until reset/deletion. A retry re-uploads everything, compounding the orphaned usage. Not blocking, but worth a follow-up if quota exhaustion becomes a support issue.

Key takeaways

  • Storage layer is solid: UUID-validated IDs, filepath.Base name normalization, flock-guarded quota check, atomic temp-dir + rename, and metadata re-validation on Open/Download close the obvious path-traversal and truncation risks. Download responses reuse the existing nosniff / CSP / X-Frame-Options middleware and only mark image/* as inline, and http.DetectContentType classifies SVG/HTML as text, so inline-render XSS is mitigated.
  • The provider interface change (RunTurn(ctx, TurnInput, sink)) is applied consistently across the Claude, Codex, and OpenCode providers and all test fakes, and reuses the pre-existing pods/exec RBAC grant — no manifest change required.
  • docs/reference.md documents the feature, limits, and lifecycle accurately; the main gap is the web double-submit guard above.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/e2e/session_test.go">

<violation number="1" location="test/e2e/session_test.go:1261">
P3: The command context is only 3m, but this helper can poll for ~6.5m (30s connect + two 3m Eventually blocks). If the "Attached" matcher uses much of its 3m budget the context expires and kills the child process while the reply matcher is still polling, yielding a confusing failure even when the turn succeeds. Extend the command context to cover the whole wait or shrink per-matcher budgets so the poll budget fits inside the command lifetime.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread test/e2e/session_test.go
}

func runTerminalAttachmentTurn(namespace, name, path, prompt string, outputMatcher gomegatypes.GomegaMatcher) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The command context is only 3m, but this helper can poll for ~6.5m (30s connect + two 3m Eventually blocks). If the "Attached" matcher uses much of its 3m budget the context expires and kills the child process while the reply matcher is still polling, yielding a confusing failure even when the turn succeeds. Extend the command context to cover the whole wait or shrink per-matcher budgets so the poll budget fits inside the command lifetime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/session_test.go, line 1261:

<comment>The command context is only 3m, but this helper can poll for ~6.5m (30s connect + two 3m Eventually blocks). If the "Attached" matcher uses much of its 3m budget the context expires and kills the child process while the reply matcher is still polling, yielding a confusing failure even when the turn succeeds. Extend the command context to cover the whole wait or shrink per-matcher budgets so the poll budget fits inside the command lifetime.</comment>

<file context>
@@ -1212,6 +1257,29 @@ func runTerminalTurn(namespace, name, prompt string, outputMatcher gomegatypes.G
 }
 
+func runTerminalAttachmentTurn(namespace, name, path, prompt string, outputMatcher gomegatypes.GomegaMatcher) {
+	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
+	defer cancel()
+	command := exec.CommandContext(ctx, framework.KelosBin(), "session", "connect", name, "-n", namespace)
</file context>
Suggested change
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)

@gjkim42
gjkim42 force-pushed the agent/session-attachments branch from d68c69b to aec44d0 Compare August 10, 2026 10:52
@gjkim42
gjkim42 enabled auto-merge August 10, 2026 10:54

@cubic-dev-ai cubic-dev-ai 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.

13 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/cli/session_terminal_tui.go">

<violation number="1" location="internal/cli/session_terminal_tui.go:475">
P3: Typing `/send` in the interactive TUI with no staged files sends an empty message to the agent. `submitInput` handles the `message` request type by attaching `AttachmentIDs` and encoding it, but — unlike the plain-terminal path in `session_terminal.go`, which guards with `if request.Text == "" && len(pendingAttachments) == 0 { continue }` — the TUI has no such guard. Since `/send` resolves to `{Type: "message"}` with empty text, a session connected via the TUI will transmit a blank user message when nothing is attached. Consider adding the same empty-message guard used by the plain terminal before encoding. (Minor, since `/send` is documented only for the plain terminal.)</violation>
</file>

<file name="cmd/kelos-session-runtime/main.go">

<violation number="1" location="cmd/kelos-session-runtime/main.go:78">
P3: New attachment CLI protocol has no automated coverage; add put/get tests covering metadata framing, binary payloads, argument validation, and store failures so API downloads cannot silently regress.

(Based on your team's feedback about adding tests for new behavior.)</violation>
</file>

<file name="internal/sessionserver/web/styles.css">

<violation number="1" location="internal/sessionserver/web/styles.css:277">
P3: The new .attach-button (30×36px) sits beside .send-button in the composer, but the mobile media query bumps .send-button to 48×48px and leaves .attach-button at its small size, so the two are misaligned and the attach control becomes a sub-44px touch target on touch devices. Add a matching rule inside that media query (e.g. width/height 48px) alongside the existing .send-button resize.</violation>
</file>

<file name="internal/sessionattachment/client.go">

<violation number="1" location="internal/sessionattachment/client.go:26">
P2: Add direct client tests around the exec transport or an injectable stream seam; current attachment endpoint tests use a fake, leaving this new runtime wire protocol unverified against regressions.

(Based on your team's feedback about adding automated tests for new behavior.)</violation>
</file>

<file name="internal/sessionserver/web/app.js">

<violation number="1" location="internal/sessionserver/web/app.js:3294">
P2: Pressing Enter while an attachment upload is in flight can send the same message twice. `submitComposer` is now async (multipart uploads via `await Promise.all(...)`), but the Enter keydown handler calls `elements.composer.requestSubmit()` unconditionally, which re-invokes `submitComposer`. During the upload window `state.sendingMessage` is true (and only the attach button + send button are disabled), so a second Enter starts another concurrent upload and emits a duplicate `message` over the socket, producing duplicate agent turns and duplicate stored attachments. Add an early return when `state.sendingMessage` is already true (or guard the Enter handler) so re-submission is blocked while a send is in progress.</violation>

<violation number="2" location="internal/sessionserver/web/app.js:3298">
P2: Failed or disconnected sends leave already-uploaded files permanently consuming this Session’s attachment quota, so retries can eventually make valid attachments impossible. Make upload-and-message submission transactional, or add cleanup for uploaded IDs whenever handoff cannot complete.</violation>
</file>

<file name="internal/sessionruntime/server.go">

<violation number="1" location="internal/sessionruntime/server.go:165">
P2: Whitespace `StateDir` lets runtime start with nil attachment storage, so every attachment submission is rejected instead of failing invalid configuration at startup. Propagate or handle `NewAttachmentStore`'s error before serving requests.</violation>

<violation number="2" location="internal/sessionruntime/server.go:717">
P2: The same attachment IDs are resolved twice per turn: once eagerly in submitMessage (only to validate, then the resolved paths are discarded, keeping just Attachment metadata) and again in runTurn, which re-opens and re-validates every attachment file from disk. This duplicates I/O and introduces a window where the EventUserMessage is durably recorded in submitMessage but runTurn's re-resolution can still fail (e.g. if an attachment is removed between the two), producing a failed turn with no direct rejection feedback to the submitter. Consider carrying the already-resolved attachments on the turnRequest for freshly submitted turns, and resolving from IDs only for journal-recovered turns (where the resolved path is not persisted), so validation happens once.</violation>
</file>

<file name="internal/cli/session.go">

<violation number="1" location="internal/cli/session.go:567">
P2: Plain `kelos session connect` accepts more than 8 `/attach` commands, then `/send` is rejected by runtime and clears all pending attachment IDs, so users cannot send the allowed subset without reattaching. Keep the plain-terminal pending list capped at `MaxAttachmentsPerMessage`, matching the TUI path.</violation>
</file>

<file name="internal/sessionserver/server.go">

<violation number="1" location="internal/sessionserver/server.go:189">
P3: Oversized multipart uploads return 400/502 instead of 413 because `http.MaxBytesError` is not handled after `MaxBytesReader`; map that error (including the transfer-wrapped form) to `StatusRequestEntityTooLarge`.</violation>

<violation number="2" location="internal/sessionserver/server.go:842">
P2: The `attachmentUploadStatus` catch-all maps every upload failure whose message contains "storing session attachment failed" to 400 Bad Request. Because the runtime always prefixes `Storing Session attachment failed:` on *any* `store.Put` error, this branch catches genuine server-side storage failures too (e.g. "creating Session attachment directory", "writing Session attachment data", "locking Session attachment store", media detection) and reports them to the client as 400. Clients treating 4xx as "retry/fix your request" will be misled when the real cause is a server-side disk/I/O problem, and the loss of the underlying status is confusing for ops debug. Prefer mapping the server-side failures to 5xx (e.g. 500/502) and reserving 4xx for the explicitly client-caused cases (invalid name, too-large, quota).</violation>
</file>

<file name="internal/cli/session_terminal.go">

<violation number="1" location="internal/cli/session_terminal.go:313">
P3: Plain-terminal users cannot discover how to submit an attachment without text because this command list omits `/send`; include it alongside `/attach`.</violation>
</file>

<file name="internal/sessionserver/server_test.go">

<violation number="1" location="internal/sessionserver/server_test.go:49">
P3: The fake Download discards the requested attachment id (4th parameter), and Upload returns a fixed attachment regardless of the file route, so the download flow's id routing/parameterization is never actually exercised by the test. A bug where downloadSessionAttachment passes the wrong identifier (e.g. the session/pod name instead of the requested id) would pass this green. Capture the id (and pod/namespace) on the fake and assert it matches the request path so the download path is truly covered.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@@ -0,0 +1,115 @@
package sessionattachment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Add direct client tests around the exec transport or an injectable stream seam; current attachment endpoint tests use a fake, leaving this new runtime wire protocol unverified against regressions.

(Based on your team's feedback about adding automated tests for new behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionattachment/client.go, line 26:

<comment>Add direct client tests around the exec transport or an injectable stream seam; current attachment endpoint tests use a fake, leaving this new runtime wire protocol unverified against regressions.

(Based on your team's feedback about adding automated tests for new behavior.) </comment>

<file context>
@@ -0,0 +1,115 @@
+const runtimeExecutable = "/kelos/bin/kelos-session-runtime"
+
+// Client transfers attachments through the Session Pod exec subresource.
+type Client struct {
+	clientset  kubernetes.Interface
+	restConfig *rest.Config
</file context>

updateComposerAction();
elements.attachFiles.disabled = true;
try {
const attachments = await Promise.all(files.map(file => uploadAttachment(session, file)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Failed or disconnected sends leave already-uploaded files permanently consuming this Session’s attachment quota, so retries can eventually make valid attachments impossible. Make upload-and-message submission transactional, or add cleanup for uploaded IDs whenever handoff cannot complete.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/web/app.js, line 3297:

<comment>Failed or disconnected sends leave already-uploaded files permanently consuming this Session’s attachment quota, so retries can eventually make valid attachments impossible. Make upload-and-message submission transactional, or add cleanup for uploaded IDs whenever handoff cannot complete.</comment>

<file context>
@@ -3168,21 +3213,131 @@ function scheduleBottomAnchor() {
+  updateComposerAction();
+  elements.attachFiles.disabled = true;
+  try {
+    const attachments = await Promise.all(files.map(file => uploadAttachment(session, file)));
+    if (!state.selected || sessionViewKey(state.selected) !== sessionViewKey(session) || !state.socket || state.socket.readyState !== WebSocket.OPEN) {
+      throw new Error('Session disconnected before the message could be sent');
</file context>

}
if config.StateDir != "" {
server.activityMarkerPath = filepath.Join(config.StateDir, activityPublishedFile)
server.attachmentStore, _ = NewAttachmentStore(config.StateDir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Whitespace StateDir lets runtime start with nil attachment storage, so every attachment submission is rejected instead of failing invalid configuration at startup. Propagate or handle NewAttachmentStore's error before serving requests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionruntime/server.go, line 165:

<comment>Whitespace `StateDir` lets runtime start with nil attachment storage, so every attachment submission is rejected instead of failing invalid configuration at startup. Propagate or handle `NewAttachmentStore`'s error before serving requests.</comment>

<file context>
@@ -160,6 +162,7 @@ func NewServer(config Config, journal *Journal, provider Provider) *Server {
 	}
 	if config.StateDir != "" {
 		server.activityMarkerPath = filepath.Join(config.StateDir, activityPublishedFile)
+		server.attachmentStore, _ = NewAttachmentStore(config.StateDir)
 	}
 	if provider, ok := provider.(runtimeStatusProvider); ok {
</file context>

Comment thread internal/cli/session.go
if request.Type == "subscribe" {
continue
}
if request.Type == sessionTerminalRequestAttachment {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Plain kelos session connect accepts more than 8 /attach commands, then /send is rejected by runtime and clears all pending attachment IDs, so users cannot send the allowed subset without reattaching. Keep the plain-terminal pending list capped at MaxAttachmentsPerMessage, matching the TUI path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/cli/session.go, line 567:

<comment>Plain `kelos session connect` accepts more than 8 `/attach` commands, then `/send` is rejected by runtime and clears all pending attachment IDs, so users cannot send the allowed subset without reattaching. Keep the plain-terminal pending list capped at `MaxAttachmentsPerMessage`, matching the TUI path.</comment>

<file context>
@@ -540,6 +564,24 @@ func connectSessionWithDependencies(
 				if request.Type == "subscribe" {
 					continue
 				}
+				if request.Type == sessionTerminalRequestAttachment {
+					var event sessionruntime.Event
+					if dependencies.uploadAttachment == nil {
</file context>

const session = state.selected;
const files = [...currentAttachmentFiles()];
if (!text && files.length === 0) return;
state.sendingMessage = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Pressing Enter while an attachment upload is in flight can send the same message twice. submitComposer is now async (multipart uploads via await Promise.all(...)), but the Enter keydown handler calls elements.composer.requestSubmit() unconditionally, which re-invokes submitComposer. During the upload window state.sendingMessage is true (and only the attach button + send button are disabled), so a second Enter starts another concurrent upload and emits a duplicate message over the socket, producing duplicate agent turns and duplicate stored attachments. Add an early return when state.sendingMessage is already true (or guard the Enter handler) so re-submission is blocked while a send is in progress.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/web/app.js, line 3293:

<comment>Pressing Enter while an attachment upload is in flight can send the same message twice. `submitComposer` is now async (multipart uploads via `await Promise.all(...)`), but the Enter keydown handler calls `elements.composer.requestSubmit()` unconditionally, which re-invokes `submitComposer`. During the upload window `state.sendingMessage` is true (and only the attach button + send button are disabled), so a second Enter starts another concurrent upload and emits a duplicate `message` over the socket, producing duplicate agent turns and duplicate stored attachments. Add an early return when `state.sendingMessage` is already true (or guard the Enter handler) so re-submission is blocked while a send is in progress.</comment>

<file context>
@@ -3168,21 +3213,131 @@ function scheduleBottomAnchor() {
+  const session = state.selected;
+  const files = [...currentAttachmentFiles()];
+  if (!text && files.length === 0) return;
+  state.sendingMessage = true;
+  updateComposerAction();
+  elements.attachFiles.disabled = true;
</file context>

}
}

func runAttachment() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: New attachment CLI protocol has no automated coverage; add put/get tests covering metadata framing, binary payloads, argument validation, and store failures so API downloads cannot silently regress.

(Based on your team's feedback about adding tests for new behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/kelos-session-runtime/main.go, line 78:

<comment>New attachment CLI protocol has no automated coverage; add put/get tests covering metadata framing, binary payloads, argument validation, and store failures so API downloads cannot silently regress.

(Based on your team's feedback about adding tests for new behavior.) </comment>

<file context>
@@ -66,12 +67,67 @@ func main() {
 	}
 }
 
+func runAttachment() {
+	if len(os.Args) < 3 {
+		fmt.Fprintln(os.Stderr, "Usage: kelos-session-runtime attachment <put|get>")
</file context>

.composer:focus-within { border-color: #85a493; box-shadow: 0 12px 38px rgba(28,45,36,.1), 0 0 0 3px rgba(57,112,84,.08); }
.composer textarea { flex: 1; max-height: 160px; resize: none; border: 0; outline: 0; padding: 8px 0 6px; background: transparent; color: var(--ink); font-size: 14px; line-height: 1.45; }
.composer textarea::placeholder { color: #99a19c; }
.attach-button { flex: none; width: 30px; height: 36px; padding: 0; border: 0; background: transparent; color: var(--accent-2); font-size: 22px; cursor: pointer; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new .attach-button (30×36px) sits beside .send-button in the composer, but the mobile media query bumps .send-button to 48×48px and leaves .attach-button at its small size, so the two are misaligned and the attach control becomes a sub-44px touch target on touch devices. Add a matching rule inside that media query (e.g. width/height 48px) alongside the existing .send-button resize.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/web/styles.css, line 277:

<comment>The new .attach-button (30×36px) sits beside .send-button in the composer, but the mobile media query bumps .send-button to 48×48px and leaves .attach-button at its small size, so the two are misaligned and the attach control becomes a sub-44px touch target on touch devices. Add a matching rule inside that media query (e.g. width/height 48px) alongside the existing .send-button resize.</comment>

<file context>
@@ -262,10 +266,16 @@ button { color: inherit; }
 .composer:focus-within { border-color: #85a493; box-shadow: 0 12px 38px rgba(28,45,36,.1), 0 0 0 3px rgba(57,112,84,.08); }
 .composer textarea { flex: 1; max-height: 160px; resize: none; border: 0; outline: 0; padding: 8px 0 6px; background: transparent; color: var(--ink); font-size: 14px; line-height: 1.45; }
 .composer textarea::placeholder { color: #99a19c; }
+.attach-button { flex: none; width: 30px; height: 36px; padding: 0; border: 0; background: transparent; color: var(--accent-2); font-size: 22px; cursor: pointer; }
+.attach-button:disabled { color: var(--faint); cursor: default; }
 .send-button { flex: 0 0 auto; width: 36px; height: 36px; border: 0; border-radius: 11px; background: var(--accent); color: white; font-size: 20px; font-weight: 500; cursor: pointer; }
</file context>

return nil, errors.New("Kubernetes clients and REST config are required")
}
attachmentClient, err := sessionattachment.New(config.RESTConfig)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Oversized multipart uploads return 400/502 instead of 413 because http.MaxBytesError is not handled after MaxBytesReader; map that error (including the transfer-wrapped form) to StatusRequestEntityTooLarge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/server.go, line 189:

<comment>Oversized multipart uploads return 400/502 instead of 413 because `http.MaxBytesError` is not handled after `MaxBytesReader`; map that error (including the transfer-wrapped form) to `StatusRequestEntityTooLarge`.</comment>

<file context>
@@ -176,6 +185,10 @@ func New(config Config) (*Server, error) {
 		return nil, errors.New("Kubernetes clients and REST config are required")
 	}
+	attachmentClient, err := sessionattachment.New(config.RESTConfig)
+	if err != nil {
+		return nil, err
+	}
</file context>

write("\n%s\n", formatter.muted("Earlier Session history is available. Use /history to load the previous page."))
}
write("\n%s\n\n", formatter.muted("Connected. Type a message, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))
write("\n%s\n\n", formatter.muted("Connected. Type a message, /attach PATH, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Plain-terminal users cannot discover how to submit an attachment without text because this command list omits /send; include it alongside /attach.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/cli/session_terminal.go, line 313:

<comment>Plain-terminal users cannot discover how to submit an attachment without text because this command list omits `/send`; include it alongside `/attach`.</comment>

<file context>
@@ -306,7 +310,7 @@ func runSessionPlainTerminalWithWidth(ctx context.Context, input io.Reader, outp
 					write("\n%s\n", formatter.muted("Earlier Session history is available. Use /history to load the previous page."))
 				}
-				write("\n%s\n\n", formatter.muted("Connected. Type a message, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))
+				write("\n%s\n\n", formatter.muted("Connected. Type a message, /attach PATH, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))
 			case sessionruntime.EventRuntimeRecovered:
 				if !pageEvent {
</file context>
Suggested change
write("\n%s\n\n", formatter.muted("Connected. Type a message, /attach PATH, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))
write("\n%s\n\n", formatter.muted("Connected. Type a message, /attach PATH, /send, /history, /interrupt, /answer INPUT QUESTION VALUE, or /quit."))

return f.attachment, nil
}

func (f *fakeSessionAttachmentTransfer) Download(_ context.Context, _, _, _ string) (sessionruntime.Attachment, []byte, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The fake Download discards the requested attachment id (4th parameter), and Upload returns a fixed attachment regardless of the file route, so the download flow's id routing/parameterization is never actually exercised by the test. A bug where downloadSessionAttachment passes the wrong identifier (e.g. the session/pod name instead of the requested id) would pass this green. Capture the id (and pod/namespace) on the fake and assert it matches the request path so the download path is truly covered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/server_test.go, line 49:

<comment>The fake Download discards the requested attachment id (4th parameter), and Upload returns a fixed attachment regardless of the file route, so the download flow's id routing/parameterization is never actually exercised by the test. A bug where downloadSessionAttachment passes the wrong identifier (e.g. the session/pod name instead of the requested id) would pass this green. Capture the id (and pod/namespace) on the fake and assert it matches the request path so the download path is truly covered.</comment>

<file context>
@@ -27,9 +29,27 @@ import (
+	return f.attachment, nil
+}
+
+func (f *fakeSessionAttachmentTransfer) Download(_ context.Context, _, _, _ string) (sessionruntime.Attachment, []byte, error) {
+	return f.attachment, f.downloadData, nil
+}
</file context>

@gjkim42
gjkim42 added this pull request to the merge queue Aug 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 10, 2026
@gjkim42
gjkim42 merged commit 25d6b6e into main Aug 10, 2026
62 checks passed
@gjkim42
gjkim42 deleted the agent/session-attachments branch August 10, 2026 12:54
kelos-bot Bot pushed a commit that referenced this pull request Aug 10, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 11, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 12, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 13, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 14, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 15, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 19, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 20, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 21, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 22, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Aug 26, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
kelos-bot Bot pushed a commit that referenced this pull request Sep 24, 2026
PR #1629 reviews showed that bounding a controller-owned StatefulSet name without accounting for ControllerRevision suffixes could either remain invalid or break workload and PVC identity. PR #1630 reviews found inconsistent attachment limits and empty-input behavior across web, TUI, and plain terminal clients, plus server failures classified as client errors by matching message text. Encode the durable-name, shared-client-contract, and typed error-classification expectations in the project and shared Kelos agent instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant