feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(server): accept PDF, ZIP, and other file uploads up to 50MB - #140

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads
Aug 29, 2026
Merged

feat(server): accept PDF, ZIP, and other file uploads up to 50MB#140
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-28-file-uploads

Conversation

@rynfar

@rynfarrynfar commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Pylon previously accepted images only.

Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.

Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235). 40 files.

Adaptation — Pylon has a sixth adapter

PrimeAgentAdapter needed the image-only guard the other five received. Upstream
has no Prime adapter, and Pylon's pushed every attachment into an ACP image
content block unconditionally. Once ProviderService started forwarding generic
files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type.

Prime runs models on Anthropic and OpenAI Codex, so images-only matches those
providers' own adapters, with generic files reaching the agent via the path line.

Unlike upstream, that guard ships with a test. Upstream added no coverage for
any of the five adapter guards it introduced — I checked. Pylon's uses the mock
ACP agent's existing T3_ACP_REQUEST_LOG_PATH hook to assert the prompt Prime
actually sends. Mutation-probed: removing the guard yields
expected [ 'image', 'image' ] to deeply equal [ 'image' ] — the PDF becoming a
second image block is exactly the bug.

Also: the cherry-pick dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
That surfaced as a typecheck error, not a test failure.

docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.

Verification

  • vp test run across ProviderService.test.ts, AttachmentUpload.test.ts,
    attachmentStore.test.ts, http.test.ts, assets.test.ts,
    orchestration.test.ts, Normalizer.attachments.test.ts138 passed
  • vp test run apps/server/src/provider/Layers/PrimeAgentAdapter.test.ts — 5 passed
  • vp run -F t3 typecheck, -F @t3tools/contracts, -F @t3tools/web,
    -F @t3tools/mobile — all clean
  • vp lint on every file I hand-resolved — clean

Risks you should weigh before merging

I flagged these when recommending deferral; you chose to adopt, so here they are
on the record rather than dropped:

  1. No client can attach a file yet. The web and mobile file pickers are
    upstream #8236 and #8237, and both are still OPEN. This ships server
    support with no UI to exercise it.
  2. Wire compatibility is one-way. Upstream's own PR warns that older clients
    cannot decode file-bearing messages, and that an older server can fail
    startup for a whole environment when replay reaches one — not just for the
    affected thread. Pylon's mobile app ships independently through EAS, so an
    updated server against an older installed Pylon mobile build is a realistic
    break, and rollback is not per-thread.

Neither is a reason not to merge, but #2 in particular argues for landing this
before any Pylon mobile release rather than after.

Claude Opus 5 via Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 28, 2026
@github-actions

github-actionsBot commented Aug 28, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+35 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+34 B (+0.5%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.6 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−26 B (−0.2%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+12 B (+0.2%)7.3 KiB
ClaudeLive turn WebSocket wire6.5 KiB6.4 KiB−38 B (−0.6%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB−44 B (−0.1%)66.4 KiB
ClaudeLive turn messages1110−1 (−9.1%)21

Baseline: 3d6035f · PR result: a570ecc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Independent review pass found two real defects. One fixed, one documented.

Fixed in 70d4fb32c — the guard was on the wrong Prime adapter.

Pylon has two: PrimeAgentAdapter and PrimeAgentDaemonAdapter, and
PrimeAgentDriver.ts:286 selects the daemon one whenever Prime Agent runs its
daemon backend. I had guarded only the first, so the exact bug this PR claims to
fix was still live on the path most Prime users take — at both of the daemon
adapter's attachment loops, sendTurn and followUp.

Now guarded at both sites, with a test on the sendTurn path: the fake runtime
already records prompt images, so it pins the mime types actually sent. Removing
the guard fails it with ['image/png', 'application/pdf'] against
['image/png'].

I also audited every adapter that resolves attachment paths rather than assuming
the set was complete. Cursor, Claude, and both Prime adapters use the negative
guard; Codex and Grok use a positive attachment.type === "image" filter,
which my first grep missed. OpenCode is intentionally unfiltered because it
ingests files natively.

Documented, not fixed — generic files are silently lost on the follow-up path.

ProviderService.followUp does not build the [Attached … is saved at: …] path
lines that sendTurn builds. With the adapter guard in place, a generic file
attached to a queued follow-up is therefore dropped entirely: not ingested
natively, and not surfaced as a path either. The agent never learns it exists.

This is inherited, not introduced — upstream's ProviderService has
attachmentPathLines only in sendTurn too. It is also not yet reachable in
practice, since no client can attach a generic file until upstream #8236/#8237
ship. But it becomes a real data-loss path the moment they do, so it should be
closed before the file pickers land rather than after.

t3dotggand others added 2 commits August 28, 2026 20:55
Adds the server contract, streaming upload, storage, and download support for
generic files up to 50 MiB. Previously only images were accepted.
Every attachment's absolute path goes into the turn text, and every attachment
now reaches the provider adapter, which decides what its provider ingests
natively. OpenCode sends images, text files, and PDFs up to 20 MB as native file
parts; the other adapters send images only and rely on the path line.
Adopted from T3 Code 8f49132214a40c85cf46bf5e3d8ea11c04a9610a
(pingdotgg/t3code#8235).
Adaptation:
PrimeAgentAdapter needed the image-only guard the other five adapters received.
Upstream has no Prime adapter, and Pylon's pushed every attachment into an ACP
image content block unconditionally - so once ProviderService started forwarding
generic files, a PDF would have been base64'd and sent as an image with
application/pdf as its mime type. Prime runs models on Anthropic and OpenAI
Codex, so images-only matches those providers' own adapters.
Unlike upstream, that guard ships with a test. The mock ACP agent's existing
request log is used to assert the prompt Prime actually sends.
The cherry-pick also dropped ProviderService.test.ts's EnvironmentId import,
which upstream no longer needs but Pylon's browser-credential test still uses.
docs/internals/providers.md keeps Pylon's Subscription capacity section and
gains upstream's Attachment access section, with Prime Agent listed among the
image-only providers.
Review follow-up. The image-only guard was added to PrimeAgentAdapter but not to
PrimeAgentDaemonAdapter, which PrimeAgentDriver selects whenever Prime Agent runs
its daemon backend. Both of its attachment loops - sendTurn and followUp - read
any attachment into memory and pushed it as an ACP image block, so a PDF would
have been base64'd and sent with application/pdf as its mime type. That is the
exact bug the original guard was for, still live on the path most Prime users
take.
Guards both sites and asserts the sendTurn one: the fake runtime already records
prompt images, so the test pins the mime types actually sent. Removing the guard
fails it with ['image/png', 'application/pdf'] against ['image/png'].
Audited the remaining adapters rather than assuming: Cursor, Claude, Codex, Grok,
and both Prime adapters now filter, Codex and Grok through a positive
attachment.type === "image" filter rather than the negative guard. OpenCode is
intentionally unfiltered because it ingests files natively.
@rynfar
rynfarforce-pushed the upstream/2026-08-28-file-uploads branch from 70d4fb3 to a570eccCompareAugust 29, 2026 02:55
@rynfar
rynfar merged commit 3ed6520 into pylonAug 29, 2026
15 checks passed
@rynfar
rynfar deleted the upstream/2026-08-28-file-uploads branch August 29, 2026 03:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg