Skip to content

Commit 4369e28

Browse files
authored
fix(memory): expose reasons for skipped empty-URI operations (#4168)
* 增加空uri的原因 * fix pr bug * 加入Memory diff * fix(memory): preserve semantics for skipped URI reporting
1 parent acfb396 commit 4369e28

19 files changed

Lines changed: 1016 additions & 80 deletions

docs/design/traj-exp-experience-learning-redesign.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ merge 输入/输出日志通过 `tracer.info(..., console=False)` 记录,避
650650
"trace_id": "...",
651651
"extracted_at": "...",
652652
"operations": {...},
653+
"skipped_operations": [...],
653654
"summary": {...}
654655
}
655656
```

docs/en/api/05-sessions.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,10 +1633,19 @@ When long-term memory extraction runs successfully, the commit writes a `memory_
16331633
}
16341634
]
16351635
},
1636+
"skipped_operations": [
1637+
{
1638+
"memory_type": "events",
1639+
"page_id": 101,
1640+
"reason_code": "invalid_ranges",
1641+
"reason": "No valid event range could be resolved"
1642+
}
1643+
],
16361644
"summary": {
16371645
"total_adds": 1,
16381646
"total_updates": 1,
1639-
"total_deletes": 1
1647+
"total_deletes": 1,
1648+
"total_skipped": 1
16401649
}
16411650
}
16421651
```
@@ -1648,11 +1657,13 @@ When long-term memory extraction runs successfully, the commit writes a `memory_
16481657
| `operations.adds` | array | New memories created (`uri`, `memory_type`, `after`) |
16491658
| `operations.updates` | array | Modified memories (`uri`, `memory_type`, `before`, `after`) |
16501659
| `operations.deletes` | array | Deleted memories (`uri`, `memory_type`, `deleted_content`) |
1660+
| `skipped_operations` | array | Intentionally skipped operations and their stable reason codes; these do not represent file changes |
16511661
| `summary.total_adds` | int | Number of new memories |
16521662
| `summary.total_updates` | int | Number of modified memories |
16531663
| `summary.total_deletes` | int | Number of deleted memories |
1664+
| `summary.total_skipped` | int | Number of intentionally skipped operations |
16541665

1655-
An empty `memory_diff.json` (all counts zero) is written when long-term memory extraction runs but produces no memory operations.
1666+
An empty `memory_diff.json` (all counts zero) is written when long-term memory extraction runs but produces no applied or intentionally skipped operations.
16561667

16571668
<a id="built-in-memory-types"></a>
16581669

docs/en/concepts/08-session.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,19 @@ Each `session.commit()` writes a `memory_diff.json` to the archive directory, re
197197
}
198198
]
199199
},
200+
"skipped_operations": [
201+
{
202+
"memory_type": "events",
203+
"page_id": 101,
204+
"reason_code": "invalid_ranges",
205+
"reason": "No valid event range could be resolved"
206+
}
207+
],
200208
"summary": {
201209
"total_adds": 1,
202210
"total_updates": 1,
203-
"total_deletes": 1
211+
"total_deletes": 1,
212+
"total_skipped": 1
204213
}
205214
}
206215
```
@@ -212,9 +221,10 @@ Each `session.commit()` writes a `memory_diff.json` to the archive directory, re
212221
| `operations.adds` | New memories created (no `before`) |
213222
| `operations.updates` | Modified memories (with `before` and `after`) |
214223
| `operations.deletes` | Deleted memories (with `deleted_content`) |
224+
| `skipped_operations` | Intentionally skipped operations and their stable reason codes; these are not file changes |
215225
| `summary` | Counts per operation type |
216226

217-
An empty `memory_diff.json` (all counts zero) is written even when no memory operations occurred.
227+
An empty `memory_diff.json` (all counts zero) is written when no applied or intentionally skipped operations occurred.
218228

219229
## Storage Structure
220230

docs/zh/api/05-sessions.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,10 +1603,19 @@ viking://user/{user_id}/sessions/{session_id}/
16031603
}
16041604
]
16051605
},
1606+
"skipped_operations": [
1607+
{
1608+
"memory_type": "events",
1609+
"page_id": 101,
1610+
"reason_code": "invalid_ranges",
1611+
"reason": "无法解析出有效的事件范围"
1612+
}
1613+
],
16061614
"summary": {
16071615
"total_adds": 1,
16081616
"total_updates": 1,
1609-
"total_deletes": 1
1617+
"total_deletes": 1,
1618+
"total_skipped": 1
16101619
}
16111620
}
16121621
```
@@ -1618,11 +1627,13 @@ viking://user/{user_id}/sessions/{session_id}/
16181627
| `operations.adds` | array | 新增记忆(`uri``memory_type``after`|
16191628
| `operations.updates` | array | 修改记忆(`uri``memory_type``before``after`|
16201629
| `operations.deletes` | array | 删除记忆(`uri``memory_type``deleted_content`|
1630+
| `skipped_operations` | array | 策略性跳过的操作及稳定原因码;不代表文件变更 |
16211631
| `summary.total_adds` | int | 新增记忆数 |
16221632
| `summary.total_updates` | int | 修改记忆数 |
16231633
| `summary.total_deletes` | int | 删除记忆数 |
1634+
| `summary.total_skipped` | int | 策略性跳过的操作数 |
16241635

1625-
如果长记忆抽取已运行但没有产生记忆操作,也会写入空结构的 `memory_diff.json`(所有计数为零)。
1636+
如果长记忆抽取已运行但没有产生实际变更或策略性跳过,也会写入空结构的 `memory_diff.json`(所有计数为零)。
16261637

16271638
<a id="内置记忆类型"></a>
16281639

docs/zh/concepts/08-session.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,19 @@ LLM 去重决策 → candidate(skip/create/none) + item(merge/delete)
197197
}
198198
]
199199
},
200+
"skipped_operations": [
201+
{
202+
"memory_type": "events",
203+
"page_id": 101,
204+
"reason_code": "invalid_ranges",
205+
"reason": "无法解析出有效的事件范围"
206+
}
207+
],
200208
"summary": {
201209
"total_adds": 1,
202210
"total_updates": 1,
203-
"total_deletes": 1
211+
"total_deletes": 1,
212+
"total_skipped": 1
204213
}
205214
}
206215
```
@@ -212,9 +221,10 @@ LLM 去重决策 → candidate(skip/create/none) + item(merge/delete)
212221
| `operations.adds` | 新增的记忆(无 `before`|
213222
| `operations.updates` | 修改的记忆(含 `before``after`|
214223
| `operations.deletes` | 删除的记忆(含 `deleted_content`|
224+
| `skipped_operations` | 策略性跳过的操作及稳定原因码;不代表文件变更 |
215225
| `summary` | 各操作类型的计数 |
216226

217-
即使没有记忆操作,也会写入空结构的 `memory_diff.json`(所有计数为零)。
227+
如果没有实际变更或策略性跳过,也会写入空结构的 `memory_diff.json`(所有计数为零)。
218228

219229
## 存储结构
220230

openviking/session/compressor_v3.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@ async def _build_memory_diff(
346346
adds=adds,
347347
updates=updates,
348348
deletes=deletes,
349+
skipped_operations=_serialize_skipped_operations(
350+
getattr(result, "skipped_operations", [])
351+
),
349352
)
350353

351354
@tracer(ignore_result=True)
@@ -363,6 +366,7 @@ async def extract_long_term_memories(
363366
allow_self_memory: bool = True,
364367
allowed_peer_ids: Optional[set[str]] = None,
365368
event_search_tags: Optional[List[str]] = None,
369+
peer_memory_enabled: bool = True,
366370
):
367371
if not agent_evolution_enabled:
368372
effective_types = (
@@ -400,6 +404,7 @@ async def extract_long_term_memories(
400404
archive_uri=archive_uri,
401405
allowed_memory_types=allowed_memory_types,
402406
allow_self_memory=allow_self_memory,
407+
peer_memory_enabled=peer_memory_enabled,
403408
allowed_peer_ids=allowed_peer_ids,
404409
event_search_tags=event_search_tags,
405410
)
@@ -601,6 +606,7 @@ async def _extract_user_memories(
601606
archive_uri: Optional[str] = None,
602607
allowed_memory_types: Optional[set[str]] = None,
603608
allow_self_memory: bool = True,
609+
peer_memory_enabled: bool = True,
604610
allowed_peer_ids: Optional[set[str]] = None,
605611
event_search_tags: Optional[List[str]] = None,
606612
) -> "_V3ExtractionResult":
@@ -642,6 +648,7 @@ async def _extract_user_memories(
642648
allowed_memory_types=allowed_memory_types,
643649
allow_self=allow_self_memory,
644650
allowed_peer_ids=allowed_peer_ids,
651+
peer_memory_enabled=peer_memory_enabled,
645652
)
646653
isolation_handler.prepare_messages()
647654
context_provider._isolation_handler = isolation_handler
@@ -682,6 +689,7 @@ async def _extract_user_memories(
682689
"allowed_memory_types": allowed_memory_types,
683690
"allow_self": allow_self_memory,
684691
"allowed_peer_ids": allowed_peer_ids,
692+
"peer_memory_enabled": peer_memory_enabled,
685693
},
686694
metadata={
687695
"source_extraction_id": extraction_id,
@@ -719,6 +727,9 @@ async def _extract_user_memories(
719727
cases=canonical_cases,
720728
memory_diff=memory_diff,
721729
case_uri_by_name=_case_uri_by_name(canonical_cases, patch_operations, result),
730+
skipped_operations=_serialize_skipped_operations(
731+
getattr(result, "skipped_operations", [])
732+
),
722733
)
723734

724735
def _session_skill_extraction_enabled(self) -> bool:
@@ -1223,6 +1234,7 @@ class _V3ExtractionResult:
12231234
cases: list[Case] = field(default_factory=list)
12241235
memory_diff: dict[str, Any] | None = None
12251236
case_uri_by_name: dict[str, str] = field(default_factory=dict)
1237+
skipped_operations: list[dict[str, Any]] = field(default_factory=list)
12261238

12271239

12281240
@dataclass(slots=True)
@@ -2041,6 +2053,22 @@ def _same_memory_file(before: Optional[MemoryFile], after: Optional[MemoryFile])
20412053
)
20422054

20432055

2056+
def _serialize_skipped_operations(items: Any) -> list[dict[str, Any]]:
2057+
serialized: list[dict[str, Any]] = []
2058+
for item in list(items or []):
2059+
if isinstance(item, dict):
2060+
payload = dict(item)
2061+
else:
2062+
model_dump = getattr(item, "model_dump", None)
2063+
if not callable(model_dump):
2064+
continue
2065+
payload = model_dump(mode="json", exclude_none=True)
2066+
if isinstance(payload, dict):
2067+
payload.pop("source", None)
2068+
serialized.append(payload)
2069+
return serialized
2070+
2071+
20442072
def _v3_extraction_response(
20452073
*,
20462074
contexts: list[Context],
@@ -2051,10 +2079,9 @@ def _v3_extraction_response(
20512079
20522080
Historically ``extract_long_term_memories`` returned ``list[Context]`` and
20532081
a number of direct callers still index/compare the return value as a list.
2054-
Commit orchestration now also understands the execution-memory style
2055-
``{"contexts": ..., "session_skills": ...}`` shape so it can count
2056-
session skills. Preserve the old list shape unless there are actual
2057-
session skills to report.
2082+
Commit orchestration also understands a structured response for session
2083+
skills. Preserve the old list shape unless there are actual session skills
2084+
to report.
20582085
"""
20592086
skill_dicts: list[dict[str, Any]] = []
20602087
seen: set[str] = set()
@@ -2075,7 +2102,9 @@ def _make_memory_diff(
20752102
adds: list[dict[str, Any]],
20762103
updates: list[dict[str, Any]],
20772104
deletes: list[dict[str, Any]],
2105+
skipped_operations: Optional[list[dict[str, Any]]] = None,
20782106
) -> dict[str, Any]:
2107+
skipped = list(skipped_operations or [])
20792108
return {
20802109
"archive_uri": archive_uri,
20812110
"trace_id": tracer.get_trace_id() or None,
@@ -2085,10 +2114,12 @@ def _make_memory_diff(
20852114
"updates": list(updates),
20862115
"deletes": list(deletes),
20872116
},
2117+
"skipped_operations": skipped,
20882118
"summary": {
20892119
"total_adds": len(adds),
20902120
"total_updates": len(updates),
20912121
"total_deletes": len(deletes),
2122+
"total_skipped": len(skipped),
20922123
},
20932124
}
20942125

@@ -2101,12 +2132,16 @@ def _merge_memory_diffs(
21012132
adds: list[dict[str, Any]] = []
21022133
updates: list[dict[str, Any]] = []
21032134
deletes: list[dict[str, Any]] = []
2135+
skipped_operations: list[dict[str, Any]] = []
21042136
trace_id = tracer.get_trace_id() or None
21052137
for diff in diffs:
21062138
if not isinstance(diff, dict):
21072139
continue
21082140
if trace_id is None and diff.get("trace_id"):
21092141
trace_id = str(diff.get("trace_id"))
2142+
skipped_operations.extend(
2143+
item for item in diff.get("skipped_operations", []) if isinstance(item, dict)
2144+
)
21102145
operations = diff.get("operations")
21112146
if not isinstance(operations, dict):
21122147
continue
@@ -2118,6 +2153,7 @@ def _merge_memory_diffs(
21182153
adds=adds,
21192154
updates=updates,
21202155
deletes=deletes,
2156+
skipped_operations=skipped_operations,
21212157
)
21222158
merged["trace_id"] = trace_id
21232159
return merged
@@ -2130,7 +2166,8 @@ def _memory_diff_has_changes(diff: Any) -> bool:
21302166
if not isinstance(summary, dict):
21312167
return False
21322168
return any(
2133-
int(summary.get(key) or 0) > 0 for key in ("total_adds", "total_updates", "total_deletes")
2169+
int(summary.get(key) or 0) > 0
2170+
for key in ("total_adds", "total_updates", "total_deletes", "total_skipped")
21342171
)
21352172

21362173

openviking/session/memory/dataclass.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,39 @@ class MemoryOperationSource(BaseModel):
169169
extracted_at: Optional[str] = None
170170

171171

172+
class MemoryOperationSkipCode(str, Enum):
173+
"""Stable reason codes for intentionally skipped memory operations."""
174+
175+
MEMORY_TYPE_FILTERED = "memory_type_filtered"
176+
SELF_MEMORY_DISABLED = "self_memory_disabled"
177+
PEER_MEMORY_DISABLED = "peer_memory_disabled"
178+
INVALID_PEER_ID = "invalid_peer_id"
179+
PEER_NOT_ALLOWED = "peer_not_allowed"
180+
INVALID_RANGES = "invalid_ranges"
181+
AMBIGUOUS_TARGET = "ambiguous_target"
182+
NO_WRITABLE_TARGET = "no_writable_target"
183+
184+
185+
class MemoryOperationSkip(BaseModel):
186+
"""Internal policy/validation decision explaining why no URI was produced."""
187+
188+
reason_code: MemoryOperationSkipCode
189+
reason: str
190+
191+
192+
class SkippedMemoryOperation(BaseModel):
193+
"""Structured, task-visible record for one intentionally skipped operation."""
194+
195+
memory_type: str
196+
page_id: Optional[int] = None
197+
uri: Optional[str] = None
198+
reason_code: MemoryOperationSkipCode
199+
reason: str
200+
# Source is used only to scope shared streaming-batch results back to the
201+
# submitting commit. It must never be serialized into the public task result.
202+
source: Optional[MemoryOperationSource] = Field(default=None, exclude=True)
203+
204+
172205
# ============================================================================
173206
# Memory Field and Schema Definitions
174207
# ============================================================================
@@ -298,6 +331,9 @@ class ResolvedOperation(BaseModel):
298331
uris: List[str]
299332
page_id: Optional[int] = None # Temporary page_id for link resolution (not persisted)
300333
source: Optional[MemoryOperationSource] = None
334+
# Runtime-only resolution decision. It is deliberately excluded from model
335+
# serialization so it cannot enter later LLM merge prompts or memory files.
336+
resolution_skip: Optional[MemoryOperationSkip] = Field(default=None, exclude=True)
301337
# Custom scalar tags (already normalized as "key=value") to attach to this
302338
# operation's memories in the vector index. None means "no tags"; used by
303339
# event-memory auto-tagging. Not persisted in the memory file content.

0 commit comments

Comments
 (0)