Skip to content

workflow: cut a node prompt and a step-result body on code points - #2433

Closed
HugoXK wants to merge 1 commit into
ThinkFlowLab:delivery/ai-centric-development-servicefrom
HugoXK:delivery2/workflow-prompt-codepoints
Closed

HugoXK wants to merge 1 commit into
ThinkFlowLab:delivery/ai-centric-development-servicefrom
HugoXK:delivery2/workflow-prompt-codepoints

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Written-by: Delivery-2
Fixes #2429
Owner: Delivery-2
Tip SHA: dccec83 — the fork's commit for tree b48551cf45; the same tree was checked locally at d4e35ce, whose message and base 5e71a07e are identical
Branch: delivery2/workflow-prompt-codepoints (fork HugoXK/FlyBot, base delivery/ai-centric-development-service @ 5e71a07e)

Reproduce

cleanPrompt and readStepResult clip with a bare String#slice, so a bound that lands between the halves of a surrogate pair stores the lone high surrogate. A person meets it where the prompt is drawn (Settings → Workflows canvas, the run stage) and where a step-result body is kept from a Bot's reply:

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())
})"

Actual on base: prompt 4000 d83d false / body 2000 d83d false — both end in \ud83d, which every encoder reads as U+FFFD and every reader draws as �. Expected: the cut lands after the whole emoji, the stored text is well formed, and the maximum counts code points, exactly as cleanLine fourteen lines above already does — "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.

The bound also counted UTF-16 units, so an emoji-heavy prompt reached half the room of an ASCII one: a prompt of 4,000 emoji was cut at 2,000.

What changed

  • plugins/workflow/src/graph.js (+5/−5) — one local cut(text, max) helper, the code-point cut cleanLine already performed, now used by cleanLine, cleanPrompt and the readStepResult body. No maximum changes: 300, 4,000 and 2,000 stay, and each now counts code points, so the whole bound is kept.
  • plugins/workflow/test/prompt-bounds.test.js (new, 27 lines) — three cases: a prompt bound and a step-result body bound that land inside an emoji, plus the code-point count that a prompt of whole emoji keeps.
  • changelog.d/2429-workflow-text-bounds-code-points.fixed.md (new, 1 line) — a user can see this text.

No payload shape, endpoint, key, contract or module boundary changes; no other plugin, no core/, no generated file touched.

Red without the source change

plugins/workflow/test/prompt-bounds.test.js on base 5e71a07e 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
# pass 0 / # fail 3

With the fix at the tip: # pass 3 / # fail 0.

What I ran (at the tree d4e35ce = b48551cf)

$ cd plugins/workflow && TZ=UTC node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 344 / # pass 344 / # fail 0        (3 of them are the new file)

$ for p in social tree timeline; do (cd plugins/$p && TZ=UTC node --import ./test/resolve.mjs --test "test/*.test.js"); done
social   # tests 541 / # pass 541 / # fail 0
tree     # tests 127 / # pass 127 / # fail 0
timeline # tests 135 / # pass 135 / # fail 0

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test "test/*.test.js"
# tests 3159 / # pass 3152 / # fail 7

$ ARCH_DOCS_BASE=upstream/delivery/ai-centric-development-service npm --prefix core run lint
Found 105 warnings and 0 errors.          arch-docs: OK (3 changed file(s))

$ node scripts/lint-plugins.mjs
social 36 / timeline 9 / tree 2 / workflow 16 warnings, 0 errors in every plugin; all four pack-imports ran

$ node core/scripts/build-client.mjs --check                       # exit 0
$ node scripts/ds-bot-patches.mjs check && node scripts/ds-bot-patches.mjs canonical
ok   ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
ok   patches/ds-bot/ is what generate writes (generate is a no-op)
$ node .github/scripts/changelog.mjs check
changelog: 80 fragment(s) in changelog.d/ are well formed

The 7 core failures are the same environment group on the base tree, unrelated to this diff: test/project-runtime-setup.test.js (no Codex login cache on this box) and test/workbench-client.test.js (the six scoped-routing/composer cases). Base full run was # tests 3159 / # pass 3152 / # fail 7 — same names, same count.

Not run

  • npm run verify as one command stops before the suites on this box: core lint's arch-docs step calls git diff origin/main…, and this clone has no origin/main ref (its remote is the fork; the lane's base is the delivery branch). Each gate above was run individually, with ARCH_DOCS_BASE pointing at the lane's base.
  • node core/scripts/build-workbench.mjs --check fails here on ERR_MODULE_NOT_FOUND: Cannot find package 'react' — a dependency of this box, not of this diff; the workbench panel is untouched.
  • No smoke test: core/index.js and core/src/host/ are untouched.
  • No live Workflow run against a real Bot: the two bounds are pure functions and the three cases exercise them directly.

Docs-not-needed: no ownership, contract or module boundary changed; two text bounds in graph.js now cut on code points like cleanLine beside them. This line is also in the branch commit.

Checkpoint

{
  "pr": 2433,
  "claim": "Delivery-2",
  "issue": 2429,
  "summary": "cleanPrompt and readStepResult clipped with a bare String#slice, so a bound that landed between the halves of a surrogate pair stored a lone high surrogate, drawn as a replacement character. The bound also counted UTF-16 units, so an emoji-heavy prompt reached half the room of an ASCII one. One local cut() helper, the code-point cut cleanLine already made, now serves cleanLine, cleanPrompt and the step-result body, and three cases prove both bounds on code points.",
  "files": ["plugins/workflow/src/graph.js", "plugins/workflow/test/prompt-bounds.test.js", "changelog.d/2429-workflow-text-bounds-code-points.fixed.md"],
  "checks": { "ran": "cd plugins/workflow && node --import ./test/resolve.mjs --test \"test/*.test.js\" && ARCH_DOCS_BASE=upstream/delivery/ai-centric-development-service npm --prefix core run lint && node scripts/ds-bot-patches.mjs check && node .github/scripts/changelog.mjs check", "result": "green", "at": "d4e35ce43695df777b1a3f08d5ec7130c09a897f" },
  "next": "none — ready for review"
}

…inkFlowLab#2429)

Fixes ThinkFlowLab#2429

Written-by: Delivery-2

cleanPrompt and readStepResult clipped with a bare String#slice, so a bound
that landed between the halves of a surrogate pair stored a lone high
surrogate: a prompt of 3,999 characters plus an emoji came back as 4,000
units ending in \ud83d, which every encoder reads as U+FFFD and every reader
draws as a replacement character. cleanLine fourteen lines above already cut
on code points and says why.

The bound also counted UTF-16 units, so an emoji-heavy prompt reached half
the room of an ASCII one. One local cut() helper now serves all three
callers; no maximum, payload shape or endpoint changes.

Docs-not-needed: no ownership, contract or module boundary changed; two
text bounds in graph.js now cut on code points like cleanLine beside them.
@github-actions github-actions Bot added the risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff label Nov 4, 2026
@HugoXK

HugoXK commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Written-by: Delivery-2

@Birfy review request for this exact tip: dccec83422 (base delivery/ai-centric-development-service, merge base 5e71a07e; non-draft, maintainer_can_modify: true).

Red-first on a clean base plus the gates are in the PR body; enqueue-guard is red only because no Verifier report names this tip yet.

Please review dccec83422 and post Verdict: PASS (or the changes you want). No new tip will be pushed for CI noise: the base head moved to 5ba3c0bb and this branch is not rebased, per instruction.

@HugoXK

HugoXK commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Closing: our account has no write access to ThinkFlowLab/FlyBot, so it cannot merge into delivery/ai-centric-development-service; the fix is complete and verified at dccec834222f351cd84f0c745dcfd69216fe20aa, and we will re-open it against main or this branch the moment someone with merge rights wants it.

Owner: Delivery-2
Written-by: Delivery-2

@HugoXK HugoXK closed this Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant