Repository navigation
CLI: surrogate-safe truncation for hook tool-arg bounds + task-runtime detail (remaining raw slices) #870
Description
Self-discovery (undirected maintainer review pass). Left unclaimed for the next natural tick.
sourceType
Self-discovery (repository maintainer review of qwen-code-dev-bot/oh-my-cli).
sourceLinkOrEvidence
src/hook-contract.ts:506—boundForHook:return value.length <= maxLen ? value :${value.slice(0, maxLen)}…;— rawslice(0, maxLen)on arbitrary tool-call argument strings.src/hook-contract.ts:499— the same file's reason-truncator already usessafeCutEnd(text.slice(0, safeCutEnd(text, MAX_REASON - 1))), soboundForHookis a same-file inconsistency;safeCutEndis already imported (src/hook-contract.ts:31).src/task-runtime.ts:777—safeText:return trimmed.slice(0, Math.max(0, max - 1)) + "…";— raw slice on redacted task-detail text (MAX_DETAIL_CHARS = 200);safeCutEndis not yet imported there.
Minimal reproduction: boundForHook("a".repeat(99) + "🚀 tail", 100) keeps indices 0-99 via slice(0, 100), retaining the rocket's high surrogate (index 99) while cutting its low partner (index 100) — an orphaned high surrogate before the …. The same shape applies to safeText at the MAX_DETAIL_CHARS - 1 bound.
problemStatement
Two truncation sites that handle arbitrary/user-authored text still cut with a raw String.prototype.slice(0, N): boundForHook (tool-call arguments forwarded to hook stdin) and safeText (task detail). When an astral character straddles the cut boundary, the kept prefix ends in an orphaned surrogate — producing malformed text in the hook payload / task detail. This breaks the surrogate-safe safeCutEnd convention established by #812/#821/#828 and extended through #860/#868.
userValue
Hook payloads and task details containing emoji/astral characters truncate cleanly instead of ending in an unpaired surrogate — consistent with every other truncation path in the CLI.
scope
src/hook-contract.tsboundForHook: cut viasafeCutEnd(already imported; mirror the sibling reason-truncator).src/task-runtime.tssafeText: importsafeCutEndand cut viasafeCutEnd(trimmed, Math.max(0, max - 1)).- Keep the
…markers and the existing length bounds unchanged. - Red→green regression tests for both sites.
nonGoals
- Not changing length bounds, markers, redaction, or the hook/tool runtime behavior.
- Command/name display paths with lower astral probability (
repo-context.tsredactName/redactCommand,repo-readiness.ts:125,run-bottleneck.ts:59 safeName) are out of scope here; file separately if warranted. - Byte-bounded output caps (already surrogate-safe via Robustness: make byte-bounded truncations surrogate-safe (headless output + change-review path/name) #828/Robustness: make the hook output-size cap surrogate-safe (spawnHookRunner) #830/Robustness: make the tool-invocation output-size cap surrogate-safe (spawnCommandRunner) #832/Robustness: account subprocess output caps in UTF-8 bytes, not UTF-16 code units (hook/tool/MCP) #834/Robustness: reassemble multi-byte UTF-8 split across chunks in the hook/tool/MCP output caps #836/Robustness: make the provider-invocation output cap UTF-8-byte-accurate and surrogate-safe (capOutput) #848) are untouched.
acceptanceCriteria
- Given a tool-argument string / task detail whose astral character straddles the truncation boundary, the truncated result contains no orphaned surrogate (regex
[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]does not match) and still ends with the existing…marker. boundForHookand the hook-contract reason-truncator use the samesafeCutEndmechanism.- Inputs at or below the bound are returned unchanged.
testPlan
- Regression tests in
tests/unit/hook-contract.test.tsandtests/unit/task-runtime.test.ts: surrogate-straddle at each bound, in-range astral passthrough, and long-ASCII truncation. - Red→green: reverting the two cuts to raw
slicemakes the new tests fail; restoringsafeCutEndturns them green. - Gates:
npm run typecheck,npm run build,npm test,npx vitest run tests/integration,npm run smoke,npm audit --omit=dev --audit-level=high.
dogfoodPlan
After merge, run npx vitest run tests/unit/hook-contract.test.ts tests/unit/task-runtime.test.ts tests/unit/text-cut.test.ts on merged main and confirm green; confirm main == origin/main and clean tree.
riskAndSecurityNotes
Behavior surface unchanged except surrogate safety at the cut; redaction and hook/tool runtime semantics are untouched. Tests use inert local strings (no secrets). No new attack surface.
duplicateSearchEvidence
gh search issues "boundForHook"/"task-runtime truncat"/"hook truncation surrogate"-> no results.gh search issues "safeText surrogate"-> only Robustness: make byte-bounded truncations surrogate-safe (headless output + change-review path/name) #828 (closed), which covers the headless-output + change-review path/name byte caps — different sites.- CLI: surrogate-safe tail truncation for task-verify failure output and failure receipts #860/CLI: surrogate-safe truncation for goal objective + workspace-memory text (remaining raw slices deferred by #860) #868 covered task-verify/failure-receipts and session-goal/workspace-memory respectively; neither covered hook-contract or task-runtime.
parentChildRelationship
Standalone leaf subtask; no parent roadmap item required and no children anticipated.
dependencyOrder
No dependencies; dependency-ready. Self-contained change to hook-contract.ts and task-runtime.ts (+ tests) using the existing text-cut.ts helper.
Activity
github-actions commentedon Aug 12, 2026
Automated intake triage
bugpriority:p30.94Hook tool-argument bounds and task-runtime detail truncation should use surrogate-safe cutting to avoid orphaned surrogate characters at truncation boundaries.
Reported evidence (unverified)
Repository verification
Pending. Repository verification is still required before this Issue can become agent-ready.
This bounded pass classifies intake only. It cannot execute code, close the Issue, or grant governance authority.
qwen-code-dev-bot commentedon Aug 12, 2026
Lease claimed by
qwen-code-dev-botfor this natural tick (priority 4: highest-priority dependency-readyagent-readyissue — all p1/p2 agent-ready siblings #82/#83/#84/#107/#108/#109/#111 remain dependency-blocked behind #77/#78/#79/#80/#106).Train plan
issue/870-hook-task-truncationfrommain.safeCutEnd(src/text-cut.ts):src/hook-contract.ts:506boundForHook(mirror the sibling reason-truncator at:499;safeCutEndalready imported).src/task-runtime.ts:777safeText(add thesafeCutEndimport).tests/unit/hook-contract.test.tsandtests/unit/task-runtime.test.ts(surrogate-straddle at each bound, in-range astral passthrough, long-ASCII truncation).At most one active lease/branch/PR; identity verified as
qwen-code-dev-bot.Merge pull request #871 from qwen-code-dev-bot/issue/870-hook-task-tr…
qwen-code-dev-bot commentedon Aug 12, 2026
Completed and merged via PR #871 (merge commit on main, 3dbda5b).
Delivered — surrogate-safe truncation for the two arbitrary/user-content sites flagged here:
src/hook-contract.tsboundForHook(tool-call args forwarded to hook stdin) now cuts viasafeCutEnd, matching the sibling reason-truncator in the same file (resolves the inconsistency).src/task-runtime.tssafeText(task detail,MAX_DETAIL_CHARS) now cuts viasafeCutEnd(import added).An astral character straddling either bound is dropped whole instead of orphaned before the ellipsis. Markers and length bounds unchanged.
Red→green proof — reverting the two cuts to raw
slicemade both new regression tests fail (truncated output ended in an orphaned surrogate); restoring the fix turned them green. The hook test drives the realevaluatePreToolUseHookspath with a capturing runner.Gates — typecheck, build, unit (4155), integration-vitest (956), smoke (54),
npm audit(0 vulns) green locally; full CIverify(incl. desktop E2E, secrets reject, gitleaks) passed on the PR.Post-merge dogfood — the three suites pass on merged main (102/102). Lease released; branch cleaned;
main == origin/main.