agentcache-core 0.9.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentcache/__init__.py +29 -0
- agentcache/app.py +312 -0
- agentcache/cli.py +346 -0
- agentcache/connect.py +724 -0
- agentcache/core/__init__.py +19 -0
- agentcache/core/audit_log.py +104 -0
- agentcache/core/config.py +51 -0
- agentcache/core/context_builder.py +209 -0
- agentcache/core/graph.py +120 -0
- agentcache/core/image_store.py +111 -0
- agentcache/core/infer.py +142 -0
- agentcache/core/kv_scopes.py +86 -0
- agentcache/core/lessons.py +242 -0
- agentcache/core/llm.py +596 -0
- agentcache/core/memory_store.py +207 -0
- agentcache/core/observation_store.py +576 -0
- agentcache/core/privacy.py +34 -0
- agentcache/core/project_profile.py +625 -0
- agentcache/core/search_service.py +444 -0
- agentcache/core/session_store.py +382 -0
- agentcache/core/slots.py +504 -0
- agentcache/db.py +359 -0
- agentcache/import_data.py +86 -0
- agentcache/legacy.py +94 -0
- agentcache/mcp_stdio.py +141 -0
- agentcache/py.typed +0 -0
- agentcache/replay_import.py +680 -0
- agentcache/routes/__init__.py +29 -0
- agentcache/routes/_deps.py +46 -0
- agentcache/routes/auth.py +68 -0
- agentcache/routes/graph.py +81 -0
- agentcache/routes/health.py +149 -0
- agentcache/routes/mcp.py +614 -0
- agentcache/routes/memories.py +116 -0
- agentcache/routes/migration.py +32 -0
- agentcache/routes/observations.py +253 -0
- agentcache/routes/search.py +80 -0
- agentcache/search.py +935 -0
- agentcache/storage/__init__.py +29 -0
- agentcache/storage/images.py +102 -0
- agentcache/storage/paths.py +116 -0
- agentcache/storage/scopes.py +9 -0
- agentcache/viewer/favicon.svg +1 -0
- agentcache/viewer/index.html +4235 -0
- agentcache/viewer_helpers.py +61 -0
- agentcache/workers.py +192 -0
- agentcache_core-0.9.9.dist-info/METADATA +194 -0
- agentcache_core-0.9.9.dist-info/RECORD +52 -0
- agentcache_core-0.9.9.dist-info/WHEEL +5 -0
- agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
- agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
- agentcache_core-0.9.9.dist-info/top_level.txt +1 -0
agentcache/core/slots.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""Memory slots — pinned context windows with read/append/replace/delete."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..db import StateKV
|
|
8
|
+
from .audit_log import safe_audit
|
|
9
|
+
from .config import commit_if_enabled, get_agent_id
|
|
10
|
+
from .kv_scopes import KV
|
|
11
|
+
from .privacy import strip_private_data
|
|
12
|
+
|
|
13
|
+
DEFAULT_SLOTS = [
|
|
14
|
+
{
|
|
15
|
+
"label": "persona",
|
|
16
|
+
"content": "",
|
|
17
|
+
"sizeLimit": 1000,
|
|
18
|
+
"description": "How the agent should see itself: role, tone, behavioural guidelines.",
|
|
19
|
+
"pinned": True,
|
|
20
|
+
"readOnly": False,
|
|
21
|
+
"scope": "global",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"label": "user_preferences",
|
|
25
|
+
"content": "",
|
|
26
|
+
"sizeLimit": 2000,
|
|
27
|
+
"description": "Coding style, tool preferences, naming conventions, and other habits the user wants preserved across sessions.",
|
|
28
|
+
"pinned": True,
|
|
29
|
+
"readOnly": False,
|
|
30
|
+
"scope": "global",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"label": "tool_guidelines",
|
|
34
|
+
"content": "",
|
|
35
|
+
"sizeLimit": 1500,
|
|
36
|
+
"description": "Rules the agent should follow when picking or sequencing tools (e.g. prefer X over Y, never run Z without confirmation).",
|
|
37
|
+
"pinned": True,
|
|
38
|
+
"readOnly": False,
|
|
39
|
+
"scope": "global",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"label": "project_context",
|
|
43
|
+
"content": "",
|
|
44
|
+
"sizeLimit": 3000,
|
|
45
|
+
"description": "Architecture decisions, codebase conventions, build/test commands, and cross-cutting constraints for the current project.",
|
|
46
|
+
"pinned": True,
|
|
47
|
+
"readOnly": False,
|
|
48
|
+
"scope": "project",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"label": "guidance",
|
|
52
|
+
"content": "",
|
|
53
|
+
"sizeLimit": 1500,
|
|
54
|
+
"description": "Active advice for the next session: what to focus on, what to avoid, open risks.",
|
|
55
|
+
"pinned": True,
|
|
56
|
+
"readOnly": False,
|
|
57
|
+
"scope": "project",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"label": "pending_items",
|
|
61
|
+
"content": "",
|
|
62
|
+
"sizeLimit": 2000,
|
|
63
|
+
"description": "Unfinished work, explicit TODOs, and promises made but not yet delivered.",
|
|
64
|
+
"pinned": True,
|
|
65
|
+
"readOnly": False,
|
|
66
|
+
"scope": "project",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"label": "session_patterns",
|
|
70
|
+
"content": "",
|
|
71
|
+
"sizeLimit": 1500,
|
|
72
|
+
"description": "Recurring behaviours and common struggles observed across recent sessions.",
|
|
73
|
+
"pinned": False,
|
|
74
|
+
"readOnly": False,
|
|
75
|
+
"scope": "project",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"label": "self_notes",
|
|
79
|
+
"content": "",
|
|
80
|
+
"sizeLimit": 1500,
|
|
81
|
+
"description": "Free-form notes the agent keeps for itself: hypotheses, dead ends, things to revisit.",
|
|
82
|
+
"pinned": False,
|
|
83
|
+
"readOnly": False,
|
|
84
|
+
"scope": "project",
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_current_project(kv: StateKV) -> Optional[str]:
|
|
90
|
+
try:
|
|
91
|
+
sessions = kv.list(KV.sessions)
|
|
92
|
+
if not sessions:
|
|
93
|
+
return None
|
|
94
|
+
active_sessions = [s for s in sessions if s.get("status") == "active"]
|
|
95
|
+
if active_sessions:
|
|
96
|
+
active_sessions.sort(key=lambda s: s.get("updatedAt", ""), reverse=True)
|
|
97
|
+
return active_sessions[0].get("project")
|
|
98
|
+
sessions.sort(key=lambda s: s.get("updatedAt", ""), reverse=True)
|
|
99
|
+
return sessions[0].get("project")
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def project_slots_scope(kv: StateKV, project: Optional[str] = None) -> str:
|
|
105
|
+
if not project:
|
|
106
|
+
project = get_current_project(kv)
|
|
107
|
+
if not project:
|
|
108
|
+
return KV.slots
|
|
109
|
+
return f"mem:slots:{project}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def seed_defaults(kv: StateKV) -> None:
|
|
113
|
+
now = (
|
|
114
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
115
|
+
)
|
|
116
|
+
for tmpl in DEFAULT_SLOTS:
|
|
117
|
+
scope = tmpl["scope"]
|
|
118
|
+
target = KV.globalSlots if scope == "global" else KV.slots
|
|
119
|
+
existing = kv.get(target, tmpl["label"])
|
|
120
|
+
if existing:
|
|
121
|
+
continue
|
|
122
|
+
slot = dict(tmpl)
|
|
123
|
+
slot["createdAt"] = now
|
|
124
|
+
slot["updatedAt"] = now
|
|
125
|
+
kv.set(target, tmpl["label"], slot)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def list_pinned_slots(
|
|
129
|
+
kv: StateKV, project: Optional[str] = None
|
|
130
|
+
) -> List[Dict[str, Any]]:
|
|
131
|
+
p_slots = kv.list(project_slots_scope(kv, project))
|
|
132
|
+
g_slots = kv.list(KV.globalSlots)
|
|
133
|
+
merged = {}
|
|
134
|
+
for s in g_slots:
|
|
135
|
+
merged[s["label"]] = s
|
|
136
|
+
for s in p_slots:
|
|
137
|
+
merged[s["label"]] = s
|
|
138
|
+
pinned = [
|
|
139
|
+
s for s in merged.values() if s.get("pinned") and s.get("content", "").strip()
|
|
140
|
+
]
|
|
141
|
+
pinned.sort(key=lambda s: s["label"])
|
|
142
|
+
return pinned
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_pinned_context(slots: List[Dict[str, Any]]) -> str:
|
|
146
|
+
if not slots:
|
|
147
|
+
return ""
|
|
148
|
+
lines = ["# agentcache pinned slots", ""]
|
|
149
|
+
for s in slots:
|
|
150
|
+
lines.append(f"## {s['label']}")
|
|
151
|
+
lines.append(s["content"].strip())
|
|
152
|
+
lines.append("")
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def slot_list(kv: StateKV, project: Optional[str] = None) -> Dict[str, Any]:
|
|
157
|
+
p_slots = kv.list(project_slots_scope(kv, project))
|
|
158
|
+
g_slots = kv.list(KV.globalSlots)
|
|
159
|
+
merged = {}
|
|
160
|
+
for s in g_slots:
|
|
161
|
+
merged[s["label"]] = s
|
|
162
|
+
for s in p_slots:
|
|
163
|
+
merged[s["label"]] = s
|
|
164
|
+
slots = sorted(list(merged.values()), key=lambda s: s["label"])
|
|
165
|
+
return {"success": True, "slots": slots}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def slot_get(kv: StateKV, label: str, project: Optional[str] = None) -> Dict[str, Any]:
|
|
169
|
+
p_scope = project_slots_scope(kv, project)
|
|
170
|
+
project_s = kv.get(p_scope, label)
|
|
171
|
+
if project_s:
|
|
172
|
+
return {"success": True, "slot": project_s, "scope": "project"}
|
|
173
|
+
global_s = kv.get(KV.globalSlots, label)
|
|
174
|
+
if global_s:
|
|
175
|
+
return {"success": True, "slot": global_s, "scope": "global"}
|
|
176
|
+
return {"success": False, "error": "slot not found"}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def slot_create(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
180
|
+
label = data.get("label")
|
|
181
|
+
if not label or not re.match(r"^[a-z][a-z0-9_]*$", label):
|
|
182
|
+
return {
|
|
183
|
+
"success": False,
|
|
184
|
+
"error": "label required (lowercase, starts with letter, [a-z0-9_])",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
scope = data.get("scope") or "project"
|
|
188
|
+
if scope not in ("project", "global"):
|
|
189
|
+
return {"success": False, "error": "scope must be 'project' or 'global'"}
|
|
190
|
+
|
|
191
|
+
limit = data.get("sizeLimit") or 2000
|
|
192
|
+
if not isinstance(limit, int) or limit < 1 or limit > 20000:
|
|
193
|
+
return {
|
|
194
|
+
"success": False,
|
|
195
|
+
"error": "sizeLimit must be an integer between 1 and 20000",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
content = strip_private_data(data.get("content") or "")
|
|
199
|
+
if len(content) > limit:
|
|
200
|
+
return {
|
|
201
|
+
"success": False,
|
|
202
|
+
"error": f"content exceeds sizeLimit ({len(content)} > {limit})",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
description = data.get("description") or ""
|
|
206
|
+
pinned = data.get("pinned", True)
|
|
207
|
+
project = data.get("project")
|
|
208
|
+
|
|
209
|
+
target_kv = (
|
|
210
|
+
KV.globalSlots if scope == "global" else project_slots_scope(kv, project)
|
|
211
|
+
)
|
|
212
|
+
existing = kv.get(target_kv, label)
|
|
213
|
+
if existing:
|
|
214
|
+
return {"success": False, "error": f"slot already exists in {scope} scope"}
|
|
215
|
+
|
|
216
|
+
now = (
|
|
217
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
218
|
+
)
|
|
219
|
+
slot = {
|
|
220
|
+
"label": label,
|
|
221
|
+
"content": content,
|
|
222
|
+
"sizeLimit": limit,
|
|
223
|
+
"description": description,
|
|
224
|
+
"pinned": pinned,
|
|
225
|
+
"readOnly": False,
|
|
226
|
+
"scope": scope,
|
|
227
|
+
"createdAt": now,
|
|
228
|
+
"updatedAt": now,
|
|
229
|
+
}
|
|
230
|
+
kv.set(target_kv, label, slot)
|
|
231
|
+
safe_audit(
|
|
232
|
+
kv,
|
|
233
|
+
"slot_create",
|
|
234
|
+
"mem::slot-create",
|
|
235
|
+
[label],
|
|
236
|
+
{"scope": scope, "sizeLimit": limit, "pinned": pinned},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
agent_id = data.get("agentId") or get_agent_id()
|
|
240
|
+
commit_if_enabled(kv, f"Create slot: {label}", agent_id)
|
|
241
|
+
|
|
242
|
+
return {"success": True, "slot": slot}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def slot_append(
|
|
246
|
+
kv: StateKV,
|
|
247
|
+
label: str,
|
|
248
|
+
text: str,
|
|
249
|
+
agent_id: Optional[str] = None,
|
|
250
|
+
project: Optional[str] = None,
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
res = slot_get(kv, label, project)
|
|
253
|
+
if not res.get("success"):
|
|
254
|
+
return {"success": False, "error": "slot not found"}
|
|
255
|
+
|
|
256
|
+
slot = res["slot"]
|
|
257
|
+
scope = res["scope"]
|
|
258
|
+
target_kv = (
|
|
259
|
+
KV.globalSlots if scope == "global" else project_slots_scope(kv, project)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if slot.get("readOnly"):
|
|
263
|
+
return {"success": False, "error": "slot is read-only"}
|
|
264
|
+
|
|
265
|
+
content = slot.get("content") or ""
|
|
266
|
+
sep = "\n" if content and not content.endswith("\n") else ""
|
|
267
|
+
next_content = content + sep + strip_private_data(text)
|
|
268
|
+
|
|
269
|
+
limit = slot.get("sizeLimit") or 2000
|
|
270
|
+
if len(next_content) > limit:
|
|
271
|
+
return {
|
|
272
|
+
"success": False,
|
|
273
|
+
"error": f"append would exceed sizeLimit ({len(next_content)} > {limit})",
|
|
274
|
+
"currentSize": len(content),
|
|
275
|
+
"sizeLimit": limit,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
slot["content"] = next_content
|
|
279
|
+
slot["updatedAt"] = (
|
|
280
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
281
|
+
)
|
|
282
|
+
kv.set(target_kv, label, slot)
|
|
283
|
+
|
|
284
|
+
safe_audit(
|
|
285
|
+
kv,
|
|
286
|
+
"slot_append",
|
|
287
|
+
"mem::slot-append",
|
|
288
|
+
[label],
|
|
289
|
+
{"scope": scope, "added": len(text), "total": len(next_content)},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
commit_if_enabled(kv, f"Append slot: {label}", agent_id or get_agent_id())
|
|
293
|
+
|
|
294
|
+
return {"success": True, "slot": slot, "size": len(next_content)}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def slot_replace(
|
|
298
|
+
kv: StateKV,
|
|
299
|
+
label: str,
|
|
300
|
+
content: str,
|
|
301
|
+
agent_id: Optional[str] = None,
|
|
302
|
+
project: Optional[str] = None,
|
|
303
|
+
) -> Dict[str, Any]:
|
|
304
|
+
res = slot_get(kv, label, project)
|
|
305
|
+
if not res.get("success"):
|
|
306
|
+
return {"success": False, "error": "slot not found"}
|
|
307
|
+
|
|
308
|
+
slot = res["slot"]
|
|
309
|
+
scope = res["scope"]
|
|
310
|
+
target_kv = (
|
|
311
|
+
KV.globalSlots if scope == "global" else project_slots_scope(kv, project)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if slot.get("readOnly"):
|
|
315
|
+
return {"success": False, "error": "slot is read-only"}
|
|
316
|
+
|
|
317
|
+
content = strip_private_data(content)
|
|
318
|
+
limit = slot.get("sizeLimit") or 2000
|
|
319
|
+
if len(content) > limit:
|
|
320
|
+
return {
|
|
321
|
+
"success": False,
|
|
322
|
+
"error": f"content exceeds sizeLimit ({len(content)} > {limit})",
|
|
323
|
+
"sizeLimit": limit,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
before_len = len(slot.get("content") or "")
|
|
327
|
+
slot["content"] = content
|
|
328
|
+
slot["updatedAt"] = (
|
|
329
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
330
|
+
)
|
|
331
|
+
kv.set(target_kv, label, slot)
|
|
332
|
+
|
|
333
|
+
safe_audit(
|
|
334
|
+
kv,
|
|
335
|
+
"slot_replace",
|
|
336
|
+
"mem::slot-replace",
|
|
337
|
+
[label],
|
|
338
|
+
{"scope": scope, "before": before_len, "after": len(content)},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
commit_if_enabled(kv, f"Replace slot: {label}", agent_id or get_agent_id())
|
|
342
|
+
|
|
343
|
+
return {"success": True, "slot": slot, "size": len(content)}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def slot_delete(
|
|
347
|
+
kv: StateKV,
|
|
348
|
+
label: str,
|
|
349
|
+
agent_id: Optional[str] = None,
|
|
350
|
+
project: Optional[str] = None,
|
|
351
|
+
) -> Dict[str, Any]:
|
|
352
|
+
res = slot_get(kv, label, project)
|
|
353
|
+
if not res.get("success"):
|
|
354
|
+
return {"success": False, "error": "slot not found"}
|
|
355
|
+
|
|
356
|
+
slot = res["slot"]
|
|
357
|
+
scope = res["scope"]
|
|
358
|
+
target_kv = (
|
|
359
|
+
KV.globalSlots if scope == "global" else project_slots_scope(kv, project)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if slot.get("readOnly"):
|
|
363
|
+
return {"success": False, "error": "slot is read-only"}
|
|
364
|
+
|
|
365
|
+
kv.delete(target_kv, label)
|
|
366
|
+
safe_audit(
|
|
367
|
+
kv,
|
|
368
|
+
"slot_delete",
|
|
369
|
+
"mem::slot-delete",
|
|
370
|
+
[label],
|
|
371
|
+
{"scope": scope, "size": len(slot.get("content") or "")},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
commit_if_enabled(kv, f"Delete slot: {label}", agent_id or get_agent_id())
|
|
375
|
+
|
|
376
|
+
return {"success": True}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def slot_reflect(kv: StateKV, session_id: str, max_obs: int = 50) -> Dict[str, Any]:
|
|
380
|
+
session = kv.get(KV.sessions, session_id)
|
|
381
|
+
project = session.get("project") if session else None
|
|
382
|
+
|
|
383
|
+
observations = kv.list(KV.observations(session_id))
|
|
384
|
+
if not observations:
|
|
385
|
+
return {"success": True, "applied": 0, "reason": "no observations for session"}
|
|
386
|
+
|
|
387
|
+
recent = sorted(observations, key=lambda x: x.get("timestamp", ""), reverse=True)[
|
|
388
|
+
:max_obs
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
pending_lines = []
|
|
392
|
+
pattern_counts = {}
|
|
393
|
+
files = set()
|
|
394
|
+
|
|
395
|
+
for obs in recent:
|
|
396
|
+
title = (obs.get("title") or "").lower()
|
|
397
|
+
narrative = (obs.get("narrative") or "").lower()
|
|
398
|
+
if "todo" in narrative or "todo" in title:
|
|
399
|
+
pending_lines.append(f"- {obs.get('title') or obs['id']}")
|
|
400
|
+
if obs.get("type") == "error":
|
|
401
|
+
pattern_counts["errors"] = pattern_counts.get("errors", 0) + 1
|
|
402
|
+
if obs.get("type") == "command_run":
|
|
403
|
+
pattern_counts["commands"] = pattern_counts.get("commands", 0) + 1
|
|
404
|
+
for f in obs.get("files") or []:
|
|
405
|
+
files.add(f)
|
|
406
|
+
|
|
407
|
+
applied = 0
|
|
408
|
+
now = (
|
|
409
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if pending_lines:
|
|
413
|
+
res = slot_get(kv, "pending_items", project)
|
|
414
|
+
if res.get("success"):
|
|
415
|
+
slot = res["slot"]
|
|
416
|
+
scope = res["scope"]
|
|
417
|
+
target_kv = (
|
|
418
|
+
KV.globalSlots
|
|
419
|
+
if scope == "global"
|
|
420
|
+
else project_slots_scope(kv, project)
|
|
421
|
+
)
|
|
422
|
+
already = set((slot.get("content") or "").split("\n"))
|
|
423
|
+
fresh = [line for line in pending_lines if line not in already]
|
|
424
|
+
if fresh:
|
|
425
|
+
sep = (
|
|
426
|
+
"\n"
|
|
427
|
+
if slot.get("content") and not slot["content"].endswith("\n")
|
|
428
|
+
else ""
|
|
429
|
+
)
|
|
430
|
+
next_content = (slot.get("content") or "") + sep + "\n".join(fresh)
|
|
431
|
+
limit = slot.get("sizeLimit") or 2000
|
|
432
|
+
if len(next_content) > limit:
|
|
433
|
+
next_content = next_content[-limit:]
|
|
434
|
+
slot["content"] = next_content
|
|
435
|
+
slot["updatedAt"] = now
|
|
436
|
+
kv.set(target_kv, "pending_items", slot)
|
|
437
|
+
applied += 1
|
|
438
|
+
|
|
439
|
+
if pattern_counts:
|
|
440
|
+
res = slot_get(kv, "session_patterns", project)
|
|
441
|
+
if res.get("success"):
|
|
442
|
+
slot = res["slot"]
|
|
443
|
+
scope = res["scope"]
|
|
444
|
+
target_kv = (
|
|
445
|
+
KV.globalSlots
|
|
446
|
+
if scope == "global"
|
|
447
|
+
else project_slots_scope(kv, project)
|
|
448
|
+
)
|
|
449
|
+
summary = [f"last reflection: {now}"]
|
|
450
|
+
for k, v in pattern_counts.items():
|
|
451
|
+
summary.append(f"- {k}: {v} in last {len(recent)} observations")
|
|
452
|
+
next_content = "\n".join(summary)
|
|
453
|
+
limit = slot.get("sizeLimit") or 2000
|
|
454
|
+
if len(next_content) > limit:
|
|
455
|
+
next_content = next_content[:limit]
|
|
456
|
+
slot["content"] = next_content
|
|
457
|
+
slot["updatedAt"] = now
|
|
458
|
+
kv.set(target_kv, "session_patterns", slot)
|
|
459
|
+
applied += 1
|
|
460
|
+
|
|
461
|
+
if files:
|
|
462
|
+
res = slot_get(kv, "project_context", project)
|
|
463
|
+
if res.get("success"):
|
|
464
|
+
slot = res["slot"]
|
|
465
|
+
scope = res["scope"]
|
|
466
|
+
target_kv = (
|
|
467
|
+
KV.globalSlots
|
|
468
|
+
if scope == "global"
|
|
469
|
+
else project_slots_scope(kv, project)
|
|
470
|
+
)
|
|
471
|
+
already = slot.get("content") or ""
|
|
472
|
+
fresh = [f for f in files if f not in already][:20]
|
|
473
|
+
if fresh:
|
|
474
|
+
header_line = "Files touched in recent sessions:" if not already else ""
|
|
475
|
+
sep = "\n" if already and not already.endswith("\n") else ""
|
|
476
|
+
lines = [already]
|
|
477
|
+
if header_line:
|
|
478
|
+
lines.append(header_line)
|
|
479
|
+
for f in fresh:
|
|
480
|
+
lines.append(f"- {f}")
|
|
481
|
+
next_content = sep.join([line for line in lines if line])
|
|
482
|
+
limit = slot.get("sizeLimit") or 2000
|
|
483
|
+
if len(next_content) > limit:
|
|
484
|
+
next_content = next_content[-limit:]
|
|
485
|
+
slot["content"] = next_content
|
|
486
|
+
slot["updatedAt"] = now
|
|
487
|
+
kv.set(target_kv, "project_context", slot)
|
|
488
|
+
applied += 1
|
|
489
|
+
|
|
490
|
+
if applied > 0:
|
|
491
|
+
safe_audit(
|
|
492
|
+
kv,
|
|
493
|
+
"slot_reflect",
|
|
494
|
+
"mem::slot-reflect",
|
|
495
|
+
[session_id],
|
|
496
|
+
{"observationCount": len(recent), "slotsUpdated": applied},
|
|
497
|
+
)
|
|
498
|
+
commit_if_enabled(
|
|
499
|
+
kv,
|
|
500
|
+
f"Slot reflect: updated {applied} slots in session {session_id[:8]}",
|
|
501
|
+
"system",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return {"success": True, "applied": applied, "observationsReviewed": len(recent)}
|