Created
August 19, 2026 04:42
-
-
Save feifanzhou/f4cedb8ce355d87d3a56bcd5ac49c66b to your computer and use it in GitHub Desktop.
Rivet history that leads to "Failed to decode workflow history"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * Reproduces the RivetKit 2.3.10 Workflow tab decode failure without an actor, | |
| * network connection, or application data. | |
| * | |
| * The inspector protocol requires raw BARE workflow-history bytes. The failing | |
| * runtime path instead sends the CBOR form of jsonStringifyCompat's | |
| * ["$ArrayBuffer", "<base64>"] value. The frontend then reads those CBOR bytes | |
| * as BARE and reports "BareError: (byte:3) invalid UTF-8 string". | |
| * | |
| * Run in a project with rivetkit 2.3.10 installed: | |
| * node reproduce-rivet-workflow-history-decode.mjs | |
| */ | |
| import { | |
| decodeWorkflowHistoryTransport, | |
| encodeWorkflowHistoryTransport, | |
| } from 'rivetkit/inspector/client' | |
| const history = { | |
| nameRegistry: ['agent-loop'], | |
| entries: [{ | |
| id: 'entry-1', | |
| location: [{ tag: 'WorkflowNameIndex', val: 0 }], | |
| kind: { | |
| tag: 'WorkflowStepEntry', | |
| // A realistic large step output makes the CBOR string-length bytes match | |
| // the production failure's invalid UTF-8 path instead of failing later. | |
| val: { output: new Uint8Array(32 * 1024).buffer, error: null }, | |
| }, | |
| }], | |
| entryMetadata: new Map([[ | |
| 'entry-1', | |
| { | |
| status: 'COMPLETED', | |
| error: null, | |
| attempts: 1, | |
| lastAttemptAt: 10n, | |
| createdAt: 5n, | |
| completedAt: 10n, | |
| rollbackCompletedAt: null, | |
| rollbackError: null, | |
| }, | |
| ]]), | |
| } | |
| const bareHistory = encodeWorkflowHistoryTransport(history) | |
| const compatWrappedHistory = encodeCborStringPair( | |
| '$ArrayBuffer', | |
| Buffer.from(bareHistory).toString('base64'), | |
| ) | |
| console.log('Rivet workflow-history transport reproduction') | |
| console.log(`rivetkit: 2.3.10`) | |
| console.log(`raw BARE bytes: ${bareHistory.byteLength}`) | |
| console.log(`raw BARE prefix: ${hexPrefix(bareHistory)}`) | |
| decodeAndReport('raw BARE payload', bareHistory) | |
| console.log(`\ncompat-wrapped bytes: ${compatWrappedHistory.byteLength}`) | |
| console.log(`compat-wrapped prefix: ${hexPrefix(compatWrappedHistory)}`) | |
| console.log('decoded CBOR shape: ["$ArrayBuffer", "<base64>"]') | |
| decodeAndReport('compat-wrapped payload', compatWrappedHistory.buffer) | |
| function decodeAndReport(label, bytes) { | |
| try { | |
| const decoded = decodeWorkflowHistoryTransport(bytes) | |
| console.log(`${label}: OK (${decoded.entries.length} entry)`) | |
| } catch (error) { | |
| console.log(`${label}: FAILED: ${String(error)}`) | |
| } | |
| } | |
| function encodeCborStringPair(first, second) { | |
| return concatBytes( | |
| Uint8Array.of(0x82), | |
| encodeCborString(first), | |
| encodeCborString(second), | |
| ) | |
| } | |
| function encodeCborString(value) { | |
| const bytes = new TextEncoder().encode(value) | |
| return concatBytes(encodeCborLength(0x60, bytes.byteLength), bytes) | |
| } | |
| function encodeCborLength(shortBase, length) { | |
| if (length < 24) return Uint8Array.of(shortBase + length) | |
| if (length <= 0xff) return Uint8Array.of(shortBase + 24, length) | |
| if (length <= 0xffff) { | |
| return Uint8Array.of(shortBase + 25, length >> 8, length & 0xff) | |
| } | |
| throw new RangeError(`Diagnostic CBOR string is too large: ${length}`) | |
| } | |
| function concatBytes(...parts) { | |
| const result = new Uint8Array( | |
| parts.reduce((length, part) => length + part.byteLength, 0), | |
| ) | |
| let offset = 0 | |
| for (const part of parts) { | |
| result.set(part, offset) | |
| offset += part.byteLength | |
| } | |
| return result | |
| } | |
| function hexPrefix(bytes) { | |
| return [...new Uint8Array(bytes).slice(0, 16)] | |
| .map((byte) => byte.toString(16).padStart(2, '0')) | |
| .join(' ') | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment