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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Released versions are named in the entry title; the version on PyPI is
[deepcode-hku](https://pypi.org/project/deepcode-hku/). 中文版见
[CHANGELOG_ZH.md](CHANGELOG_ZH.md).

## 2026-10-31 · Keep distinct memory topics in the index

- **Preserve different topic links during memory consolidation.** A filename
prefix or a `|` inside a title or target no longer makes separate pointers
collapse into one; repeated links still merge and prose still deduplicates.

## 2026-10-29 · Lighter compaction, checked Goal completion, and folder browsing on the Web

- **Clear old tool output first.** With `agents.defaults.microcompact` on,
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ DeepCode 的主要变化,最新的在前,每天一条。正式版本号写
PyPI 上的包名是 [deepcode-hku](https://pypi.org/project/deepcode-hku/)。
English: [CHANGELOG.md](CHANGELOG.md).

## 2026-10-31 · 整理记忆索引时保留不同主题

- **不再误合并不同主题链接。** 文件名前缀相同、标题或目标中包含 `|` 时,
不同的记忆指针仍会保留;重复指针和正文去重规则保持不变。

## 2026-10-29 · 更轻的上下文压缩、目标完成前先检查、Web 端可浏览文件夹

- **先清掉旧的工具输出。** 打开 `agents.defaults.microcompact` 后,长会话接近
Expand Down
20 changes: 12 additions & 8 deletions core/harness/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,27 +764,31 @@ def _line_key(line: str) -> str:
return text.rstrip(" .,;:!—-")


def _candidate_key(line: str) -> str:
_CandidateKey = str | tuple[str, str]


def _candidate_key(line: str) -> _CandidateKey:
"""Dedupe key: pointer identity for pointers, normalized text otherwise."""
pointer = parse_memory_pointer(line)
if pointer is not None:
# Same title + target with a different hook is one topic, not two.
return f"{pointer.title.casefold()}|{pointer.target.casefold()}"
return (pointer.title.casefold(), pointer.target.casefold())
return _line_key(line)


def _drop_subsumed(order: Sequence[str]) -> list[str]:
def _drop_subsumed(order: Sequence[_CandidateKey]) -> list[_CandidateKey]:
"""Keys whose normalized form is a strict prefix of a longer entry.

Safe by construction: a strict prefix is literally contained in the longer
line, so folding the stub into it cannot lose content.
"""
dropped: set[str] = set()
dropped: set[_CandidateKey] = set()
for key in order:
if len(key) < _MIN_USEFUL_CHARS:
# Prefix subsumption applies to prose, never to distinct topic links.
if not isinstance(key, str) or len(key) < _MIN_USEFUL_CHARS:
continue
for other in order:
if other == key or other in dropped:
if not isinstance(other, str) or other == key or other in dropped:
continue
if len(other) > len(key) and other.startswith(key):
dropped.add(key)
Expand All @@ -801,8 +805,8 @@ def consolidate(lines: Sequence[str]) -> list[str]:
order is first-seen, and first-seen is decided solely by the input
sequence: no set or dict iteration decides anything.
"""
unique: dict[str, str] = {}
order: list[str] = []
unique: dict[_CandidateKey, str] = {}
order: list[_CandidateKey] = []
for raw in lines:
text = " ".join(str(raw).split())
key = _candidate_key(text)
Expand Down
40 changes: 40 additions & 0 deletions tests/test_memory_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
MemoryPointer,
consolidate,
consolidate_memory_index,
consolidate_pointer_index,
fetch_memory_topic,
is_pointer_index,
memory_dir,
Expand Down Expand Up @@ -256,6 +257,45 @@ def test_consolidate_dedupes_and_merges_prefixes():
]


@pytest.mark.parametrize(
"lines",
[
[
"- [API reference](api.md) — current endpoint contract",
"- [API reference](api.md.v2.md) — next version contract",
],
[
"- [A|B](c.md) — first topic",
"- [A](B|c.md) — separate topic",
],
],
)
@pytest.mark.parametrize("reverse", [False, True])
def test_consolidate_keeps_distinct_pointer_identities(lines, reverse):
if reverse:
lines = list(reversed(lines))
assert consolidate(lines) == lines


def test_pointer_index_rewrite_keeps_distinct_topics(tmp_path):
first = "- [API reference](api.md) — current endpoint contract"
second = "- [API reference](api.md.v2.md) — next version contract"
index = _write(tmp_path, "MEMORY.md", f"{first}\n{second}\n{first}\n")
_write(tmp_path, "api.md", "Current endpoint: /v1/search\n")
_write(tmp_path, "api.md.v2.md", "Next endpoint: /v2/search\n")

assert consolidate_pointer_index(tmp_path)

assert index.read_text(encoding="utf-8") == f"{first}\n{second}\n"
assert (
fetch_memory_topic(tmp_path, "api.md").text == "Current endpoint: /v1/search\n"
)
assert (
fetch_memory_topic(tmp_path, "api.md.v2.md").text
== "Next endpoint: /v2/search\n"
)


def test_prune_line_cap_at_the_boundary():
lines = ["first line", "second line", "third line"]
assert prune(lines, max_lines=3) == lines # exactly at the cap
Expand Down