Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4bdbf563 |
No files matched your search
@@ -86,6 +86,7 @@ MEMORY_VECTOR_STORE=chroma
|
||||
# MEMORY_CODING_DB_PATH= # coding_memory.db 路径(默认 {DATA_DIR}/data/coding_memory.db)
|
||||
# MEMORY_CODING_TOOL_RESULT_MAX_BYTES=2048 # 单条 tool_result 截断字节上限
|
||||
# MEMORY_CODING_VERBATIM_FALLBACK_ENABLED=false # 模型无结果时是否保存原文 fact;默认关,避免 tool trace 外泄
|
||||
# MEMORY_CODING_CHAIN_BLOCK_ENABLED=false # search 是否注入版本演进链块(diff);默认关,省 token
|
||||
# MEMORY_EXTRACT_HISTORY_TURNS=5 # 除本次 write 外,额外注入 extractor 的历史 user-turn 数
|
||||
|
||||
# ── 写入流程开关 ──
|
||||
|
||||
@@ -14,6 +14,10 @@ Hy Memory and MemHub are versioned and released independently.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Coding search no longer injects the version-evolution diff block
|
||||
(`chain_block`) by default. Enable with
|
||||
`MEMORY_CODING_CHAIN_BLOCK_ENABLED=true` or
|
||||
`CodingConfig.chain_block_enabled=True` (write-side observe unchanged).
|
||||
- Fixed governed-memory revisions and repeated evidence links failing when the
|
||||
visibility dirty-marker trigger encounters the same memory twice. Startup
|
||||
now replaces the affected legacy trigger without changing schema versions
|
||||
|
||||
@@ -153,7 +153,7 @@ result = client.search(
|
||||
|
||||
result["facts"] # fact layer
|
||||
result["experiences"] # experience layer
|
||||
result["chain_block"] # coding only: version-evolution context
|
||||
result["chain_block"] # coding only: version-evolution context (empty unless MEMORY_CODING_CHAIN_BLOCK_ENABLED)
|
||||
```
|
||||
|
||||
### 5. Routing, Source Isolation & Advanced APIs
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ result = client.search(
|
||||
|
||||
result["facts"] # 事实层
|
||||
result["experiences"] # 经验层
|
||||
result["chain_block"] # 仅 coding:版本演进上下文
|
||||
result["chain_block"] # 仅 coding:版本演进上下文(默认空;需 MEMORY_CODING_CHAIN_BLOCK_ENABLED)
|
||||
```
|
||||
|
||||
### 5. 自动路由、来源过滤与进阶 API
|
||||
|
||||
@@ -197,6 +197,7 @@ SDK 不设置 Rerank Search 并发上限。开启后模型在 `HyMemoryClient`
|
||||
| `MEMORY_HISTORY_DB_PATH` | `{DATA_DIR}/data/history.db` | History SQLite 路径 |
|
||||
| `MEMORY_HISTORY_RECORD_SEARCHES` | `false` | 是否记录 Search 操作 |
|
||||
| `MEMORY_CODING_CHAIN_REPO_ROOT` | — | coding 版本链读取本地 Git diff 的仓库根目录 |
|
||||
| `MEMORY_CODING_CHAIN_BLOCK_ENABLED` | `false` | search(scene=coding) 是否注入版本演进链块(diff / `chain_block`);默认关 |
|
||||
|
||||
## HTTP Server
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ default 多-query 超过 `MEMORY_QUERY_REWRITE_MAX_CHARS` 时不改写:Coding
|
||||
Coding 关闭则直接使用最后一条 query。
|
||||
|
||||
路由到 `working` / `coding` 后,Search 读取独立生产力记忆库,返回 `facts` /
|
||||
`experiences`,不使用 chat 三通道结构。`coding` 还返回 `chain_block` / `chain_used`。
|
||||
`experiences`,不使用 chat 三通道结构。`coding` 还返回 `chain_block` / `chain_used`
|
||||
(默认空;需 `MEMORY_CODING_CHAIN_BLOCK_ENABLED` 才注入 diff 块)。
|
||||
|
||||
## Chat 成功返回结构
|
||||
|
||||
|
||||
@@ -168,9 +168,11 @@ working/coding 搜索返回:
|
||||
|
||||
`limit` 分别限制 facts 与 experiences,因此两层最多各返回 `limit` 条。
|
||||
|
||||
`scene="coding"` 额外返回 `chain_block` 和 `chain_used`:
|
||||
`scene="coding"` 额外返回 `chain_block` 和 `chain_used` 字段(键始终存在;默认内容为空):
|
||||
|
||||
- `chain_block` 是与已召回 fact 对齐的版本演进上下文;无法挂链时为 `""`。
|
||||
- 注入 diff / 版本演进块需显式开启 `MEMORY_CODING_CHAIN_BLOCK_ENABLED=true`
|
||||
(或 `CodingConfig.chain_block_enabled=True`)。默认关闭,以节约 prompt token。
|
||||
- 开启后,`chain_block` 是与已召回 fact 对齐的版本演进上下文;无法挂链时为 `""`。
|
||||
- `chain_used` 是已经进入 chain block 的 `facts` 下标。
|
||||
- SDK 不会从 `facts` 删除这些条目。调用方若要拼成“chain block + 剩余 facts”,需自行按 `chain_used` 过滤。
|
||||
|
||||
@@ -222,10 +224,12 @@ working/coding 搜索返回:
|
||||
| `MEMORY_CODING_DB_PATH` | `{DATA_DIR}/data/coding_memory.db` | working 基础 SQLite 路径;coding/trace 路径由此派生 |
|
||||
| `MEMORY_CODING_TOOL_RESULT_MAX_BYTES` | `2048` | 单条 tool result 的字节上限 |
|
||||
| `MEMORY_CODING_VERBATIM_FALLBACK_ENABLED` | `false` | 模型无结果时是否保存原文 fact;默认关闭,避免 assistant/tool trace 直接展示 |
|
||||
| `MEMORY_CODING_CHAIN_BLOCK_ENABLED` | `false` | search 是否注入版本演进链块(`chain_block` / diff);默认关闭(opt-in) |
|
||||
| `MEMORY_EXTRACT_HISTORY_TURNS` | `5` | 回读的历史 user-turn 数;当前 prompt 最终最多使用 5 |
|
||||
| `MEMORY_CODING_CHAIN_REPO_ROOT` | 空 | coding 版本链读取本地 Git diff 的仓库路径 |
|
||||
|
||||
`HCM_*` 变量属于版本链的高级调优项,不是稳定的公共配置契约;普通接入不应依赖其默认值。
|
||||
其中 `HCM_BLOCK_OFF=1` 可在已开启 `MEMORY_CODING_CHAIN_BLOCK_ENABLED` 时做消融(完全不出块)。
|
||||
|
||||
## 8. 定向验证
|
||||
|
||||
|
||||
+2
-1
@@ -210,7 +210,8 @@ result = client.search(
|
||||
```
|
||||
|
||||
`working` / `coding` 路径返回 `scene="productivity"`、`facts`、`experiences`,不使用
|
||||
chat 的三桶结构;`coding` 还会返回 `chain_block` 和 `chain_used`。
|
||||
chat 的三桶结构;`coding` 还会返回 `chain_block` 和 `chain_used` 字段(默认空;
|
||||
需 `MEMORY_CODING_CHAIN_BLOCK_ENABLED=true` 才注入 diff 块)。
|
||||
|
||||
#### `search_mode`
|
||||
|
||||
|
||||
+18
-2
@@ -1192,6 +1192,19 @@ class HyMemoryClient:
|
||||
logger.warning("[coding] chain observe failed: %s", e)
|
||||
return 0
|
||||
|
||||
def _chain_block_enabled(self) -> bool:
|
||||
"""search 是否注入 coding 版本演进链块(diff / chain_block)。
|
||||
|
||||
公共开关:CodingConfig.chain_block_enabled /
|
||||
MEMORY_CODING_CHAIN_BLOCK_ENABLED(默认 False,opt-in)。
|
||||
无 _config 时按关闭处理。高级消融开关 HCM_BLOCK_OFF=1 仍在
|
||||
chain_store.decorate 内生效(仅当本开关已开启时才有意义)。
|
||||
"""
|
||||
coding = getattr(getattr(self, "_config", None), "coding", None)
|
||||
if coding is None:
|
||||
return False
|
||||
return bool(getattr(coding, "chain_block_enabled", False))
|
||||
|
||||
def _decorate_with_chain(self, facts, instruction: str = "", scope=None,
|
||||
user_id=None, workspace_id=None):
|
||||
"""读取侧:给向量层召回的 facts 挂上版本演进链块。
|
||||
@@ -1204,11 +1217,14 @@ class HyMemoryClient:
|
||||
facts 顺序不重排 —— top-k 是向量层的事,这层只负责"每条 fact 后面跟什么"。
|
||||
|
||||
best-effort:失败返回 ("", []),退化成纯向量双层(= working 的行为)。
|
||||
默认不注入(见 _chain_block_enabled);需显式开启 chain_block_enabled。
|
||||
"""
|
||||
if not facts:
|
||||
return "", []
|
||||
# 走类而不是 self:_chain_owner 是 staticmethod,而这两个方法会被测试绑到裸
|
||||
# SimpleNamespace 上调(同 _scene_variant 那处),self 上没有这个属性。
|
||||
# 走类而不是 self:测试常把本方法绑到裸 SimpleNamespace 上,self 上未必有
|
||||
# 同名 bound method;_chain_owner 同理。
|
||||
if not HyMemoryClient._chain_block_enabled(self):
|
||||
return "", []
|
||||
store = self._get_chain_store(
|
||||
HyMemoryClient._chain_owner(user_id, workspace_id))
|
||||
if store is None:
|
||||
|
||||
@@ -647,7 +647,9 @@ class HyCodeMem:
|
||||
top-k,但它们的 diff 对当前工作区不可能有用,因此挂链前必须做同仓过滤。
|
||||
"""
|
||||
facts = facts or []
|
||||
# 开关: HCM_BLOCK_OFF=1 → 完全不出块, 也不从 bullet 删任何 fact。
|
||||
# 高级消融开关: HCM_BLOCK_OFF=1 → 完全不出块, 也不从 bullet 删任何 fact。
|
||||
# 公共 opt-in 见 CodingConfig.chain_block_enabled /
|
||||
# MEMORY_CODING_CHAIN_BLOCK_ENABLED(默认关;客户端在调用 decorate 前拦截)。
|
||||
if os.environ.get("HCM_BLOCK_OFF") == "1":
|
||||
return "", []
|
||||
picked, used = [], []
|
||||
|
||||
@@ -1136,6 +1136,9 @@ class CodingConfig:
|
||||
# LLM 失败或没有抽出 fact 时,是否把对话原文存成 working fact。
|
||||
# 默认关闭,避免 assistant/tool 轨迹以 statement 形式直接暴露给消费方。
|
||||
verbatim_fallback_enabled: bool = None
|
||||
# search(scene="coding") 是否注入版本演进链块(diff / chain_block)。
|
||||
# 默认关闭(opt-in):中重度 coding 用户可省下大量 prompt token;写入侧仍可 observe。
|
||||
chain_block_enabled: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.enable is None:
|
||||
@@ -1153,6 +1156,10 @@ class CodingConfig:
|
||||
self.verbatim_fallback_enabled = _get_env_bool(
|
||||
"MEMORY_CODING_VERBATIM_FALLBACK_ENABLED", False
|
||||
)
|
||||
if self.chain_block_enabled is None:
|
||||
self.chain_block_enabled = _get_env_bool(
|
||||
"MEMORY_CODING_CHAIN_BLOCK_ENABLED", False
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from hy_memory.config import MemoryConfig
|
||||
|
||||
|
||||
def test_chain_block_defaults_disabled(monkeypatch):
|
||||
monkeypatch.delenv("MEMORY_CODING_CHAIN_BLOCK_ENABLED", raising=False)
|
||||
assert MemoryConfig().coding.chain_block_enabled is False
|
||||
|
||||
|
||||
def test_chain_block_env_can_enable(monkeypatch):
|
||||
monkeypatch.setenv("MEMORY_CODING_CHAIN_BLOCK_ENABLED", "true")
|
||||
assert MemoryConfig().coding.chain_block_enabled is True
|
||||
|
||||
|
||||
def test_chain_block_from_dict_can_enable(monkeypatch):
|
||||
monkeypatch.delenv("MEMORY_CODING_CHAIN_BLOCK_ENABLED", raising=False)
|
||||
config = MemoryConfig.from_dict({
|
||||
"coding": {"chain_block_enabled": True},
|
||||
})
|
||||
assert config.coding.chain_block_enabled is True
|
||||
@@ -62,7 +62,21 @@ def _seed_chain(tmpdir, branch="feat/q4", path=_PATH):
|
||||
return H
|
||||
|
||||
|
||||
def _bind_chain(ctx, store):
|
||||
def _enable_chain_block(ctx):
|
||||
"""search 侧 chain_block 默认关;测注入路径时显式 opt-in。"""
|
||||
from hy_memory.config import MemoryConfig
|
||||
|
||||
cfg = getattr(ctx, "_config", None)
|
||||
if cfg is None:
|
||||
cfg = MemoryConfig()
|
||||
ctx._config = cfg
|
||||
cfg.coding.chain_block_enabled = True
|
||||
ctx._chain_block_enabled = HyMemoryClient._chain_block_enabled.__get__(
|
||||
ctx, type(ctx))
|
||||
return ctx
|
||||
|
||||
|
||||
def _bind_chain(ctx, store, *, enable_block=True):
|
||||
"""把链层挂到 search_ctx 这个 SimpleNamespace 上。
|
||||
|
||||
search_ctx 是裸 namespace,不是真 client,所以 _decorate_with_chain 这个
|
||||
@@ -73,6 +87,8 @@ def _bind_chain(ctx, store):
|
||||
ctx._get_chain_store = lambda owner="": store
|
||||
ctx._decorate_with_chain = HyMemoryClient._decorate_with_chain.__get__(
|
||||
ctx, type(ctx))
|
||||
if enable_block:
|
||||
_enable_chain_block(ctx)
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -129,6 +145,17 @@ class TestSearchCodingVariantEndToEnd:
|
||||
assert "第1版" in blk and "第2版" in blk
|
||||
assert "255.0" in blk and "360.0" in blk
|
||||
|
||||
async def test_coding_variant_chain_block_default_off(self, search_ctx, chain_dir):
|
||||
"""默认不注入 chain_block(省 token);库里有链也返回空块。"""
|
||||
await self._seed_fact(search_ctx)
|
||||
# 挂上真链层,但不开启 chain_block_enabled
|
||||
_bind_chain(search_ctx, _seed_chain(chain_dir), enable_block=False)
|
||||
|
||||
resp = await self._search(search_ctx, _STATEMENT, variant="coding")
|
||||
assert len(resp["facts"]) == 1
|
||||
assert resp["chain_block"] == ""
|
||||
assert resp["chain_used"] == []
|
||||
|
||||
async def test_facts_not_deduped(self, search_ctx, chain_dir):
|
||||
"""★ 挂上链的 fact 仍留在 facts 里 —— 刻意不去重。
|
||||
|
||||
|
||||
@@ -326,12 +326,34 @@ class TestChainDecorate:
|
||||
os.environ.pop("HCM_BLOCK_OFF", None)
|
||||
|
||||
|
||||
def _ns_with_chain_block(**kwargs):
|
||||
"""胶水测需要显式 opt-in:默认 chain_block_enabled=False 会在进 store 前短路。"""
|
||||
import types
|
||||
from hy_memory.config import MemoryConfig
|
||||
|
||||
cfg = MemoryConfig()
|
||||
cfg.coding.chain_block_enabled = True
|
||||
ns = types.SimpleNamespace(_config=cfg, **kwargs)
|
||||
ns._chain_block_enabled = HyMemoryClient._chain_block_enabled.__get__(
|
||||
ns, type(ns))
|
||||
return ns
|
||||
|
||||
|
||||
class TestDecorateGlue:
|
||||
"""client._decorate_with_chain —— 胶水层的 fail-safe。"""
|
||||
|
||||
def test_returns_empty_when_chain_unavailable(self):
|
||||
def test_returns_empty_when_disabled_by_default(self):
|
||||
import types
|
||||
ns = types.SimpleNamespace(
|
||||
_chain_store=None, _chain_store_failed=False,
|
||||
_get_chain_store=lambda owner="": object(),
|
||||
)
|
||||
ns._chain_block_enabled = HyMemoryClient._chain_block_enabled.__get__(
|
||||
ns, type(ns))
|
||||
assert HyMemoryClient._decorate_with_chain(ns, [dict(_FACT)]) == ("", [])
|
||||
|
||||
def test_returns_empty_when_chain_unavailable(self):
|
||||
ns = _ns_with_chain_block(
|
||||
_chain_store=None, _chain_store_failed=True,
|
||||
_get_chain_store=lambda owner="": None,
|
||||
)
|
||||
@@ -339,13 +361,12 @@ class TestDecorateGlue:
|
||||
|
||||
def test_store_exception_degrades_not_raises(self):
|
||||
# 这层是增量, 挂了要退回纯向量双层而不是把整次 search 拖失败。
|
||||
import types
|
||||
|
||||
class _Boom:
|
||||
def decorate(self, *a, **k):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
ns = types.SimpleNamespace(_get_chain_store=lambda owner="": _Boom())
|
||||
ns = _ns_with_chain_block(_get_chain_store=lambda owner="": _Boom())
|
||||
assert HyMemoryClient._decorate_with_chain(ns, [dict(_FACT)]) == ("", [])
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user