Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions contextfun/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,7 +2037,7 @@ def cmd_pack(args: argparse.Namespace):
lines.append("")
lines.append("## Pinned context")
for e in pinned_entries:
preview = (e["content"] or "").strip()
preview = _neutralize_pack_sentinels((e["content"] or "").strip())
if len(preview) > 500:
preview = preview[:497] + "..."
lines.append(f"- E{e['id']} S{e['session_id']} `{_entry_display_label(e)}`:\n\n {preview}")
Expand All @@ -2053,7 +2053,7 @@ def cmd_pack(args: argparse.Namespace):
title += f" (types: {', '.join(sorted(focus))})"
lines.append(title)
for e in recent_entries:
preview = (e["content"] or "").strip()
preview = _neutralize_pack_sentinels((e["content"] or "").strip())
if len(preview) > 500:
preview = preview[:497] + "..."
lines.append(f"- E{e['id']} S{e['session_id']} `{_entry_display_label(e)}`:\n\n {preview}")
Expand All @@ -2073,7 +2073,7 @@ def cmd_pack(args: argparse.Namespace):
lines.append("")
lines.append("Pinned context:")
for e in pinned_entries:
preview = (e["content"] or "").strip().replace("\n", " ")
preview = _neutralize_pack_sentinels((e["content"] or "").strip().replace("\n", " "))
if len(preview) > 160:
preview = preview[:157] + "..."
lines.append(f"- E{e['id']} S{e['session_id']} {_entry_display_label(e)}: {preview}")
Expand All @@ -2086,13 +2086,20 @@ def cmd_pack(args: argparse.Namespace):
lines.append("")
lines.append("Recent entries:" + (f" (types: {', '.join(sorted(focus))})" if focus else ""))
for e in recent_entries:
preview = (e["content"] or "").strip().replace("\n", " ")
preview = _neutralize_pack_sentinels((e["content"] or "").strip().replace("\n", " "))
if len(preview) > 160:
preview = preview[:157] + "..."
lines.append(f"- E{e['id']} S{e['session_id']} {_entry_display_label(e)}: {preview}")
print("\n".join(lines))


def _neutralize_pack_sentinels(text: str) -> str:
# Prevent entry content from closing the outer <ctx-pack>…</ctx-pack> wrapper
# emitted by the resume skill (scripts/ctx_cmd.py). A zero-width space is
# invisible but breaks the literal tag match used by consumers.
return text.replace("</ctx-pack>", "</ctx-pack​>").replace("<ctx-pack>", "<ctx-pack​>")


def _read_stdin_if_dash(text_arg):
if text_arg == "-":
return sys.stdin.read()
Expand Down
58 changes: 58 additions & 0 deletions tests/test_pack_sentinel_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Regression tests for _neutralize_pack_sentinels (contextfun/cli.py).

Entry content that contains a literal </ctx-pack> would prematurely close the
<ctx-pack>…</ctx-pack> wrapper emitted by the resume skill, so a consumer that
splits on the literal tag mis-parses the pack. The guard inserts a zero-width
space inside any such tag: the literal match breaks while the text stays
visually identical.
"""

import importlib.util
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CLI_PY = ROOT / "contextfun" / "cli.py"

ZWSP = "​"


def _load_cli_module():
spec = importlib.util.spec_from_file_location("ctx_cli_module", CLI_PY)
module = importlib.util.module_from_spec(spec)
assert spec is not None and spec.loader is not None
spec.loader.exec_module(module)
return module


class PackSentinelGuardTests(unittest.TestCase):
def setUp(self):
self.cli = _load_cli_module()
self.guard = self.cli._neutralize_pack_sentinels

def test_closing_tag_no_longer_matches_literal(self):
out = self.guard("notes </ctx-pack> more")
self.assertNotIn("</ctx-pack>", out)
self.assertIn("</ctx-pack" + ZWSP + ">", out)

def test_opening_tag_no_longer_matches_literal(self):
out = self.guard("<ctx-pack> injected")
self.assertNotIn("<ctx-pack>", out)
self.assertIn("<ctx-pack" + ZWSP + ">", out)

def test_guarded_text_is_visually_identical_when_zwsp_stripped(self):
evil = "a </ctx-pack> b <ctx-pack> c"
self.assertEqual(self.guard(evil).replace(ZWSP, ""), evil)

def test_benign_text_is_unchanged(self):
benign = "ordinary entry with <other> tags & </closing> bits"
self.assertEqual(self.guard(benign), benign)

def test_multiple_occurrences_all_guarded(self):
out = self.guard("</ctx-pack> x </ctx-pack>")
self.assertNotIn("</ctx-pack>", out)
self.assertEqual(out.count("</ctx-pack" + ZWSP + ">"), 2)


if __name__ == "__main__":
unittest.main()