Repository navigation
CLI: surrogate-safe truncation for goal objective + workspace-memory text (remaining raw slices deferred by #860) #868
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/session-goal.ts:28—safeObjective:return redacted.length <= 500 ? redacted :${redacted.slice(0, 499)}…;— rawslice(0, 499).src/workspace-memory.ts:161—remember:redactedText =${redactedText.slice(0, MEMORY_MAX_TEXT_CHARS)}… [truncated];(MEMORY_MAX_TEXT_CHARS = 2_000).src/workspace-memory.ts:287— display:?${entry.text.slice(0, DISPLAY_TEXT_CHARS)}…`` (DISPLAY_TEXT_CHARS = 500).- Contrast:
src/session-goal.ts:45(safeTitle, same file) already usessafeCutEnd— sosafeObjectiveis a same-file inconsistency. src/text-cut.ts—safeCutEndis the established surrogate-safe head-cut helper (used across the Robustness: make ellipsis truncation surrogate-safe — never split an emoji/astral character at the cut boundary #812–CLI: surrogate-safe tail truncation for task-verify failure output and failure receipts #860 series).
Minimal reproduction: a goal objective or memory note of the form "a".repeat(498) + "🚀 tail" straddles the cut boundary — slice(0, 499) keeps the rocket's high surrogate (index 498) but cuts its low partner (index 499), leaving an orphaned high surrogate immediately before the …. The #860 display path was fixed for this; these user-content sites were explicitly deferred.
problemStatement
Three user-authored-content truncation sites still cut with a raw String.prototype.slice(0, N). When an astral character (emoji, etc.) straddles the cut boundary, the kept prefix ends in an orphaned surrogate, producing malformed text in the persisted goal objective / memory entry and its display. This breaks the surrogate-safe truncation convention established by #812/#821 and extended through #860, which explicitly deferred workspace-memory and session-goal as "file separately if warranted."
userValue
Goal objectives and workspace-memory notes that contain emoji/astral characters truncate cleanly instead of ending in an unpaired surrogate — consistent with every other truncation path in the CLI.
scope
src/session-goal.tssafeObjective: cut viasafeCutEnd(already imported; mirror siblingsafeTitle).src/workspace-memory.ts: importsafeCutEndand cutremember(MEMORY_MAX_TEXT_CHARS) and the display path (DISPLAY_TEXT_CHARS) via it.- Keep the
…/… [truncated]markers and truncation thresholds unchanged. - Red→green regression tests for each site.
nonGoals
- Not changing truncation thresholds, markers, redaction, or the terminal-safety pipeline.
- Command/name paths with lower astral probability (
repo-context.tsredactName/redactCommand,repo-readiness.ts:125,run-bottleneck.ts:59 safeName,hook-contract.ts,task-runtime.ts) are out of scope here; file separately if warranted. - Hex/hash/array slices (e.g.
randomUUID().slice(0, 8), githead.slice(0, 12)) are ASCII and unaffected.
acceptanceCriteria
- Given a goal objective / memory text 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. safeObjectiveandsafeTitleuse the samesafeCutEndmechanism.- Inputs below the threshold are returned unchanged.
testPlan
- Regression tests in
tests/unit/session-goal.test.tsandtests/unit/workspace-memory.test.ts: surrogate-straddle at each boundary, in-range astral passthrough, and long-ASCII truncation. - Red→green: reverting the three 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/session-goal.test.ts tests/unit/workspace-memory.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 terminal-safety pipelines are untouched. Tests must use inert local strings (no secrets). No new attack surface.
duplicateSearchEvidence
gh search issues "workspace-memory truncat"/"session-goal surrogate"-> no results.gh search issues "safeCutEnd"-> only the closed truncation series Robustness: make ellipsis truncation surrogate-safe — never split an emoji/astral character at the cut boundary #812/Robustness: complete surrogate-safe ellipsis truncation across the ~26 remaining raw-string sites (follow-up to #812) #821/Robustness: make the hook output-size cap surrogate-safe (spawnHookRunner) #830/Robustness: surrogate-safe truncation for subagent result and side-question content clamps #840/Robustness: surrogate-safe truncation for tui-shell tool-text rendering (transcript, output, hints) #842/Robustness: session-export clamp() splits surrogate pairs — exported transcripts can carry an unpaired surrogate at the truncation boundary #854; none coverssafeObjectiveorworkspace-memory.- CLI: surrogate-safe tail truncation for task-verify failure output and failure receipts #860 (surrogate-safe tail truncation, closed) explicitly listed
workspace-memory/session-goalas deferred follow-ups.
parentChildRelationship
Standalone leaf subtask; no parent roadmap item required and no children anticipated.
dependencyOrder
No dependencies; dependency-ready. Self-contained change to session-goal.ts and workspace-memory.ts (+ tests) using the existing text-cut.ts helper.
Activity
github-actions commentedon Aug 12, 2026
Automated intake triage
bugpriority:p30.90Goal objectives and workspace-memory text truncation can leave orphaned surrogate characters when emoji or other astral characters straddle the truncation boundary.
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/868-goal-memory-truncationfrommain.safeCutEndhelper (src/text-cut.ts):src/session-goal.ts:28safeObjective(mirror siblingsafeTitle;safeCutEndalready imported).src/workspace-memory.ts:161remember(MEMORY_MAX_TEXT_CHARS) and:287display (DISPLAY_TEXT_CHARS) — add thesafeCutEndimport.tests/unit/session-goal.test.tsandtests/unit/workspace-memory.test.ts(surrogate-straddle at each boundary, in-range astral passthrough, long-ASCII truncation).At most one active lease/branch/PR; identity verified as
qwen-code-dev-bot.Merge pull request #869 from qwen-code-dev-bot/issue/868-goal-memory-…
qwen-code-dev-bot commentedon Aug 12, 2026
Completed and merged via PR #869 (merge commit on main, 263c707).
Delivered — surrogate-safe truncation for the user-content sites deferred by #860:
src/session-goal.tssafeObjectivenow cuts viasafeCutEnd(matching siblingsafeTitle; resolves the same-file inconsistency).src/workspace-memory.tsaddWorkspaceMemory(MEMORY_MAX_TEXT_CHARS) and the memory-list display (DISPLAY_TEXT_CHARS) now cut viasafeCutEnd(import added).An astral character straddling any of these bounds is dropped whole instead of orphaned before the ellipsis/truncation marker. Thresholds, markers, and redaction unchanged.
Red→green proof — reverting the three cuts to raw
slicemade all three new regression tests fail (truncated output ended in an orphaned surrogate); restoring the fix turned them green.Gates — typecheck, build, unit (4153), 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 (58/58). Lease released; branch cleaned;
main == origin/main.