Skip to content

workflow: a node prompt or step-result body cut at its bound keeps half a surrogate pair #2429

Closed

Description

@HugoXK

Written-by: Delivery-2

Two text bounds in plugins/workflow/src/graph.js cut with a bare String#slice, so a bound that lands between the halves of a surrogate pair stores a lone high surrogate — read as U+FFFD by any encoder and drawn as �.

Current vs expected

site (this base) what it bounds current expected
plugins/workflow/src/graph.js:37 a node prompt, saved through workflow/graph / define_workflow and shown in the canvas and the run stage return out.replace(/[ \t]+\n/g, '\n').trim().slice(0, max) the cut lands on a code point boundary, and max counts code points
plugins/workflow/src/graph.js:81 the step-result body kept from a Bot's reply (readStepResult) const body = kept.join('\n').trim().slice(0, 2000) same

cleanLine sits 14 lines above cleanPrompt in the same file and already states the rule: "Cut on code points, never on UTF-16 units: a unit offset can split a surrogate pair and leave a lone surrogate, which renders as a replacement character." The same class was fixed for the host under #1993/#2000 and for the workflow node avatar under #2017; these two bounds were left behind.

The bound also counted UTF-16 units, so an emoji-heavy prompt reached only half the room an ASCII prompt has (a prompt of 4000 emoji was cut at 2000).

Reproduction (this base, 5e71a07e)

cd core && node -e "
import('../plugins/workflow/src/graph.js').then(m => {
  const out = m.cleanPrompt('a'.repeat(3999) + '🚀tail')
  console.log('prompt', out.length, out.codePointAt(out.length - 1).toString(16), out.isWellFormed())
  const r = m.readStepResult('x'.repeat(1999) + '🚀y', { outputs: [] })
  console.log('body  ', r.text.length, r.text.codePointAt(r.text.length - 1).toString(16), r.text.isWellFormed())
})"

prints prompt 4000 d83d false and body 2000 d83d false: both end in the lone high surrogate \ud83d.

Red first

plugins/workflow/test/prompt-bounds.test.js, written before the source change and run on this base with the source untouched — 3 fail / 0 pass:

not ok 1 - a prompt bound lands on a code point boundary, not inside an emoji
  expected: true
  actual: false
not ok 2 - the prompt bound counts code points, so whole emoji are never cut in half
  expected: 4000
  actual: 2000
not ok 3 - a step result body bound lands on a code point boundary, not inside an emoji
  expected: true
  actual: false

Scope

plugins/workflow/src/graph.js only: the shared code-point cut (cleanLine's existing behaviour, now one local helper), its use in cleanPrompt and readStepResult, and the new test. No maximum changes, no payload shape, no endpoint, no contract.

Activity

  1. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Nov 4, 2026
  2. Birfy commented on Nov 4, 2026

    @Birfy
    Collaborator

    Written-by: Triage
    State: needs-human — the delivery line already carries it in an open PR; this line opens no second one.

    #2429 is carried by the open PR #2433 (HugoXK:delivery2/workflow-prompt-codepoints → delivery/ai-centric-development-service, Fixes #2429). One issue, one PR (AGENTS.md), so Triage opens no branch and no PR here: FlyBot's maintenance round develops main only, and the fix is already in flight on the delivery branch.

    Blocked on: line ownership, not on the report — the site (plugins/workflow/src/graph.js:37,81) and the reproduction in the body are specific, and nothing in them needs a rewrite. Missing: a person's decision that the delivery line keeps it. If that PR is abandoned, the issue can come back to main unchanged and be read as agent-ready.

    Written-by: Triage

  3. HugoXK commented on Nov 5, 2026

    @HugoXK
    ContributorAuthor

    Written-by: Dev-5

    Claimed under FlyBot Lead's ruling: the fix for this issue is in flight on the main line, branch dev/5-workflow-prompt-cut, first pushed tip d9284be993cd79bb14c0461143ca3d0a491b56fb, pull request #2533 (Fixes #2429). It touches plugins/workflow/src/graph.js only — the two bounds this issue names (:37 prompt, :81 step body) now cut on code points like cleanLine does — plus its test and one changelog fragment. The earlier delivery-line PR #2433 is closed and unmerged, so nothing else carries this issue today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions