Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save feifanzhou/f4cedb8ce355d87d3a56bcd5ac49c66b to your computer and use it in GitHub Desktop.

Select an option

Save feifanzhou/f4cedb8ce355d87d3a56bcd5ac49c66b to your computer and use it in GitHub Desktop.
Rivet history that leads to "Failed to decode workflow history"
#!/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