Skip to content

Commit bd8de37

Browse files
committed
v0.116.6 — replace perf_stats with OTel tracing spans
- Remove keep/perf_stats.py (custom profiling layer, 164 lines) - Replace all perf.timer() / perf.record() calls with OTel spans via get_tracer() - Add span attributes (item_id, source, found, result_count, etc.) for observability - Wrap get_context, _find_direct, _get_direct, query_fts, query_embedding in spans - Flow runtime uses OTel spans per action instead of perf.timer - Remove CLAUDE.md (replaced by AGENTS.md)
1 parent f445785 commit bd8de37

20 files changed

Lines changed: 706 additions & 965 deletions

CLAUDE.md

Lines changed: 0 additions & 51 deletions
This file was deleted.

SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: keep
3-
version: 0.116.5
3+
version: 0.116.6
44
description: Reflective Memory
55
homepage: https://github.com/keepnotes-ai/keep
66
runtime: python:3.12-slim

bench/put_perf.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,6 @@ def main():
229229
print(f"{'TOTAL':<25} {total_put:7.1f}ms {total_proc:9.1f}ms {total_all:9.1f}ms ({n} items)", file=sys.stderr)
230230
print(f"{'PER ITEM (avg)':<25} {total_put/n:7.1f}ms {total_proc/n:9.1f}ms {total_all/n:9.1f}ms", file=sys.stderr)
231231

232-
# Per-action perf stats
233-
try:
234-
from keep.perf_stats import perf
235-
lines = perf.format_summary()
236-
if lines:
237-
print(f"\nPerf stats:\n{lines}", file=sys.stderr)
238-
except Exception:
239-
pass
240-
241232
# Output JSON for programmatic comparison
242233
import json
243234
print(json.dumps(results, indent=2))

claude-code-plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "keep",
3-
"version": "0.116.5",
3+
"version": "0.116.6",
44
"description": "Reflective memory with semantic search for AI agents",
55
"author": {
66
"name": "keepnotes-ai",

keep/_context_resolution.py

Lines changed: 133 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -179,142 +179,147 @@ def get_context(
179179
include_parts: Whether to include parts manifest
180180
include_versions: Whether to include version navigation
181181
"""
182-
from .perf_stats import perf
183-
import time
184-
_ctx_t0 = time.monotonic()
182+
with get_tracer("keeper").start_as_current_span(
183+
"get_context",
184+
attributes={"item_id": id},
185+
) as span:
186+
# Ensure state docs are in the store for the read flow.
187+
self._ensure_sysdocs()
188+
189+
# Parse @V{N} version ref from ID (explicit version= takes precedence)
190+
base_id, id_version = parse_version_ref(id)
191+
if id_version is not None and version is None:
192+
version = id_version
193+
id = base_id
194+
id = normalize_id(id)
195+
resolved = self.resolve_version_offset(id, version)
196+
if resolved is None:
197+
span.set_attribute("found", False)
198+
return None
199+
offset = resolved
200+
span.set_attribute("viewing_offset", offset)
201+
if offset > 0:
202+
item = self.get_version(id, offset)
203+
else:
204+
item = self.get(id)
205+
if item is None:
206+
span.set_attribute("found", False)
207+
return None
208+
209+
# System docs are authored reference/configuration content.
210+
# Rendering them should show the document itself only; surrounding
211+
# context assembly (similar/meta/parts/edges/version nav) adds noise
212+
# and unnecessary work.
213+
if is_system_id(item.id):
214+
span.set_attribute("system_id", True)
215+
return ItemContext(
216+
item=item,
217+
viewing_offset=offset,
218+
similar=[],
219+
meta={},
220+
edges={},
221+
parts=[],
222+
prev=[],
223+
next=[],
224+
)
185225

186-
# Ensure state docs are in the store for the read flow.
187-
self._ensure_sysdocs()
226+
# Version navigation
227+
prev_refs: list[VersionRef] = []
228+
next_refs: list[VersionRef] = []
229+
if include_versions:
230+
if offset == 0:
231+
nav = self.get_version_nav(id, None, limit=versions_limit)
232+
for i, v in enumerate(nav.get("prev", [])[:versions_limit]):
233+
prev_refs.append(VersionRef(
234+
offset=i + 1,
235+
date=local_date(v.tags.get("_created") or v.created_at or ""),
236+
summary=v.summary,
237+
))
238+
else:
239+
# Keep navigation in user-visible offset space.
240+
# This avoids mixing offset (V{N}) with internal version numbers.
241+
older = self.get_version(id, offset + 1)
242+
if older is not None:
243+
prev_refs.append(VersionRef(
244+
offset=offset + 1,
245+
date=local_date(older.tags.get("_created", "")),
246+
summary=older.summary,
247+
))
248+
if offset > 1:
249+
newer = self.get_version(id, offset - 1)
250+
if newer is not None:
251+
next_refs.append(VersionRef(
252+
offset=offset - 1,
253+
date=local_date(newer.tags.get("_created", "")),
254+
summary=newer.summary,
255+
))
256+
257+
# Data gathering via state-doc flow (similar, parts, meta).
258+
# Edge resolution stays inline — it requires direct database
259+
# queries (inverse edges, explicit edge-tag lookup) that the
260+
# generic traverse action doesn't support.
261+
similar_refs: list[SimilarRef] = []
262+
meta_refs: dict[str, list[MetaRef]] = {}
263+
part_refs: list[PartRef] = []
264+
edge_refs: dict[str, list[EdgeRef]] = {}
188265

189-
# Parse @V{N} version ref from ID (explicit version= takes precedence)
190-
base_id, id_version = parse_version_ref(id)
191-
if id_version is not None and version is None:
192-
version = id_version
193-
id = base_id
194-
id = normalize_id(id)
195-
resolved = self.resolve_version_offset(id, version)
196-
if resolved is None:
197-
return None
198-
offset = resolved
199-
if offset > 0:
200-
item = self.get_version(id, offset)
201-
else:
202-
item = self.get(id)
203-
if item is None:
204-
return None
266+
if offset == 0:
267+
if include_similar or include_meta or include_parts:
268+
with get_tracer("keeper").start_as_current_span(
269+
"get_context.read_flow",
270+
attributes={"item_id": id},
271+
):
272+
flow_result = self._run_read_flow(
273+
"get",
274+
{
275+
"item_id": id,
276+
"similar_limit": similar_limit if include_similar else 0,
277+
"meta_limit": meta_limit if include_meta else 0,
278+
"parts_limit": parts_limit if include_parts else 0,
279+
"edges_limit": edges_limit,
280+
"versions_limit": versions_limit if include_versions else 0,
281+
},
282+
)
283+
if flow_result.status == "done":
284+
bindings = flow_result.bindings
285+
if include_similar:
286+
similar_refs = self._map_flow_similar(bindings.get("similar", {}))
287+
similar_refs = similar_refs[:similar_limit]
288+
if include_meta:
289+
meta_refs = self._map_flow_meta(bindings.get("meta", {}))
290+
# Cap each meta section to meta_limit
291+
for key in list(meta_refs.keys()):
292+
meta_refs[key] = meta_refs[key][:meta_limit]
293+
if include_parts:
294+
part_refs = self._map_flow_parts(bindings.get("parts", {}))
295+
part_refs = part_refs[:parts_limit]
296+
edge_refs = self._map_flow_edges(bindings.get("edges", {}))
297+
# Cap each edge predicate to edges_limit
298+
for key in list(edge_refs.keys()):
299+
edge_refs[key] = edge_refs[key][:edges_limit]
300+
else:
301+
logger.warning(
302+
"get-context flow returned %s for %r: %s",
303+
flow_result.status, id, flow_result.data,
304+
)
305+
# Fallback: inline edge resolution when flow fails
306+
edge_refs = self._resolve_edge_refs(item, id)
205307

206-
# System docs are authored reference/configuration content.
207-
# Rendering them should show the document itself only; surrounding
208-
# context assembly (similar/meta/parts/edges/version nav) adds noise
209-
# and unnecessary work.
210-
if is_system_id(item.id):
211-
perf.record("get_context", "total", time.monotonic() - _ctx_t0,
212-
context_id=id)
308+
span.set_attribute("similar_count", len(similar_refs))
309+
span.set_attribute("meta_count", sum(len(v) for v in meta_refs.values()))
310+
span.set_attribute("edge_count", sum(len(v) for v in edge_refs.values()))
311+
span.set_attribute("part_count", len(part_refs))
213312
return ItemContext(
214313
item=item,
215314
viewing_offset=offset,
216-
similar=[],
217-
meta={},
218-
edges={},
219-
parts=[],
220-
prev=[],
221-
next=[],
315+
similar=similar_refs,
316+
meta=meta_refs,
317+
edges=edge_refs,
318+
parts=part_refs,
319+
prev=prev_refs,
320+
next=next_refs,
222321
)
223322

224-
# Version navigation
225-
prev_refs: list[VersionRef] = []
226-
next_refs: list[VersionRef] = []
227-
if include_versions:
228-
if offset == 0:
229-
nav = self.get_version_nav(id, None, limit=versions_limit)
230-
for i, v in enumerate(nav.get("prev", [])[:versions_limit]):
231-
prev_refs.append(VersionRef(
232-
offset=i + 1,
233-
date=local_date(v.tags.get("_created") or v.created_at or ""),
234-
summary=v.summary,
235-
))
236-
else:
237-
# Keep navigation in user-visible offset space.
238-
# This avoids mixing offset (V{N}) with internal version numbers.
239-
older = self.get_version(id, offset + 1)
240-
if older is not None:
241-
prev_refs.append(VersionRef(
242-
offset=offset + 1,
243-
date=local_date(older.tags.get("_created", "")),
244-
summary=older.summary,
245-
))
246-
if offset > 1:
247-
newer = self.get_version(id, offset - 1)
248-
if newer is not None:
249-
next_refs.append(VersionRef(
250-
offset=offset - 1,
251-
date=local_date(newer.tags.get("_created", "")),
252-
summary=newer.summary,
253-
))
254-
255-
# Data gathering via state-doc flow (similar, parts, meta).
256-
# Edge resolution stays inline — it requires direct database
257-
# queries (inverse edges, explicit edge-tag lookup) that the
258-
# generic traverse action doesn't support.
259-
similar_refs: list[SimilarRef] = []
260-
meta_refs: dict[str, list[MetaRef]] = {}
261-
part_refs: list[PartRef] = []
262-
edge_refs: dict[str, list[EdgeRef]] = {}
263-
264-
if offset == 0:
265-
if include_similar or include_meta or include_parts:
266-
from .tracing import get_tracer as _get_tracer
267-
with perf.timer("get_context", "read_flow", context_id=id), \
268-
_get_tracer("keeper").start_as_current_span("get_context", attributes={"item_id": id}):
269-
flow_result = self._run_read_flow(
270-
"get",
271-
{
272-
"item_id": id,
273-
"similar_limit": similar_limit if include_similar else 0,
274-
"meta_limit": meta_limit if include_meta else 0,
275-
"parts_limit": parts_limit if include_parts else 0,
276-
"edges_limit": edges_limit,
277-
"versions_limit": versions_limit if include_versions else 0,
278-
},
279-
)
280-
if flow_result.status == "done":
281-
bindings = flow_result.bindings
282-
if include_similar:
283-
similar_refs = self._map_flow_similar(bindings.get("similar", {}))
284-
similar_refs = similar_refs[:similar_limit]
285-
if include_meta:
286-
meta_refs = self._map_flow_meta(bindings.get("meta", {}))
287-
# Cap each meta section to meta_limit
288-
for key in list(meta_refs.keys()):
289-
meta_refs[key] = meta_refs[key][:meta_limit]
290-
if include_parts:
291-
part_refs = self._map_flow_parts(bindings.get("parts", {}))
292-
part_refs = part_refs[:parts_limit]
293-
edge_refs = self._map_flow_edges(bindings.get("edges", {}))
294-
# Cap each edge predicate to edges_limit
295-
for key in list(edge_refs.keys()):
296-
edge_refs[key] = edge_refs[key][:edges_limit]
297-
else:
298-
logger.warning(
299-
"get-context flow returned %s for %r: %s",
300-
flow_result.status, id, flow_result.data,
301-
)
302-
# Fallback: inline edge resolution when flow fails
303-
edge_refs = self._resolve_edge_refs(item, id)
304-
305-
perf.record("get_context", "total", time.monotonic() - _ctx_t0,
306-
context_id=id)
307-
return ItemContext(
308-
item=item,
309-
viewing_offset=offset,
310-
similar=similar_refs,
311-
meta=meta_refs,
312-
edges=edge_refs,
313-
parts=part_refs,
314-
prev=prev_refs,
315-
next=next_refs,
316-
)
317-
318323
def resolve_version_offset(self, id: str, selector: int | None) -> Optional[int]:
319324
"""Resolve a public version selector to a concrete offset.
320325

0 commit comments

Comments
 (0)