fix(agent): TextEncoder byte-length in modelMessages (Workflows has no Buffer) - #939
Conversation
…o Buffer) #937's derivePersistFold runs on the Workflows canvas and called Buffer.byteLength. Vercel Workflows has no Node Buffer, so every persist threw ReferenceError: Buffer is not defined and the loop failed the turn (~12s in, after the first model round). Use TextEncoder like usageSummary / sessionRepository.utf8ByteLength. Lock with a no-Buffer runtime test + source-lock. Refs #936 #549
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #939
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← fix/model-messages-no-buffer · 3 files · canvas Buffer.byteLength → TextEncoder
Lenses run: L1, L5, L6, L8 (skip: L2 no secrets/runner/API boundary; L3 no UI/Wasm; L4 no CI/artifact/workflow change; L7 no config/host hardcoding; L9 no UI)
AGENTS.md read: yes (feature-divide + SECURITY skimmed; this diff is workflow-canvas agent-loop only)
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Minor | L6 | Canvas Buffer lock is file-local (lib/agent/modelMessages.test.ts source-lock greps only modelMessages.ts). Production closure is reachableImports('lib/workflows/turnWorkflow.ts') (already used by the B11 static-graph test). |
Add Buffer.byteLength(JSON.stringify(fold)) in lib/workflows/turnLog.ts or turnLoop.ts (both canvas-reachable, not 'use step' leaves). This PR's new tests stay green. Next durable persist throws Buffer is not defined — the same ~12s production brick this PR is fixing. |
Defender: the runtime test hides globalThis.Buffer then calls derivePersistFold, so a Buffer on that path is caught. That does not cover other canvas helpers (logTurnLoop, formatTurnSse, modelFinish, workPath) that run on the same VM. Graph walk of this SHA: zero Buffer hits in non-step closure — so this is a lock gap, not a live leak. |
high |
| Nit | L6 | lib/workflows/turnLoop.test.ts · Buffer-absent derivePersistFold test does not assert the hide worked. |
If Reflect.deleteProperty(globalThis, 'Buffer') is a no-op, the test passes on Node while proving nothing about the Workflows VM. Sibling modelMessages.test.ts does expect(typeof g.Buffer).toBe('undefined'). |
Defender: on current Node 22 Buffer is a configurable getter; delete works (probed). The sibling assertion would fail the suite if delete ever stopped working. Still a copy-paste hole on the production call-site test. |
high |
Residual risk
Workflows VM (vercel/workflow packages/core/src/vm/index.ts) injects TextEncoder / TextDecoder and does not inject Buffer — so the swap matches the actual sandbox, not just usageSummary / sessionRepository (those are not canvas-reachable). Current turnWorkflow closure is Buffer-free. encoder.encode(s).length allocates a Uint8Array per byte-cap JSON.stringify in buildModelMessages's drop-oldest loop (cap 8 MiB × up to 4096 rows); Buffer.byteLength did not. Unlikely to OOM vs the stringify itself. Durable-turn int CI is Node, not the canvas VM — it cannot catch this class. Production stays bricked until this SHA is deployed to Production (Preview was ignored).
Merge guidance
PASS WITH NOTES: safe to merge from this attack; nits optional. Do not treat green durable-turn int as canvas proof. Ship a Production deploy after merge — that is the unbrick.
What was not attacked
Live Production Workflows canvas (no deploy of this SHA). Self-hosted runner. Gateway. Wasm/bridge. 'use step' bodies (persistStep → messageCheckpoint still uses Buffer.byteLength — legal: step VM is Node).
Summary
Production durable turns fail immediately after the first model round:
POST /.well-known/workflow/v1/flowonmain(Workflows canvas) — not a Node Function.#937 added
buildModelMessagestoderivePersistFold, which the'use workflow'loop calls on every persist. That helper usedBuffer.byteLength. Vercel Workflows has no NodeBufferglobal. The throw is caught byrunTurnLoop's outer catch, persist is skipped, and the operator seesTurn ended · error · Buffer is not defined.Fix
TextEncoderfor UTF-8 byte length — same pattern asusageSummary.ts/sessionRepository.utf8ByteLength. Do not importsessionRepositoryinto the workflow graph.Tests (+3)
modelMessages.test.tssource-lock: noBufferidentifier in executable codemodelMessages.test.tsruntime: hideglobalThis.Buffer, projection + byte-cap still workturnLoop.test.ts:derivePersistFolddoes not throw whenBufferis absent (the production call site)Gates
npm run typecheck✅vitest run lib/agent/modelMessages.test.ts lib/workflows/turnLoop.test.ts— 123 passed (was 120: 8 + 112)Ops
Cloud ops N/A — code-only. Needs a Production deploy of this PR to unbrick durable turns.
Refs #936 #549