@simbimbo/memory-ocmemog 0.1.17 → 0.1.19
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.
- package/CHANGELOG.md +14 -0
- package/ocmemog/doctor.py +8 -1
- package/ocmemog/runtime/memory/conversation_state.py +101 -6
- package/ocmemog/runtime/memory/memory_links.py +6 -0
- package/ocmemog/runtime/memory/promote.py +73 -1
- package/ocmemog/runtime/memory/retrieval.py +275 -4
- package/ocmemog/runtime/memory/store.py +11 -0
- package/ocmemog/runtime/memory/unresolved_state.py +91 -26
- package/ocmemog/sidecar/app.py +170 -21
- package/package.json +1 -1
- package/scripts/ocmemog-continuity-benchmark.py +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.19 — 2026-03-29
|
|
6
|
+
|
|
7
|
+
Hydrate/resume hardening, unresolved-state main-DB consolidation, and retrieval/rehydration source-of-truth completion.
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
- eliminated expensive hydrate hot-path scans by adding release-critical indexes for linked-memory and unresolved-state lookups
|
|
11
|
+
- moved unresolved state into the main SQLite memory DB with compatibility import from legacy `unresolved_state.db`
|
|
12
|
+
- removed inline self-heal from `/conversation/hydrate` and kept hydrate read-mostly/fast by default
|
|
13
|
+
- disabled predictive brief generation on hydrate by default and added long-session hydrate guardrails for oversized scopes
|
|
14
|
+
- surfaced hydrate budget/warning metadata for long sessions and added doctor visibility for stale legacy unresolved-state DB residue
|
|
15
|
+
- completed the stranded retrieval/rehydration hardening lane: preserved canonical operator-facing `selected_because` semantics while retaining richer ranking signals, and validated canonical source-of-truth retrieval behavior
|
|
16
|
+
- added regression coverage for resume-latency query plans, long-session guardrails, unresolved-state main-DB migration, and updated retrieval explanation semantics
|
|
17
|
+
- validation passed across the combined retrieval + hydrate + migration + doctor suite, and the canonical `./scripts/ocmemog-release-check.sh` gate passed
|
|
18
|
+
|
|
5
19
|
## 0.1.17 — 2026-03-26
|
|
6
20
|
|
|
7
21
|
Promotion/governance observability, anti-cruft hardening, queue/runtime summary parity, and release validation recovery.
|
package/ocmemog/doctor.py
CHANGED
|
@@ -279,6 +279,8 @@ def _run_sqlite_schema(_: None) -> CheckResult:
|
|
|
279
279
|
tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
|
|
280
280
|
missing = sorted(required - tables)
|
|
281
281
|
quick = str(conn.execute("PRAGMA quick_check(1)").fetchone()[0] or "unknown")
|
|
282
|
+
legacy_unresolved_state_path = state_store.data_dir() / "unresolved_state.db"
|
|
283
|
+
legacy_unresolved_state_exists = legacy_unresolved_state_path.exists()
|
|
282
284
|
for table in sorted(required):
|
|
283
285
|
if table in missing:
|
|
284
286
|
continue
|
|
@@ -319,6 +321,7 @@ def _run_sqlite_schema(_: None) -> CheckResult:
|
|
|
319
321
|
"schema_version_expected": store.SCHEMA_VERSION,
|
|
320
322
|
"schema_versions": version_map,
|
|
321
323
|
"schema_version_issues": version_issues,
|
|
324
|
+
"legacy_unresolved_state_db": str(legacy_unresolved_state_path) if legacy_unresolved_state_exists else None,
|
|
322
325
|
}
|
|
323
326
|
else:
|
|
324
327
|
details = {
|
|
@@ -329,6 +332,7 @@ def _run_sqlite_schema(_: None) -> CheckResult:
|
|
|
329
332
|
"schema_version_expected": store.SCHEMA_VERSION,
|
|
330
333
|
"schema_versions": version_map,
|
|
331
334
|
"schema_version_issues": version_issues,
|
|
335
|
+
"legacy_unresolved_state_db": str(legacy_unresolved_state_path) if legacy_unresolved_state_exists else None,
|
|
332
336
|
}
|
|
333
337
|
if version_issues:
|
|
334
338
|
details["schema_version_issues"] = version_issues
|
|
@@ -361,11 +365,14 @@ def _run_sqlite_schema(_: None) -> CheckResult:
|
|
|
361
365
|
message="Schema metadata includes unexpected versions or schema column issues.",
|
|
362
366
|
details=details,
|
|
363
367
|
)
|
|
368
|
+
message = "SQLite schema and DB open state are healthy."
|
|
369
|
+
if legacy_unresolved_state_exists:
|
|
370
|
+
message = "SQLite schema and DB open state are healthy, but legacy unresolved_state.db still exists and should be cleaned up after migration verification."
|
|
364
371
|
return CheckResult(
|
|
365
372
|
key="sqlite/schema-access",
|
|
366
373
|
label="sqlite and schema",
|
|
367
374
|
status="ok",
|
|
368
|
-
message=
|
|
375
|
+
message=message,
|
|
369
376
|
details=details,
|
|
370
377
|
)
|
|
371
378
|
|
|
@@ -29,6 +29,10 @@ _SHORT_REPLY_NORMALIZED = {
|
|
|
29
29
|
"okay",
|
|
30
30
|
"do it",
|
|
31
31
|
"go ahead",
|
|
32
|
+
"proceed",
|
|
33
|
+
"get on it",
|
|
34
|
+
"lets go",
|
|
35
|
+
"let us go",
|
|
32
36
|
"sounds good",
|
|
33
37
|
"lets do it",
|
|
34
38
|
"let us do it",
|
|
@@ -395,7 +399,10 @@ def _infer_short_reply_resolution(
|
|
|
395
399
|
if not referent_content:
|
|
396
400
|
return None
|
|
397
401
|
decision = "decline" if normalized in _NEGATIVE_SHORT_REPLY_NORMALIZED else "confirm"
|
|
398
|
-
|
|
402
|
+
if decision == "confirm":
|
|
403
|
+
effective_summary = f"Continue approved step: {referent_content}"
|
|
404
|
+
else:
|
|
405
|
+
effective_summary = f"Decline proposed step: {referent_content}"
|
|
399
406
|
return {
|
|
400
407
|
"kind": "short_reply_reference",
|
|
401
408
|
"decision": decision,
|
|
@@ -447,10 +454,17 @@ def _enrich_turn_metadata(
|
|
|
447
454
|
enriched["resolution"] = resolution
|
|
448
455
|
if reply_target is None:
|
|
449
456
|
reply_target = _get_turn_by_id(resolution.get("resolved_turn_id"))
|
|
457
|
+
lane_pivot = role == "user" and _looks_like_lane_pivot(content)
|
|
450
458
|
if reply_target:
|
|
451
459
|
reply_meta = _turn_meta(reply_target)
|
|
452
|
-
|
|
453
|
-
|
|
460
|
+
if lane_pivot:
|
|
461
|
+
branch_root_turn_id = int(reply_target.get("id") or 0) or None
|
|
462
|
+
branch_id = f"pivot:{branch_root_turn_id or (message_id or 'unknown')}"
|
|
463
|
+
enriched["lane_pivot"] = True
|
|
464
|
+
enriched["lane_pivot_from_turn_id"] = int(reply_target.get("id") or 0) or None
|
|
465
|
+
else:
|
|
466
|
+
branch_root_turn_id = int(reply_meta.get("branch_root_turn_id") or reply_target.get("id") or 0) or None
|
|
467
|
+
branch_id = str(reply_meta.get("branch_id") or f"branch:{branch_root_turn_id or reply_target.get('id')}")
|
|
454
468
|
enriched["reply_to_turn_id"] = int(reply_target.get("id") or 0) or None
|
|
455
469
|
enriched["reply_to_reference"] = reply_target.get("reference")
|
|
456
470
|
if reply_target.get("message_id"):
|
|
@@ -458,7 +472,7 @@ def _enrich_turn_metadata(
|
|
|
458
472
|
if branch_root_turn_id:
|
|
459
473
|
enriched["branch_root_turn_id"] = branch_root_turn_id
|
|
460
474
|
enriched["branch_id"] = branch_id
|
|
461
|
-
enriched["branch_depth"] = int(reply_meta.get("branch_depth") or 0) + 1
|
|
475
|
+
enriched["branch_depth"] = int(reply_meta.get("branch_depth") or 0) + 1 if not lane_pivot else 1
|
|
462
476
|
elif message_id and "branch_id" not in enriched:
|
|
463
477
|
enriched["branch_id"] = f"message:{message_id}"
|
|
464
478
|
enriched["branch_depth"] = 0
|
|
@@ -478,6 +492,30 @@ def _effective_turn_content(turn: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
|
478
492
|
return content or None
|
|
479
493
|
|
|
480
494
|
|
|
495
|
+
def _looks_like_lane_pivot(text: str) -> bool:
|
|
496
|
+
lowered = _normalize_conversation_text(text).lower()
|
|
497
|
+
if not lowered:
|
|
498
|
+
return False
|
|
499
|
+
return any(
|
|
500
|
+
token in lowered
|
|
501
|
+
for token in (
|
|
502
|
+
"before we continue",
|
|
503
|
+
"let's pause",
|
|
504
|
+
"lets pause",
|
|
505
|
+
"back to",
|
|
506
|
+
"move back",
|
|
507
|
+
"return to",
|
|
508
|
+
"failing us",
|
|
509
|
+
"before we move back",
|
|
510
|
+
"pause for one second",
|
|
511
|
+
"task list for",
|
|
512
|
+
"can you show me",
|
|
513
|
+
"what did we just fix",
|
|
514
|
+
"what is the task list",
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
481
519
|
def _reply_chain_for_turn(turn: Optional[Dict[str, Any]], turns: Sequence[Dict[str, Any]], *, limit: int = 6) -> List[Dict[str, Any]]:
|
|
482
520
|
if not turn:
|
|
483
521
|
return []
|
|
@@ -494,7 +532,21 @@ def _reply_chain_for_turn(turn: Optional[Dict[str, Any]], turns: Sequence[Dict[s
|
|
|
494
532
|
break
|
|
495
533
|
seen.add(reply_to_turn_id)
|
|
496
534
|
current = lookup.get(reply_to_turn_id) or _get_turn_by_id(reply_to_turn_id)
|
|
497
|
-
|
|
535
|
+
chain = list(reversed(chain))
|
|
536
|
+
|
|
537
|
+
# Trim temporary side-answer prefixes when the later cluster clearly returns to the foreground lane.
|
|
538
|
+
if len(chain) >= 4:
|
|
539
|
+
for idx in range(len(chain) - 3):
|
|
540
|
+
first = chain[idx]
|
|
541
|
+
second = chain[idx + 1]
|
|
542
|
+
third = chain[idx + 2]
|
|
543
|
+
first_text = _normalize_conversation_text(str(first.get("content") or "").strip()).lower()
|
|
544
|
+
second_text = _normalize_conversation_text(str(second.get("content") or "").strip()).lower()
|
|
545
|
+
third_text = _normalize_conversation_text(str(third.get("content") or "").strip()).lower()
|
|
546
|
+
if first.get("role") == "assistant" and second.get("role") == "user" and third.get("role") == "assistant":
|
|
547
|
+
if any(token in first_text for token in ("recent", "repo work", "list", "includes", "task list", "show me")) and any(token in second_text for token in ("great", "proceed", "task list", "back to", "continue")):
|
|
548
|
+
return chain[idx + 1 :]
|
|
549
|
+
return chain
|
|
498
550
|
|
|
499
551
|
|
|
500
552
|
def _active_branch_payload(turns: Sequence[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
@@ -512,13 +564,56 @@ def _active_branch_payload(turns: Sequence[Dict[str, Any]]) -> Optional[Dict[str
|
|
|
512
564
|
]
|
|
513
565
|
if not branch_turns:
|
|
514
566
|
branch_turns = [latest_turn]
|
|
567
|
+
|
|
568
|
+
reply_chain = _reply_chain_for_turn(latest_turn, turns_list, limit=8)
|
|
569
|
+
reply_chain_ids = [int(item.get("id") or 0) for item in reply_chain if int(item.get("id") or 0) > 0]
|
|
570
|
+
latest_turn_id = int(latest_turn.get("id") or 0)
|
|
571
|
+
|
|
572
|
+
# When a later user/assistant cluster explicitly pivots or returns to a lane,
|
|
573
|
+
# prefer the suffix of the branch starting at the most recent non-reply turn that
|
|
574
|
+
# follows the earlier adjacent lane. This keeps fluid topic switches from dragging
|
|
575
|
+
# previous foreground work into the active branch payload.
|
|
576
|
+
suffix_start_id = reply_chain_ids[0] if reply_chain_ids else latest_turn_id
|
|
577
|
+
for turn in reversed(branch_turns):
|
|
578
|
+
turn_id = int(turn.get("id") or 0)
|
|
579
|
+
if turn_id <= 0 or turn_id >= latest_turn_id:
|
|
580
|
+
continue
|
|
581
|
+
meta = _turn_meta(turn)
|
|
582
|
+
if meta.get("reply_to_turn_id"):
|
|
583
|
+
continue
|
|
584
|
+
if turn.get("role") != "user":
|
|
585
|
+
continue
|
|
586
|
+
turn_text = _normalize_conversation_text(str(turn.get("content") or "").strip()).lower()
|
|
587
|
+
if any(token in turn_text for token in ("before we continue", "let's pause", "move back", "back to", "failing us", "return to", "resume", "task list for", "can you show me", "what did we just fix", "what is the task list")):
|
|
588
|
+
suffix_start_id = turn_id
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
# If the latest reply chain is the user explicitly returning after a temporary side answer,
|
|
592
|
+
# do not keep the side-answer assistant turn as foreground branch context.
|
|
593
|
+
if len(reply_chain_ids) >= 2:
|
|
594
|
+
first_reply_id = reply_chain_ids[0]
|
|
595
|
+
first_reply_turn = next((turn for turn in branch_turns if int(turn.get("id") or 0) == first_reply_id), None)
|
|
596
|
+
if first_reply_turn and first_reply_turn.get("role") == "assistant":
|
|
597
|
+
first_reply_text = _normalize_conversation_text(str(first_reply_turn.get("content") or "").strip()).lower()
|
|
598
|
+
if any(token in first_reply_text for token in ("recent", "repo work", "list", "includes", "task list", "show me")):
|
|
599
|
+
user_followup_id = reply_chain_ids[1] if len(reply_chain_ids) > 1 else None
|
|
600
|
+
if user_followup_id:
|
|
601
|
+
suffix_start_id = max(suffix_start_id, user_followup_id)
|
|
602
|
+
|
|
603
|
+
filtered_branch_turns = [
|
|
604
|
+
turn for turn in branch_turns
|
|
605
|
+
if int(turn.get("id") or 0) >= suffix_start_id or int(turn.get("id") or 0) in reply_chain_ids
|
|
606
|
+
]
|
|
607
|
+
if filtered_branch_turns:
|
|
608
|
+
branch_turns = filtered_branch_turns
|
|
609
|
+
|
|
515
610
|
return {
|
|
516
611
|
"branch_id": branch_id,
|
|
517
612
|
"root_turn_id": root_turn_id or latest_turn.get("id"),
|
|
518
613
|
"latest_turn": _turn_anchor(latest_turn),
|
|
519
614
|
"turn_ids": [int(turn.get("id") or 0) for turn in branch_turns],
|
|
520
615
|
"turns": [_turn_anchor(turn) for turn in branch_turns[-8:]],
|
|
521
|
-
"reply_chain":
|
|
616
|
+
"reply_chain": reply_chain,
|
|
522
617
|
}
|
|
523
618
|
|
|
524
619
|
|
|
@@ -46,6 +46,12 @@ def _ensure_table(conn) -> None:
|
|
|
46
46
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_links_unique ON memory_links(source_reference, link_type, target_reference)"
|
|
47
47
|
)
|
|
48
48
|
conn.commit()
|
|
49
|
+
conn.execute(
|
|
50
|
+
"CREATE INDEX IF NOT EXISTS idx_memory_links_target_created_source ON memory_links(target_reference, created_at DESC, source_reference DESC)"
|
|
51
|
+
)
|
|
52
|
+
conn.execute(
|
|
53
|
+
"CREATE INDEX IF NOT EXISTS idx_memory_links_source_created_target ON memory_links(source_reference, created_at DESC, target_reference)"
|
|
54
|
+
)
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
def add_memory_link(source_reference: str, link_type: str, target_reference: str) -> None:
|
|
@@ -49,7 +49,26 @@ def _should_promote(confidence: float, threshold: float | None = None) -> bool:
|
|
|
49
49
|
|
|
50
50
|
def _destination_table(summary: str) -> str:
|
|
51
51
|
lowered = summary.lower()
|
|
52
|
-
|
|
52
|
+
procedural_markers = (
|
|
53
|
+
"runbook",
|
|
54
|
+
"procedure",
|
|
55
|
+
"steps",
|
|
56
|
+
"checklist",
|
|
57
|
+
"how to",
|
|
58
|
+
"how-do-i",
|
|
59
|
+
"upgrade",
|
|
60
|
+
"recover",
|
|
61
|
+
"recovery",
|
|
62
|
+
"rollback",
|
|
63
|
+
"restart",
|
|
64
|
+
"validate",
|
|
65
|
+
"verification",
|
|
66
|
+
"diagnose",
|
|
67
|
+
"troubleshoot",
|
|
68
|
+
"fix by",
|
|
69
|
+
"safe way",
|
|
70
|
+
)
|
|
71
|
+
if any(marker in lowered for marker in procedural_markers):
|
|
53
72
|
return "runbooks"
|
|
54
73
|
if "lesson" in lowered or "postmortem" in lowered or "learned" in lowered:
|
|
55
74
|
return "lessons"
|
|
@@ -64,6 +83,46 @@ def _normalized_text(text: str) -> str:
|
|
|
64
83
|
return " ".join((text or "").strip().lower().split())
|
|
65
84
|
|
|
66
85
|
|
|
86
|
+
def _looks_like_changelog_or_release_notes(text: str) -> bool:
|
|
87
|
+
lowered = _normalized_text(text)
|
|
88
|
+
if not lowered:
|
|
89
|
+
return False
|
|
90
|
+
changelog_markers = (
|
|
91
|
+
"thanks @",
|
|
92
|
+
"(#",
|
|
93
|
+
"ghsa-",
|
|
94
|
+
"release notes",
|
|
95
|
+
"changelog",
|
|
96
|
+
"breaking change",
|
|
97
|
+
"bootstrap:",
|
|
98
|
+
"security/",
|
|
99
|
+
"agents/",
|
|
100
|
+
"telegram:",
|
|
101
|
+
"discord/",
|
|
102
|
+
"slack/",
|
|
103
|
+
"providers/",
|
|
104
|
+
"install/",
|
|
105
|
+
"docker/",
|
|
106
|
+
)
|
|
107
|
+
bulletish = lowered.count(" - ") >= 2 or lowered.startswith("-")
|
|
108
|
+
return bulletish and any(marker in lowered for marker in changelog_markers)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _looks_like_docs_index_link_list(text: str) -> bool:
|
|
112
|
+
lowered = _normalized_text(text)
|
|
113
|
+
if not lowered:
|
|
114
|
+
return False
|
|
115
|
+
markers = (
|
|
116
|
+
"start with the docs index",
|
|
117
|
+
"architecture overview",
|
|
118
|
+
"full configuration reference",
|
|
119
|
+
"run the gateway by the book",
|
|
120
|
+
"learn how the control ui/web surfaces work",
|
|
121
|
+
"https://docs.openclaw.ai",
|
|
122
|
+
)
|
|
123
|
+
return lowered.count("https://docs.openclaw.ai") >= 2 and any(marker in lowered for marker in markers)
|
|
124
|
+
|
|
125
|
+
|
|
67
126
|
def _is_redundant_generic_candidate(summary_text: str) -> bool:
|
|
68
127
|
normalized = _normalized_text(summary_text)
|
|
69
128
|
if not normalized:
|
|
@@ -83,6 +142,8 @@ def _is_redundant_generic_candidate(summary_text: str) -> bool:
|
|
|
83
142
|
|
|
84
143
|
|
|
85
144
|
def _should_reject_as_cruft(*, confidence: float, threshold: float, destination: str, summary_text: str) -> bool:
|
|
145
|
+
if destination == "runbooks" and (_looks_like_changelog_or_release_notes(summary_text) or _looks_like_docs_index_link_list(summary_text)):
|
|
146
|
+
return True
|
|
86
147
|
if destination != "knowledge" or confidence >= threshold:
|
|
87
148
|
return False
|
|
88
149
|
return bool(_normalized_text(summary_text))
|
|
@@ -189,6 +250,17 @@ def promote_candidate(candidate: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
189
250
|
candidate_metadata["candidate_id"] = candidate_id
|
|
190
251
|
candidate_metadata["derived_from_candidate_id"] = candidate_id
|
|
191
252
|
candidate_metadata["derived_via"] = "promotion"
|
|
253
|
+
# Preserve rehydration-critical anchors whenever available.
|
|
254
|
+
transcript_anchor = candidate_metadata.get("transcript_anchor")
|
|
255
|
+
if isinstance(transcript_anchor, dict):
|
|
256
|
+
path_value = transcript_anchor.get("path")
|
|
257
|
+
if path_value and not candidate_metadata.get("source_path"):
|
|
258
|
+
candidate_metadata["source_path"] = path_value
|
|
259
|
+
if not candidate_metadata.get("source_type"):
|
|
260
|
+
candidate_metadata["source_type"] = "transcript"
|
|
261
|
+
source_refs = candidate_metadata.get("source_references")
|
|
262
|
+
if isinstance(source_refs, list) and source_refs and not candidate_metadata.get("source_type"):
|
|
263
|
+
candidate_metadata["source_type"] = "derived"
|
|
192
264
|
|
|
193
265
|
conn = store.connect()
|
|
194
266
|
promotion_id = None
|
|
@@ -100,11 +100,267 @@ def _recency_score(timestamp: str | None) -> float:
|
|
|
100
100
|
|
|
101
101
|
MEMORY_BUCKETS: Tuple[str, ...] = tuple(store.MEMORY_TABLES)
|
|
102
102
|
|
|
103
|
+
_PROCEDURAL_QUERY_MARKERS: Tuple[str, ...] = (
|
|
104
|
+
"how do i",
|
|
105
|
+
"how to",
|
|
106
|
+
"procedure",
|
|
107
|
+
"steps",
|
|
108
|
+
"checklist",
|
|
109
|
+
"safely",
|
|
110
|
+
"safe way",
|
|
111
|
+
"upgrade",
|
|
112
|
+
"recover",
|
|
113
|
+
"recovery",
|
|
114
|
+
"rollback",
|
|
115
|
+
"restart",
|
|
116
|
+
"validate",
|
|
117
|
+
"verification",
|
|
118
|
+
"diagnose",
|
|
119
|
+
"troubleshoot",
|
|
120
|
+
)
|
|
121
|
+
|
|
103
122
|
|
|
104
123
|
def _empty_results() -> Dict[str, List[Dict[str, Any]]]:
|
|
105
124
|
return {bucket: [] for bucket in MEMORY_BUCKETS}
|
|
106
125
|
|
|
107
126
|
|
|
127
|
+
def _query_intent(prompt: str) -> str:
|
|
128
|
+
lowered = str(prompt or "").strip().lower()
|
|
129
|
+
if not lowered:
|
|
130
|
+
return "generic"
|
|
131
|
+
if any(marker in lowered for marker in _PROCEDURAL_QUERY_MARKERS):
|
|
132
|
+
return "procedural"
|
|
133
|
+
return "generic"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _bucket_intent_bonus(bucket: str, prompt: str, metadata: Dict[str, Any]) -> float:
|
|
137
|
+
intent = _query_intent(prompt)
|
|
138
|
+
if intent == "procedural":
|
|
139
|
+
if bucket == "runbooks":
|
|
140
|
+
return 0.28
|
|
141
|
+
if bucket == "knowledge":
|
|
142
|
+
# Some procedures are still stored as knowledge; keep them competitive.
|
|
143
|
+
lowered = str(metadata).lower()
|
|
144
|
+
if any(marker in lowered for marker in _PROCEDURAL_QUERY_MARKERS):
|
|
145
|
+
return 0.12
|
|
146
|
+
if bucket in {"tasks", "reflections"}:
|
|
147
|
+
return -0.12
|
|
148
|
+
return 0.0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _source_authority_bonus(bucket: str, metadata: Dict[str, Any], content: str, prompt: str) -> float:
|
|
152
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
153
|
+
origin_source = str(prov.get("origin_source") or metadata.get("source_path") or "").lower()
|
|
154
|
+
source_label = str(metadata.get("source_label") or "").lower()
|
|
155
|
+
text = f"{metadata} {content}".lower()
|
|
156
|
+
qtype = _query_type(prompt)
|
|
157
|
+
bonus = 0.0
|
|
158
|
+
is_policy_doc = "policy established" in text or "anti-rediscovery" in text or "authoritative recall memory" in text
|
|
159
|
+
is_audit_doc = "scorecard" in text or "results —" in text or "results -" in text or "audit" in text
|
|
160
|
+
is_continuity_doc = "current-work-continuity-source-of-truth" in origin_source or str(metadata.get("source_label") or "").lower() == "canonical-current-work-continuity"
|
|
161
|
+
is_user_pref_doc = "preferred development model" in text or "messaging auth safety" in text or "default model" in text
|
|
162
|
+
is_infra_doc = "truth plane" in text or "librenms" in text or "10.5.56.130" in text or "192.168.1.x" in text or "10.x" in text
|
|
163
|
+
is_transcript = origin_source == "transcript" or "memory/transcripts/" in text
|
|
164
|
+
is_canonical_file = "/docs/" in origin_source or str(metadata.get("canonical") or "").lower() == "true"
|
|
165
|
+
|
|
166
|
+
if bucket in {"runbooks", "knowledge", "directives"}:
|
|
167
|
+
if "docs/ops/" in text or "runtime-authority-map.md" in text:
|
|
168
|
+
bonus += 0.22
|
|
169
|
+
if "docs/runbooks/" in text or "openclaw-upgrade-validation-runbook.md" in text or "canonical-openclaw-upgrade-runbook" in source_label or "/docs/runbooks/" in origin_source:
|
|
170
|
+
bonus += 0.55
|
|
171
|
+
if "workspace/memory/" in text or "transcript:" in text:
|
|
172
|
+
bonus -= 0.05
|
|
173
|
+
|
|
174
|
+
if qtype != "policy" and is_policy_doc:
|
|
175
|
+
bonus -= 0.45
|
|
176
|
+
if qtype != "policy" and is_audit_doc:
|
|
177
|
+
bonus -= 0.22
|
|
178
|
+
|
|
179
|
+
if qtype == "infra_fact":
|
|
180
|
+
if is_infra_doc:
|
|
181
|
+
bonus += 0.38
|
|
182
|
+
if not is_infra_doc and (is_policy_doc or is_audit_doc):
|
|
183
|
+
bonus -= 0.18
|
|
184
|
+
if qtype == "user_preference":
|
|
185
|
+
if is_user_pref_doc:
|
|
186
|
+
bonus += 0.42
|
|
187
|
+
if not is_user_pref_doc and (is_policy_doc or is_audit_doc):
|
|
188
|
+
bonus -= 0.16
|
|
189
|
+
if qtype == "continuity":
|
|
190
|
+
if bucket == "reflections" and is_transcript:
|
|
191
|
+
bonus -= 0.30
|
|
192
|
+
if is_continuity_doc and (bucket in {"knowledge", "directives"} or is_canonical_file):
|
|
193
|
+
bonus += 0.70
|
|
194
|
+
if is_policy_doc or is_audit_doc:
|
|
195
|
+
bonus -= 0.18
|
|
196
|
+
|
|
197
|
+
if bucket == "runbooks":
|
|
198
|
+
if "readme.md" in origin_source or "repos/openclaw/readme.md" in origin_source or "start with the docs index" in text:
|
|
199
|
+
bonus -= 0.55
|
|
200
|
+
if "changelog.md" in text or "release notes" in text or "thanks @" in text or "(#" in text:
|
|
201
|
+
bonus -= 0.35
|
|
202
|
+
if "quarantine/" in text or "/backups/" in text or "memory/candidates/" in text:
|
|
203
|
+
bonus -= 0.25
|
|
204
|
+
return bonus
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _doc_type(metadata: Dict[str, Any], bucket: str, content: str) -> str:
|
|
208
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
209
|
+
origin_source = str(prov.get("origin_source") or metadata.get("source_path") or "").lower()
|
|
210
|
+
source_label = str(metadata.get("source_label") or "").lower()
|
|
211
|
+
text = f"{metadata} {content}".lower()
|
|
212
|
+
if "canonical-openclaw-upgrade-runbook" in source_label or "/docs/runbooks/" in origin_source or "openclaw-upgrade-validation-runbook.md" in origin_source:
|
|
213
|
+
return "runbook"
|
|
214
|
+
if "user-preferences-source-of-truth" in origin_source or "gpt 5.2 codex (ultra)" in text or "never send authentication, challenge, or verification prompts" in text:
|
|
215
|
+
return "user_preference"
|
|
216
|
+
if "tbc-truth-plane-source-of-truth" in origin_source or "librenms on 10.5.56.130" in text or "primary truth plane" in text:
|
|
217
|
+
return "infra_fact"
|
|
218
|
+
if "current-work-continuity-source-of-truth" in origin_source:
|
|
219
|
+
return "continuity"
|
|
220
|
+
if "runtime authority" in text or "launchagent" in text or "authoritative source tree" in text:
|
|
221
|
+
return "authority_map"
|
|
222
|
+
if "policy established" in text or "anti-rediscovery" in text or "authoritative recall memory" in text:
|
|
223
|
+
return "policy"
|
|
224
|
+
if "audit" in text or "scorecard" in text or "results —" in text or "results -" in text:
|
|
225
|
+
return "audit"
|
|
226
|
+
if "how to" in text or "procedure" in text or "checklist" in text or "safely upgrade" in text or "validate logs" in text:
|
|
227
|
+
return "runbook"
|
|
228
|
+
if "truth plane" in text or "librenms" in text or "10.5.56.130" in text or "192.168.1.x" in text or "10.x" in text:
|
|
229
|
+
return "infra_fact"
|
|
230
|
+
if "preferred development model" in text or "default model" in text or "messaging auth safety" in text:
|
|
231
|
+
return "user_preference"
|
|
232
|
+
if bucket != "runbooks" and bucket != "directives" and ("work unit" in text or "checkpoint" in text or "current work lane" in text or "next step" in text):
|
|
233
|
+
return "continuity"
|
|
234
|
+
return "generic"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _artifact_family(metadata: Dict[str, Any], bucket: str, content: str) -> str:
|
|
238
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
239
|
+
origin_source = str(prov.get("origin_source") or metadata.get("source_path") or "").lower()
|
|
240
|
+
text = f"{metadata} {content}".lower()
|
|
241
|
+
if "/docs/runbooks/" in origin_source:
|
|
242
|
+
return "canonical_runbook"
|
|
243
|
+
if "user-preferences-source-of-truth" in origin_source:
|
|
244
|
+
return "user_preference"
|
|
245
|
+
if "tbc-truth-plane-source-of-truth" in origin_source:
|
|
246
|
+
return "infra_fact"
|
|
247
|
+
if "current-work-continuity-source-of-truth" in origin_source:
|
|
248
|
+
return "continuity_summary"
|
|
249
|
+
if "runtime-authority-map" in origin_source or "ocmemog-operational-memory-policy" in origin_source or "docs/ops/" in origin_source:
|
|
250
|
+
return "ops_doc"
|
|
251
|
+
if "audit" in origin_source or "manifest" in origin_source or "inventory" in origin_source or "almost-trash" in origin_source:
|
|
252
|
+
return "cleanup_or_audit_meta"
|
|
253
|
+
if origin_source in {"transcript", "session"} or "memory/transcripts/" in text:
|
|
254
|
+
return "transcript_or_session_artifact"
|
|
255
|
+
if "readme.md" in origin_source:
|
|
256
|
+
return "readme_pseudorunbook"
|
|
257
|
+
if bucket == "reflections":
|
|
258
|
+
return "reflection"
|
|
259
|
+
if bucket == "directives":
|
|
260
|
+
if "preferred development model" in text or "messaging auth safety" in text or "never send authentication" in text:
|
|
261
|
+
return "user_preference"
|
|
262
|
+
if "current work lane" in text or "what still fails" in text or "next priority order" in text:
|
|
263
|
+
return "continuity_summary"
|
|
264
|
+
return "generic_process_directive"
|
|
265
|
+
if bucket == "runbooks" and str(content or "").startswith("After any substantial work block"):
|
|
266
|
+
return "generic_process_directive"
|
|
267
|
+
return "generic"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
_LEGACY_BAD_CONTINUITY_RUNBOOKS = {
|
|
271
|
+
'runbooks:177', 'runbooks:165', 'runbooks:151', 'runbooks:132', 'runbooks:126', 'runbooks:99', 'runbooks:75', 'runbooks:65', 'runbooks:45'
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _artifact_family_penalty(prompt: str, metadata: Dict[str, Any], bucket: str, content: str) -> float:
|
|
276
|
+
qtype = _query_type(prompt)
|
|
277
|
+
family = _artifact_family(metadata, bucket, content)
|
|
278
|
+
ref = str(metadata.get('reference') or '')
|
|
279
|
+
lowered = str(content or '').lower()
|
|
280
|
+
if qtype == "infra_fact":
|
|
281
|
+
if family in {"canonical_runbook", "cleanup_or_audit_meta", "transcript_or_session_artifact", "readme_pseudorunbook", "generic_process_directive"}:
|
|
282
|
+
return -0.45
|
|
283
|
+
if qtype == "user_preference":
|
|
284
|
+
if family in {"canonical_runbook", "cleanup_or_audit_meta", "transcript_or_session_artifact", "readme_pseudorunbook", "generic_process_directive"}:
|
|
285
|
+
return -0.45
|
|
286
|
+
if qtype == "continuity":
|
|
287
|
+
if ref in _LEGACY_BAD_CONTINUITY_RUNBOOKS:
|
|
288
|
+
return -1.25
|
|
289
|
+
if family in {"cleanup_or_audit_meta", "transcript_or_session_artifact", "readme_pseudorunbook", "generic_process_directive", "canonical_runbook", "ops_doc"}:
|
|
290
|
+
return -0.55
|
|
291
|
+
if family == "reflection":
|
|
292
|
+
return -0.18
|
|
293
|
+
if bucket == 'tasks' and ('name: steven imbimbo' in lowered or 'what to call them:' in lowered or 'timezone:' in lowered):
|
|
294
|
+
return -0.65
|
|
295
|
+
return 0.0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _query_type(prompt: str) -> str:
|
|
299
|
+
lowered = str(prompt or "").strip().lower()
|
|
300
|
+
if not lowered:
|
|
301
|
+
return "generic"
|
|
302
|
+
if "policy" in lowered or "rule" in lowered or "anti-rediscovery" in lowered:
|
|
303
|
+
return "policy"
|
|
304
|
+
if "what launches" in lowered or "runtime authority" in lowered or "authoritative source tree" in lowered or "health endpoint" in lowered:
|
|
305
|
+
return "authority_map"
|
|
306
|
+
if any(marker in lowered for marker in _PROCEDURAL_QUERY_MARKERS):
|
|
307
|
+
return "runbook"
|
|
308
|
+
if "truth plane" in lowered or "librenms" in lowered or "192.168.1.x" in lowered or "10.x" in lowered:
|
|
309
|
+
return "infra_fact"
|
|
310
|
+
if "preferred development model" in lowered or "auth safety rule" in lowered or "preference" in lowered:
|
|
311
|
+
return "user_preference"
|
|
312
|
+
if "current work lane" in lowered or "checkpoint" in lowered or "next step" in lowered:
|
|
313
|
+
return "continuity"
|
|
314
|
+
return "generic"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _doc_type_bonus(prompt: str, metadata: Dict[str, Any], bucket: str, content: str) -> float:
|
|
318
|
+
qtype = _query_type(prompt)
|
|
319
|
+
dtype = _doc_type(metadata, bucket, content)
|
|
320
|
+
if qtype == "generic":
|
|
321
|
+
return 0.0
|
|
322
|
+
if qtype == dtype:
|
|
323
|
+
return 0.35
|
|
324
|
+
compatible = {
|
|
325
|
+
("runbook", "authority_map"): -0.16,
|
|
326
|
+
("runbook", "policy"): -0.28,
|
|
327
|
+
("runbook", "audit"): -0.24,
|
|
328
|
+
("infra_fact", "audit"): -0.16,
|
|
329
|
+
("infra_fact", "policy"): -0.12,
|
|
330
|
+
("policy", "audit"): -0.06,
|
|
331
|
+
("policy", "runbook"): -0.12,
|
|
332
|
+
("authority_map", "audit"): -0.10,
|
|
333
|
+
("continuity", "audit"): -0.08,
|
|
334
|
+
("user_preference", "policy"): -0.06,
|
|
335
|
+
}
|
|
336
|
+
if (qtype, dtype) in compatible:
|
|
337
|
+
return compatible[(qtype, dtype)]
|
|
338
|
+
if dtype in {"policy", "audit", "authority_map"} and qtype not in {dtype, "generic"}:
|
|
339
|
+
return -0.10
|
|
340
|
+
return 0.0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _derivative_penalty(bucket: str, metadata: Dict[str, Any], content: str, governance: Dict[str, Any]) -> float:
|
|
344
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
345
|
+
source_refs = prov.get("source_references") if isinstance(prov.get("source_references"), list) else []
|
|
346
|
+
derived_via = str(prov.get("derived_via") or "").strip().lower()
|
|
347
|
+
origin_source = str(prov.get("origin_source") or "").strip().lower()
|
|
348
|
+
lowered = str(content or "").lower()
|
|
349
|
+
penalty = 0.0
|
|
350
|
+
if bucket == "reflections":
|
|
351
|
+
if source_refs:
|
|
352
|
+
penalty -= 0.28
|
|
353
|
+
if derived_via in {"ponder", "distill", "artifact_distill"}:
|
|
354
|
+
penalty -= 0.12
|
|
355
|
+
if lowered.startswith("potential durable learning:") or lowered.startswith("consolidated pattern:"):
|
|
356
|
+
penalty -= 0.18
|
|
357
|
+
if governance.get("canonical_reference") and str(governance.get("canonical_reference")) != "" and str(governance.get("canonical_reference")) != str(metadata.get("reference") or ""):
|
|
358
|
+
penalty -= 0.10
|
|
359
|
+
if origin_source == "transcript" and bucket == "reflections":
|
|
360
|
+
penalty -= 0.06
|
|
361
|
+
return penalty
|
|
362
|
+
|
|
363
|
+
|
|
108
364
|
def _parse_metadata(raw: Any) -> Dict[str, Any]:
|
|
109
365
|
if isinstance(raw, dict):
|
|
110
366
|
return raw
|
|
@@ -369,7 +625,7 @@ def retrieve(
|
|
|
369
625
|
if source_type in selected_categories and source_id:
|
|
370
626
|
semantic_scores[f"{source_type}:{source_id}"] = float(item.get("score") or 0.0)
|
|
371
627
|
|
|
372
|
-
def score_record(*, content: str, memory_ref: str, promo_conf: float, timestamp: str | None, metadata_payload: Dict[str, Any]) -> tuple[float, Dict[str, float]]:
|
|
628
|
+
def score_record(*, bucket: str, content: str, memory_ref: str, promo_conf: float, timestamp: str | None, metadata_payload: Dict[str, Any]) -> tuple[float, Dict[str, float]]:
|
|
373
629
|
keyword = _match_score(content, prompt)
|
|
374
630
|
semantic = float(semantic_scores.get(memory_ref, 0.0))
|
|
375
631
|
reinf = reinforcement.get(memory_ref, {})
|
|
@@ -377,7 +633,12 @@ def retrieve(
|
|
|
377
633
|
promo_score = float(promo_conf) * 0.2
|
|
378
634
|
recency = _recency_score(timestamp)
|
|
379
635
|
lane_bonus = _lane_bonus(metadata_payload, active_lane)
|
|
380
|
-
|
|
636
|
+
intent_bonus = _bucket_intent_bonus(bucket, prompt, metadata_payload)
|
|
637
|
+
authority_bonus = _source_authority_bonus(bucket, metadata_payload, content, prompt)
|
|
638
|
+
doc_type_bonus = _doc_type_bonus(prompt, metadata_payload, bucket, content)
|
|
639
|
+
family_penalty = _artifact_family_penalty(prompt, metadata_payload, bucket, content)
|
|
640
|
+
derivative_penalty = _derivative_penalty(bucket, metadata_payload, content, _governance_summary(_governance_state(metadata_payload)[1]))
|
|
641
|
+
score = round((keyword * 0.45) + (semantic * 0.35) + reinf_score + promo_score + recency + lane_bonus + intent_bonus + authority_bonus + doc_type_bonus + family_penalty + derivative_penalty, 3)
|
|
381
642
|
return score, {
|
|
382
643
|
"keyword": round(keyword, 3),
|
|
383
644
|
"semantic": round(semantic, 3),
|
|
@@ -389,6 +650,11 @@ def retrieve(
|
|
|
389
650
|
"promotion": round(promo_score, 3),
|
|
390
651
|
"recency": round(recency, 3),
|
|
391
652
|
"lane_bonus": round(lane_bonus, 3),
|
|
653
|
+
"intent_bonus": round(intent_bonus, 3),
|
|
654
|
+
"authority_bonus": round(authority_bonus, 3),
|
|
655
|
+
"doc_type_bonus": round(doc_type_bonus, 3),
|
|
656
|
+
"family_penalty": round(family_penalty, 3),
|
|
657
|
+
"derivative_penalty": round(derivative_penalty, 3),
|
|
392
658
|
}
|
|
393
659
|
|
|
394
660
|
for table in selected_categories:
|
|
@@ -425,11 +691,16 @@ def retrieve(
|
|
|
425
691
|
bucket_counts[memory_status] = int(bucket_counts.get(memory_status) or 0) + 1
|
|
426
692
|
continue
|
|
427
693
|
metadata = provenance.fetch_reference(mem_ref)
|
|
428
|
-
score, signals = score_record(content=content, memory_ref=mem_ref, promo_conf=promo_conf, timestamp=timestamp, metadata_payload=metadata_payload)
|
|
694
|
+
score, signals = score_record(bucket=table, content=content, memory_ref=mem_ref, promo_conf=promo_conf, timestamp=timestamp, metadata_payload=metadata_payload)
|
|
429
695
|
if memory_status == "contested":
|
|
430
696
|
score = round(max(0.0, score - 0.15), 3)
|
|
431
697
|
signals["contradiction_penalty"] = 0.15
|
|
432
|
-
|
|
698
|
+
primary_reason_order = ("keyword", "semantic", "reinforcement", "promotion", "recency")
|
|
699
|
+
primary_reason_signals = {
|
|
700
|
+
key: float(signals.get(key) or 0.0)
|
|
701
|
+
for key in primary_reason_order
|
|
702
|
+
}
|
|
703
|
+
selected_because = max(primary_reason_signals, key=primary_reason_signals.get) if primary_reason_signals else "keyword"
|
|
433
704
|
reinforcement_count = float(signals.get("reinforcement_count") or 0.0)
|
|
434
705
|
negative_penalty = float(signals.get("reinforcement_negative_penalty") or 0.0)
|
|
435
706
|
if reinforcement_count > 0.0 or negative_penalty > 0.0:
|
|
@@ -279,6 +279,17 @@ CREATE TABLE IF NOT EXISTS conversation_state (
|
|
|
279
279
|
CREATE INDEX IF NOT EXISTS idx_conversation_state_conversation ON conversation_state(conversation_id);
|
|
280
280
|
CREATE INDEX IF NOT EXISTS idx_conversation_state_session ON conversation_state(session_id);
|
|
281
281
|
CREATE INDEX IF NOT EXISTS idx_conversation_state_thread ON conversation_state(thread_id);
|
|
282
|
+
|
|
283
|
+
CREATE TABLE IF NOT EXISTS unresolved_state (
|
|
284
|
+
state_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
285
|
+
state_type TEXT NOT NULL,
|
|
286
|
+
reference TEXT,
|
|
287
|
+
summary TEXT,
|
|
288
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
289
|
+
resolved INTEGER NOT NULL DEFAULT 0
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_unresolved_state_resolved_reference_created ON unresolved_state(resolved, reference, created_at DESC);
|
|
282
293
|
"""
|
|
283
294
|
|
|
284
295
|
_WRITE_QUEUE: "queue.Queue[tuple]" = queue.Queue()
|
|
@@ -2,10 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
import time
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Dict, List
|
|
6
7
|
|
|
7
8
|
from ocmemog.runtime import state_store
|
|
8
9
|
from ocmemog.runtime.instrumentation import emit_event
|
|
10
|
+
from ocmemog.runtime.memory import store
|
|
9
11
|
|
|
10
12
|
LOGFILE = state_store.report_log_path()
|
|
11
13
|
|
|
@@ -17,12 +19,15 @@ TYPES = {
|
|
|
17
19
|
"incomplete_hypothesis",
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
_LEGACY_DB_IMPORTED = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _legacy_db_path() -> Path:
|
|
26
|
+
return state_store.data_dir() / "unresolved_state.db"
|
|
27
|
+
|
|
20
28
|
|
|
21
29
|
def _connect() -> sqlite3.Connection:
|
|
22
|
-
|
|
23
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
conn = sqlite3.connect(str(path))
|
|
25
|
-
conn.row_factory = sqlite3.Row
|
|
30
|
+
conn = store.connect()
|
|
26
31
|
conn.execute(
|
|
27
32
|
"""
|
|
28
33
|
CREATE TABLE IF NOT EXISTS unresolved_state (
|
|
@@ -35,39 +40,95 @@ def _connect() -> sqlite3.Connection:
|
|
|
35
40
|
)
|
|
36
41
|
"""
|
|
37
42
|
)
|
|
43
|
+
conn.execute(
|
|
44
|
+
"CREATE INDEX IF NOT EXISTS idx_unresolved_state_resolved_reference_created ON unresolved_state(resolved, reference, created_at DESC)"
|
|
45
|
+
)
|
|
46
|
+
_import_legacy_db_if_needed(conn)
|
|
38
47
|
return conn
|
|
39
48
|
|
|
40
49
|
|
|
50
|
+
def _import_legacy_db_if_needed(conn: sqlite3.Connection) -> None:
|
|
51
|
+
global _LEGACY_DB_IMPORTED
|
|
52
|
+
if _LEGACY_DB_IMPORTED:
|
|
53
|
+
return
|
|
54
|
+
legacy_path = _legacy_db_path()
|
|
55
|
+
if not legacy_path.exists():
|
|
56
|
+
_LEGACY_DB_IMPORTED = True
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
existing = conn.execute("SELECT COUNT(*) FROM unresolved_state").fetchone()
|
|
60
|
+
existing_count = int(existing[0]) if existing else 0
|
|
61
|
+
legacy = sqlite3.connect(str(legacy_path))
|
|
62
|
+
legacy.row_factory = sqlite3.Row
|
|
63
|
+
try:
|
|
64
|
+
tables = {
|
|
65
|
+
str(row[0])
|
|
66
|
+
for row in legacy.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
|
67
|
+
}
|
|
68
|
+
if "unresolved_state" not in tables:
|
|
69
|
+
_LEGACY_DB_IMPORTED = True
|
|
70
|
+
return
|
|
71
|
+
rows = legacy.execute(
|
|
72
|
+
"SELECT state_type, reference, summary, created_at, resolved FROM unresolved_state ORDER BY state_id ASC"
|
|
73
|
+
).fetchall()
|
|
74
|
+
finally:
|
|
75
|
+
legacy.close()
|
|
76
|
+
if rows and existing_count == 0:
|
|
77
|
+
conn.executemany(
|
|
78
|
+
"INSERT INTO unresolved_state (state_type, reference, summary, created_at, resolved) VALUES (?, ?, ?, ?, ?)",
|
|
79
|
+
[
|
|
80
|
+
(
|
|
81
|
+
str(row["state_type"] or "unresolved_question"),
|
|
82
|
+
row["reference"],
|
|
83
|
+
row["summary"],
|
|
84
|
+
row["created_at"] or time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
85
|
+
int(row["resolved"] or 0),
|
|
86
|
+
)
|
|
87
|
+
for row in rows
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
conn.commit()
|
|
91
|
+
emit_event(LOGFILE, "brain_unresolved_state_legacy_imported", status="ok", imported=len(rows))
|
|
92
|
+
finally:
|
|
93
|
+
_LEGACY_DB_IMPORTED = True
|
|
94
|
+
|
|
95
|
+
|
|
41
96
|
def add_unresolved_state(state_type: str, reference: str, summary: str) -> int:
|
|
42
97
|
if state_type not in TYPES:
|
|
43
98
|
state_type = "unresolved_question"
|
|
44
99
|
conn = _connect()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
try:
|
|
101
|
+
conn.execute(
|
|
102
|
+
"INSERT INTO unresolved_state (state_type, reference, summary, created_at, resolved) VALUES (?, ?, ?, ?, 0)",
|
|
103
|
+
(state_type, reference, summary, time.strftime("%Y-%m-%d %H:%M:%S")),
|
|
104
|
+
)
|
|
105
|
+
conn.commit()
|
|
106
|
+
row = conn.execute("SELECT last_insert_rowid()").fetchone()
|
|
107
|
+
finally:
|
|
108
|
+
conn.close()
|
|
52
109
|
emit_event(LOGFILE, "brain_unresolved_state_added", status="ok", state_type=state_type)
|
|
53
110
|
return int(row[0]) if row else 0
|
|
54
111
|
|
|
55
112
|
|
|
56
113
|
def list_unresolved_state(limit: int = 20) -> List[Dict[str, object]]:
|
|
57
114
|
conn = _connect()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
115
|
+
try:
|
|
116
|
+
rows = conn.execute(
|
|
117
|
+
"SELECT state_id, state_type, reference, summary, created_at FROM unresolved_state WHERE resolved=0 ORDER BY created_at DESC LIMIT ?",
|
|
118
|
+
(limit,),
|
|
119
|
+
).fetchall()
|
|
120
|
+
finally:
|
|
121
|
+
conn.close()
|
|
63
122
|
return [dict(row) for row in rows]
|
|
64
123
|
|
|
65
124
|
|
|
66
125
|
def resolve_unresolved_state(state_id: int) -> bool:
|
|
67
126
|
conn = _connect()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
127
|
+
try:
|
|
128
|
+
conn.execute("UPDATE unresolved_state SET resolved=1 WHERE state_id=?", (state_id,))
|
|
129
|
+
conn.commit()
|
|
130
|
+
finally:
|
|
131
|
+
conn.close()
|
|
71
132
|
emit_event(LOGFILE, "brain_unresolved_state_resolved", status="ok", state_id=state_id)
|
|
72
133
|
return True
|
|
73
134
|
|
|
@@ -78,16 +139,20 @@ def list_unresolved_state_for_references(references: List[str], limit: int = 20)
|
|
|
78
139
|
return []
|
|
79
140
|
placeholders = ",".join("?" for _ in refs)
|
|
80
141
|
conn = _connect()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
142
|
+
try:
|
|
143
|
+
rows = conn.execute(
|
|
144
|
+
f"SELECT state_id, state_type, reference, summary, created_at FROM unresolved_state WHERE resolved=0 AND reference IN ({placeholders}) ORDER BY created_at DESC LIMIT ?",
|
|
145
|
+
(*refs, limit),
|
|
146
|
+
).fetchall()
|
|
147
|
+
finally:
|
|
148
|
+
conn.close()
|
|
86
149
|
return [dict(row) for row in rows]
|
|
87
150
|
|
|
88
151
|
|
|
89
152
|
def count_unresolved_state() -> int:
|
|
90
153
|
conn = _connect()
|
|
91
|
-
|
|
92
|
-
|
|
154
|
+
try:
|
|
155
|
+
row = conn.execute("SELECT COUNT(*) FROM unresolved_state WHERE resolved=0").fetchone()
|
|
156
|
+
finally:
|
|
157
|
+
conn.close()
|
|
93
158
|
return int(row[0]) if row else 0
|
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -684,7 +684,13 @@ class GetRequest(BaseModel):
|
|
|
684
684
|
|
|
685
685
|
class ContextRequest(BaseModel):
|
|
686
686
|
reference: str
|
|
687
|
-
radius: int = Field(default=
|
|
687
|
+
radius: int = Field(default=200, ge=0, le=1000)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class TranscriptClaimSearchRequest(BaseModel):
|
|
691
|
+
query: str
|
|
692
|
+
radius: int = Field(default=40, ge=0, le=1000)
|
|
693
|
+
limit: int = Field(default=5, ge=1, le=20)
|
|
688
694
|
|
|
689
695
|
|
|
690
696
|
class RecentRequest(BaseModel):
|
|
@@ -743,6 +749,11 @@ class ConversationHydrateRequest(BaseModel):
|
|
|
743
749
|
predictive_brief_limit: int = Field(default=5, ge=1, le=12)
|
|
744
750
|
|
|
745
751
|
|
|
752
|
+
_LONG_SESSION_TURN_THRESHOLD = max(200, int(os.environ.get("OCMEMOG_LONG_SESSION_TURN_THRESHOLD", "800") or "800"))
|
|
753
|
+
_LONG_SESSION_HYDRATE_TURNS = max(6, int(os.environ.get("OCMEMOG_LONG_SESSION_HYDRATE_TURNS", "8") or "8"))
|
|
754
|
+
_LONG_SESSION_LINKED_MEMORY_LIMIT = max(1, int(os.environ.get("OCMEMOG_LONG_SESSION_LINKED_MEMORY_LIMIT", "2") or "2"))
|
|
755
|
+
|
|
756
|
+
|
|
746
757
|
class ConversationCheckpointRequest(BaseModel):
|
|
747
758
|
conversation_id: Optional[str] = None
|
|
748
759
|
session_id: Optional[str] = None
|
|
@@ -939,7 +950,19 @@ def _build_predictive_brief(
|
|
|
939
950
|
) -> Dict[str, Any]:
|
|
940
951
|
latest_user_ask = ((summary.get("latest_user_intent") or {}).get("effective_content") if isinstance(summary.get("latest_user_intent"), dict) else None) or ((summary.get("latest_user_ask") or {}).get("content") if isinstance(summary.get("latest_user_ask"), dict) else None) or ""
|
|
941
952
|
summary_text = str(summary.get("summary_text") or "").strip()
|
|
942
|
-
|
|
953
|
+
|
|
954
|
+
linked_query_hints: list[str] = []
|
|
955
|
+
for item in linked_memories[:3]:
|
|
956
|
+
if not isinstance(item, dict):
|
|
957
|
+
continue
|
|
958
|
+
hint = _compact_text(item.get("summary") or item.get("content") or "", 120)
|
|
959
|
+
lowered = hint.lower()
|
|
960
|
+
if any(token in lowered for token in ("codex", "openclaw", "tool", "runtime", "config", "setup", "session", "endpoint", "mechanism", "acp")):
|
|
961
|
+
linked_query_hints.append(hint)
|
|
962
|
+
query_seed = latest_user_ask or summary_text or "resume context"
|
|
963
|
+
if linked_query_hints:
|
|
964
|
+
query_seed = f"{query_seed} | prior linked precedent: {' | '.join(linked_query_hints[:2])}"
|
|
965
|
+
query = _compact_text(query_seed, 260)
|
|
943
966
|
lane = retrieval.infer_lane(query)
|
|
944
967
|
profiles = retrieval._load_lane_profiles()
|
|
945
968
|
profile = profiles.get(lane or "") if lane else None
|
|
@@ -986,6 +1009,53 @@ def _build_predictive_brief(
|
|
|
986
1009
|
"reference": item.get("reference"),
|
|
987
1010
|
"summary": _compact_text(item.get("summary") or item.get("content") or item.get("reference") or "", 140),
|
|
988
1011
|
})
|
|
1012
|
+
|
|
1013
|
+
lane_hints = []
|
|
1014
|
+
active_branch = summary.get("active_branch")
|
|
1015
|
+
active_branch_summary = ""
|
|
1016
|
+
if isinstance(active_branch, dict):
|
|
1017
|
+
active_branch_summary = str(active_branch.get("summary") or active_branch.get("branch_id") or "").strip()
|
|
1018
|
+
elif isinstance(active_branch, str):
|
|
1019
|
+
active_branch_summary = active_branch.strip()
|
|
1020
|
+
if active_branch_summary:
|
|
1021
|
+
lane_hints.append({"kind": "active_branch", "summary": _compact_text(active_branch_summary, 120)})
|
|
1022
|
+
if checkpoint and str(checkpoint.get("summary") or "").strip():
|
|
1023
|
+
lane_hints.append({
|
|
1024
|
+
"kind": "latest_checkpoint",
|
|
1025
|
+
"summary": _compact_text(checkpoint.get("summary") or "", 160),
|
|
1026
|
+
"reference": checkpoint.get("reference"),
|
|
1027
|
+
})
|
|
1028
|
+
if summary_text:
|
|
1029
|
+
lane_hints.append({"kind": "summary", "summary": _compact_text(summary_text, 180)})
|
|
1030
|
+
|
|
1031
|
+
next_steps = []
|
|
1032
|
+
pending_actions = summary.get("pending_actions") if isinstance(summary.get("pending_actions"), list) else []
|
|
1033
|
+
for item in pending_actions[:2]:
|
|
1034
|
+
if not isinstance(item, dict):
|
|
1035
|
+
continue
|
|
1036
|
+
summary_text_value = str(item.get("summary") or "").strip()
|
|
1037
|
+
if not summary_text_value:
|
|
1038
|
+
continue
|
|
1039
|
+
next_steps.append({
|
|
1040
|
+
"kind": item.get("kind") or "pending_action",
|
|
1041
|
+
"summary": _compact_text(summary_text_value, 140),
|
|
1042
|
+
"reference": item.get("source_reference") or item.get("reference"),
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
avoid_drift = []
|
|
1046
|
+
for item in linked_memories[:4]:
|
|
1047
|
+
if not isinstance(item, dict):
|
|
1048
|
+
continue
|
|
1049
|
+
text = _compact_text(item.get("summary") or item.get("content") or "", 160)
|
|
1050
|
+
lowered = text.lower()
|
|
1051
|
+
if not text:
|
|
1052
|
+
continue
|
|
1053
|
+
if any(token in lowered for token in ("not ", "do not", "avoid", "instead of", "same-surface", "different mechanism", "don't assume")):
|
|
1054
|
+
avoid_drift.append({
|
|
1055
|
+
"reference": item.get("reference"),
|
|
1056
|
+
"summary": text,
|
|
1057
|
+
})
|
|
1058
|
+
|
|
989
1059
|
return {
|
|
990
1060
|
"lane": lane,
|
|
991
1061
|
"query": query,
|
|
@@ -1005,6 +1075,9 @@ def _build_predictive_brief(
|
|
|
1005
1075
|
],
|
|
1006
1076
|
"memories": items,
|
|
1007
1077
|
"linked_memories": recent_linked,
|
|
1078
|
+
"lane_hints": lane_hints[:3],
|
|
1079
|
+
"next_steps": next_steps,
|
|
1080
|
+
"avoid_drift": avoid_drift[:2],
|
|
1008
1081
|
"latest_user_ask": _compact_text(latest_user_ask, 180),
|
|
1009
1082
|
"summary_text": _compact_text(summary_text, 220),
|
|
1010
1083
|
"mode": "predictive-brief",
|
|
@@ -1156,7 +1229,29 @@ def _read_transcript_snippet(path: Path, line_start: Optional[int], line_end: Op
|
|
|
1156
1229
|
}
|
|
1157
1230
|
|
|
1158
1231
|
|
|
1232
|
+
def _search_transcripts_for_claim(query: str, radius: int, limit: int) -> list[Dict[str, Any]]:
|
|
1233
|
+
needle = (query or "").strip().lower()
|
|
1234
|
+
if not needle:
|
|
1235
|
+
return []
|
|
1236
|
+
hits: list[Dict[str, Any]] = []
|
|
1237
|
+
for root in _allowed_transcript_roots():
|
|
1238
|
+
if not root.exists() or not root.is_dir():
|
|
1239
|
+
continue
|
|
1240
|
+
for path in sorted(root.glob('*.log')):
|
|
1241
|
+
try:
|
|
1242
|
+
with path.open('r', encoding='utf-8', errors='ignore') as handle:
|
|
1243
|
+
for idx, line in enumerate(handle, start=1):
|
|
1244
|
+
if needle in line.lower():
|
|
1245
|
+
hits.append(_read_transcript_snippet(path, idx, idx, radius))
|
|
1246
|
+
if len(hits) >= limit:
|
|
1247
|
+
return hits
|
|
1248
|
+
except OSError:
|
|
1249
|
+
continue
|
|
1250
|
+
return hits
|
|
1251
|
+
|
|
1252
|
+
|
|
1159
1253
|
@app.get("/healthz")
|
|
1254
|
+
@app.get("/health")
|
|
1160
1255
|
def healthz() -> dict[str, Any]:
|
|
1161
1256
|
payload = _runtime_payload()
|
|
1162
1257
|
payload["ok"] = True
|
|
@@ -1764,6 +1859,7 @@ def memory_get(request: GetRequest) -> dict[str, Any]:
|
|
|
1764
1859
|
def memory_context(request: ContextRequest) -> dict[str, Any]:
|
|
1765
1860
|
runtime = _runtime_payload()
|
|
1766
1861
|
links = memory_links.get_memory_links(request.reference)
|
|
1862
|
+
prov = provenance.hydrate_reference(request.reference, depth=2)
|
|
1767
1863
|
transcript = None
|
|
1768
1864
|
for link in links:
|
|
1769
1865
|
target = link.get("target_reference", "")
|
|
@@ -1772,12 +1868,34 @@ def memory_context(request: ContextRequest) -> dict[str, Any]:
|
|
|
1772
1868
|
path, line_start, line_end = parsed
|
|
1773
1869
|
transcript = _read_transcript_snippet(path, line_start, line_end, request.radius)
|
|
1774
1870
|
break
|
|
1871
|
+
if transcript is None and isinstance(prov, dict):
|
|
1872
|
+
anchor = prov.get("transcript_anchor") or {}
|
|
1873
|
+
path_value = anchor.get("path")
|
|
1874
|
+
if path_value:
|
|
1875
|
+
transcript = _read_transcript_snippet(
|
|
1876
|
+
Path(path_value),
|
|
1877
|
+
anchor.get("start_line"),
|
|
1878
|
+
anchor.get("end_line"),
|
|
1879
|
+
request.radius,
|
|
1880
|
+
)
|
|
1775
1881
|
return {
|
|
1776
1882
|
"ok": True,
|
|
1777
1883
|
"reference": request.reference,
|
|
1778
1884
|
"links": links,
|
|
1779
1885
|
"transcript": transcript,
|
|
1780
|
-
"provenance":
|
|
1886
|
+
"provenance": prov,
|
|
1887
|
+
**runtime,
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
@app.post("/transcript/claim_search")
|
|
1892
|
+
def transcript_claim_search(request: TranscriptClaimSearchRequest) -> dict[str, Any]:
|
|
1893
|
+
runtime = _runtime_payload()
|
|
1894
|
+
hits = _search_transcripts_for_claim(request.query, request.radius, request.limit)
|
|
1895
|
+
return {
|
|
1896
|
+
"ok": True,
|
|
1897
|
+
"query": request.query,
|
|
1898
|
+
"results": hits,
|
|
1781
1899
|
**runtime,
|
|
1782
1900
|
}
|
|
1783
1901
|
|
|
@@ -1828,18 +1946,34 @@ def conversation_hydrate(request: ConversationHydrateRequest) -> dict[str, Any]:
|
|
|
1828
1946
|
now_ms = time.time() * 1000.0
|
|
1829
1947
|
if cached and (now_ms - cached[0]) <= cache_ttl_ms:
|
|
1830
1948
|
return {**cached[1], **runtime}
|
|
1949
|
+
turn_counts = conversation_state.get_turn_counts(
|
|
1950
|
+
conversation_id=request.conversation_id,
|
|
1951
|
+
session_id=request.session_id,
|
|
1952
|
+
thread_id=request.thread_id,
|
|
1953
|
+
)
|
|
1954
|
+
_mark("get_turn_counts")
|
|
1955
|
+
requested_turns_limit = int(request.turns_limit)
|
|
1956
|
+
effective_turns_limit = requested_turns_limit
|
|
1957
|
+
long_session_mode = turn_counts.get("total", 0) >= _LONG_SESSION_TURN_THRESHOLD
|
|
1958
|
+
if long_session_mode:
|
|
1959
|
+
effective_turns_limit = min(effective_turns_limit, _LONG_SESSION_HYDRATE_TURNS)
|
|
1831
1960
|
turns = conversation_state.get_recent_turns(
|
|
1832
1961
|
conversation_id=request.conversation_id,
|
|
1833
1962
|
session_id=request.session_id,
|
|
1834
1963
|
thread_id=request.thread_id,
|
|
1835
|
-
limit=
|
|
1964
|
+
limit=effective_turns_limit,
|
|
1836
1965
|
)
|
|
1837
1966
|
_mark("get_recent_turns")
|
|
1967
|
+
linked_memories_limit = request.memory_limit
|
|
1968
|
+
if _parse_bool_env("OCMEMOG_HYDRATE_FAST_MODE", default=True):
|
|
1969
|
+
linked_memories_limit = min(linked_memories_limit, 3)
|
|
1970
|
+
if long_session_mode:
|
|
1971
|
+
linked_memories_limit = min(linked_memories_limit, _LONG_SESSION_LINKED_MEMORY_LIMIT)
|
|
1838
1972
|
linked_memories = conversation_state.get_linked_memories(
|
|
1839
1973
|
conversation_id=request.conversation_id,
|
|
1840
1974
|
session_id=request.session_id,
|
|
1841
1975
|
thread_id=request.thread_id,
|
|
1842
|
-
limit=
|
|
1976
|
+
limit=linked_memories_limit,
|
|
1843
1977
|
)
|
|
1844
1978
|
_mark("get_linked_memories")
|
|
1845
1979
|
link_targets: List[Dict[str, Any]] = []
|
|
@@ -1849,11 +1983,6 @@ def conversation_hydrate(request: ConversationHydrateRequest) -> dict[str, Any]:
|
|
|
1849
1983
|
link_targets.extend(memory_links.get_memory_links_for_session(request.session_id))
|
|
1850
1984
|
if request.conversation_id:
|
|
1851
1985
|
link_targets.extend(memory_links.get_memory_links_for_conversation(request.conversation_id))
|
|
1852
|
-
conversation_state._self_heal_legacy_continuity_artifacts(
|
|
1853
|
-
conversation_id=request.conversation_id,
|
|
1854
|
-
session_id=request.session_id,
|
|
1855
|
-
thread_id=request.thread_id,
|
|
1856
|
-
)
|
|
1857
1986
|
latest_checkpoint = conversation_state.get_latest_checkpoint(
|
|
1858
1987
|
conversation_id=request.conversation_id,
|
|
1859
1988
|
session_id=request.session_id,
|
|
@@ -1896,13 +2025,29 @@ def conversation_hydrate(request: ConversationHydrateRequest) -> dict[str, Any]:
|
|
|
1896
2025
|
runtime["warnings"] = [*runtime["warnings"], "hydrate returned persisted state while state refresh was delayed"]
|
|
1897
2026
|
elif state_status == "derived_not_persisted":
|
|
1898
2027
|
runtime["warnings"] = [*runtime["warnings"], "hydrate returned derived state without inline state refresh"]
|
|
1899
|
-
predictive_brief =
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2028
|
+
predictive_brief: Dict[str, Any] = {
|
|
2029
|
+
"query": "",
|
|
2030
|
+
"items": [],
|
|
2031
|
+
"lane": None,
|
|
2032
|
+
"lane_hints": [],
|
|
2033
|
+
"next_steps": [],
|
|
2034
|
+
"avoid_drift": [],
|
|
2035
|
+
"linked_memories": [],
|
|
2036
|
+
}
|
|
2037
|
+
predictive_brief_enabled = _parse_bool_env("OCMEMOG_HYDRATE_PREDICTIVE_BRIEF", default=False)
|
|
2038
|
+
if predictive_brief_enabled:
|
|
2039
|
+
predictive_brief = _build_predictive_brief(
|
|
2040
|
+
request=request,
|
|
2041
|
+
turns=turns,
|
|
2042
|
+
summary=summary,
|
|
2043
|
+
linked_memories=linked_memories,
|
|
2044
|
+
)
|
|
1905
2045
|
_mark("build_predictive_brief")
|
|
2046
|
+
if long_session_mode:
|
|
2047
|
+
runtime["warnings"] = [
|
|
2048
|
+
*runtime["warnings"],
|
|
2049
|
+
f"hydrate long-session guardrail active: total_turns={turn_counts.get('total', 0)} requested_turns_limit={requested_turns_limit} effective_turns_limit={effective_turns_limit}",
|
|
2050
|
+
]
|
|
1906
2051
|
response = {
|
|
1907
2052
|
"ok": True,
|
|
1908
2053
|
"conversation_id": request.conversation_id,
|
|
@@ -1911,11 +2056,15 @@ def conversation_hydrate(request: ConversationHydrateRequest) -> dict[str, Any]:
|
|
|
1911
2056
|
"recent_turns": turns,
|
|
1912
2057
|
"summary": summary,
|
|
1913
2058
|
"predictive_brief": predictive_brief,
|
|
1914
|
-
"turn_counts":
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2059
|
+
"turn_counts": turn_counts,
|
|
2060
|
+
"hydrate_budget": {
|
|
2061
|
+
"requested_turns_limit": requested_turns_limit,
|
|
2062
|
+
"effective_turns_limit": effective_turns_limit,
|
|
2063
|
+
"requested_memory_limit": int(request.memory_limit),
|
|
2064
|
+
"effective_memory_limit": linked_memories_limit,
|
|
2065
|
+
"long_session_mode": long_session_mode,
|
|
2066
|
+
"long_session_threshold": _LONG_SESSION_TURN_THRESHOLD,
|
|
2067
|
+
},
|
|
1919
2068
|
"linked_memories": linked_memories,
|
|
1920
2069
|
"linked_references": link_targets,
|
|
1921
2070
|
"checkpoint_graph": summary.get("checkpoint_graph"),
|
package/package.json
CHANGED
|
@@ -109,8 +109,22 @@ def run_scenario(scenario: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
109
109
|
expect = dict(scenario.get("expect") or {})
|
|
110
110
|
latest_user_contains = str(expect.get("latest_user_contains") or "").strip()
|
|
111
111
|
if latest_user_contains:
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
hydrate_summary = hydrate.get("summary") or {}
|
|
113
|
+
recovered_summary = recovered.get("summary") or {}
|
|
114
|
+
hydrate_latest = (hydrate_summary.get("latest_user_intent") or {}) if isinstance(hydrate_summary.get("latest_user_intent"), dict) else {}
|
|
115
|
+
recovered_latest = (recovered_summary.get("latest_user_intent") or {}) if isinstance(recovered_summary.get("latest_user_intent"), dict) else {}
|
|
116
|
+
value = str(
|
|
117
|
+
hydrate_latest.get("literal", {}).get("content")
|
|
118
|
+
or hydrate_latest.get("literal", {}).get("effective_content")
|
|
119
|
+
or (hydrate.get("state") or {}).get("latest_user_ask")
|
|
120
|
+
or ""
|
|
121
|
+
)
|
|
122
|
+
recovered_value = str(
|
|
123
|
+
recovered_latest.get("literal", {}).get("content")
|
|
124
|
+
or recovered_latest.get("literal", {}).get("effective_content")
|
|
125
|
+
or (recovered.get("state") or {}).get("latest_user_ask")
|
|
126
|
+
or ""
|
|
127
|
+
)
|
|
114
128
|
_run_check(checks, "hydrate.latest_user_contains", latest_user_contains in value, {"value": value, "expected": latest_user_contains})
|
|
115
129
|
_run_check(checks, "restart.latest_user_contains", latest_user_contains in recovered_value, {"value": recovered_value, "expected": latest_user_contains})
|
|
116
130
|
|
|
@@ -121,10 +135,13 @@ def run_scenario(scenario: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
121
135
|
_run_check(checks, "hydrate.reply_chain_contains", all(item in reply_chain_ids for item in reply_chain_expected), {"value": reply_chain_ids, "expected": reply_chain_expected})
|
|
122
136
|
_run_check(checks, "restart.reply_chain_contains", all(item in recovered_reply_chain_ids for item in reply_chain_expected), {"value": recovered_reply_chain_ids, "expected": reply_chain_expected})
|
|
123
137
|
|
|
138
|
+
branch_turn_ids = _message_id_list((hydrate.get("active_branch") or {}).get("turns") or [])
|
|
124
139
|
excluded = list(expect.get("active_branch_turns_exclude") or [])
|
|
125
140
|
if excluded:
|
|
126
|
-
branch_turn_ids = _message_id_list((hydrate.get("active_branch") or {}).get("turns") or [])
|
|
127
141
|
_run_check(checks, "hydrate.active_branch_excludes", all(item not in branch_turn_ids for item in excluded), {"value": branch_turn_ids, "excluded": excluded})
|
|
142
|
+
included = list(expect.get("active_branch_turns_include") or [])
|
|
143
|
+
if included:
|
|
144
|
+
_run_check(checks, "hydrate.active_branch_includes", all(item in branch_turn_ids for item in included), {"value": branch_turn_ids, "included": included})
|
|
128
145
|
|
|
129
146
|
top_ranked_turn_message_id = str(expect.get("top_ranked_turn_message_id") or "").strip()
|
|
130
147
|
if top_ranked_turn_message_id and checkpoint_expand:
|