Skip to content

fix: cut a long conversation opening on a character boundary - #4

Merged
VikramLex merged 1 commit into
Graphicle-AI:mainfrom
rudycelekli:fix/memory-openings-multibyte-cut
Oct 18, 2026
Merged

VikramLex merged 1 commit into
Graphicle-AI:mainfrom
rudycelekli:fix/memory-openings-multibyte-cut

Conversation

@rudycelekli

Copy link
Copy Markdown
Contributor

graphicle_memory::standing::openings panics on a conversation whose first user sentence is longer than 300 bytes when byte 300 of it falls inside a multi-byte character.

What happens

User: I need help with éééé… (200 × "é")

byte index 306 is not a char boundary; it is inside 'é' (bytes 305..307) of `User: I need help with ééé…`

Text in any language beyond ASCII can trigger it: two-byte Latin or Cyrillic letters, three-byte CJK. observer::outline calls openings on each conversation document it outlines, so one such document makes it panic.

Root cause

An opening longer than 300 bytes is shortened to its last word within the first 300 bytes:

b = a + text[a..a + 300].rfind(' ').unwrap_or(300);
while !text.is_char_boundary(b) { b -= 1; }

The boundary loop protects b, but text[a..a + 300] slices first and panics before the loop runs.

Change

Step the 300-byte cut back to a character boundary first, then look for the last space before it. When a space is found, b is that space as before; when none is found, b is the cut, the same value the old loop produced. So the result is unchanged in every case that did not panic.

Test

standing::tests::a_long_opening_ends_on_a_character_boundary: an opening where byte 300 is inside an é (cut at the last space), and one with no space in its first 300 bytes where byte 300 is inside a CJK character (cut at the last whole character). On main the first case panics.

Verification

In crates/graphicle-memory, with Rust 1.95.0:

  • cargo fmt --check: clean
  • cargo clippy --locked --all-targets -- -D warnings: clean on Rust 1.95.0 (macOS).
  • cargo test --locked --all-targets: 168 passed, 0 failed.

I left CHANGELOG.md to you.

🤖 Generated with Claude Code

`standing::openings` shortens an opening longer than 300 bytes to its
last word in the first 300 bytes, and took those bytes as
`text[a..a + 300]`. When byte 300 falls inside a multi-byte character
(any accented, Cyrillic or CJK text) the slice panics before the
boundary fix that follows it runs, so the observer's conversation
outline (`observer::outline`, which calls `openings` on each
conversation document it outlines) panics on such a document.

The 300-byte cut now steps back to a character boundary before the
slice; the result is unchanged whenever the old code did not panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
@VikramLex
VikramLex merged commit d197ac6 into Graphicle-AI:main Oct 18, 2026
VikramLex added a commit that referenced this pull request Oct 18, 2026
…-off

Dashboard: the app that `graphicle open` shows, and its product and
documentation pages, get a light theme. It follows the operating
system's light or dark setting; Settings > Appearance picks System,
Light or Dark, kept per browser. The theme is set before the first
paint by /theme.js (the Content-Security-Policy allows no inline
script). Every colour of the stylesheets is a token whose dark value is
the old colour, so the dark theme is unchanged; in the light theme, text
is at least 4.5:1 against its background, and control borders and focus
rings at least 3:1.

MCP Registry: from this release on, the mcp-registry workflow adds one
MCP bundle (.mcpb) for each release target to the GitHub release and
lists the release as io.github.Graphicle-AI/graphicle
(packaging/mcpb/, .github/workflows/mcp-registry.yml, glama.json).

Update check: each check is recorded before its request goes out, so a
check still running when a command ends counts as that day's check
instead of running again with the next command. Checks whose requests
fail, or that GitHub refuses or limits, back off a day, then two, then
four. Still one GET with no code or data, and the notice at most once a
day.

Other changes:
- The rcfile tests that use the process-wide state hold a lock, so one
  test can no longer clear the warned folders between another's calls.
- The engine's file ledger marks each application module with a passing
  comparison receipt against Graphify 0.9.91 as verified; STATUS.md,
  AUDIT.json and PROGRESS.json describe the 0.9.91 port.
- cargo-deny ignores RUSTSEC-2026-0367, RUSTSEC-2026-0368 and
  RUSTSEC-2026-0369 (zip 8.6.0): they affect only zip's extract-to-disk
  APIs, and Graphicle reads archive members in memory and never
  extracts them. The ignores go once calamine moves to zip 9.
- Every crate is at 0.1.2, with a CHANGELOG entry. It also credits the
  four contributed fixes merged just before this commit: #3 (a Codex
  config with non-ASCII text in a multi-line string), #4 (a conversation
  opening cut on a character boundary), #5 (initialize answers a protocol
  version the engine speaks) and #6 (Method not found for an unknown MCP
  method), all by @rudycelekli.

Closes #2

Signed-off-by: VikramLex <vikramlex@karlexai.com>
@VikramLex

Copy link
Copy Markdown
Member

Thank you, @rudycelekli. Merged for 0.1.2, with your test. Backing off to a character boundary before looking for the space fixes the panic and keeps the cut where it was for ASCII text. The CHANGELOG credits you.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants