From 071e1169aa5c692d20166bdb83219efd67d7ef3e Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:44:07 +0300 Subject: [PATCH 001/168] feature: L4 memory layer - persist delayed memory reports to files - support multi-report append/remove --- .gitignore | 8 +- clients/brain_client.py | 2 - contracts/append_delayed_memory.json | 13 +- contracts/list_delayed_memory.json | 24 - contracts/remove_delayed_memory.json | 10 +- contracts/rules_assembler.py | 6 - contracts/save_active_memory.json | 5 +- memory/active/.gitkeep | 1 + memory/delayed/.gitkeep | 1 + memory/facts/.gitkeep | 1 + memory/runtime/.gitkeep | 1 + memory/session/.gitkeep | 1 + rules/brain_context_builder.py | 106 +++- runtime/runtime_context.py | 10 + runtime/stream.py | 92 ++++ tests/runtime_actions/test_delayed_memory.py | 256 ++++++++-- tests/runtime_actions/test_stream_filter.py | 8 +- tests/test_brain_asset_flow.py | 85 ---- tests/test_brain_runtime_actions.py | 64 ++- tests/test_delayed_memory_client_contract.py | 82 ++++ tests/test_delayed_memory_file_store.py | 271 +++++++++++ ui/static/js/chat-attachments.js | 55 ++- ui/static/js/chat-runtime-actions.js | 14 +- ui/static/js/socket/delayed-memory.js | 33 ++ ui/static/js/socket/runtime-actions.js | 137 +++++- ui/templates/index.html | 6 +- utils/actions/common_action_utils.py | 13 - utils/actions/delayed_memory_actions.py | 98 ++-- utils/actions/dispatcher.py | 27 -- utils/brain_client_utils.py | 198 ++++---- utils/context/context_exports.py | 2 - utils/context/delayed_memory.py | 69 --- utils/delayed_memory_file_store.py | 480 +++++++++++++++++++ utils/session_actions_history.py | 1 - websocket/__init__.py | 28 +- websocket/bootstrap.py | 329 ++++--------- 36 files changed, 1872 insertions(+), 665 deletions(-) delete mode 100644 contracts/list_delayed_memory.json create mode 100644 memory/active/.gitkeep create mode 100644 memory/delayed/.gitkeep create mode 100644 memory/facts/.gitkeep create mode 100644 memory/runtime/.gitkeep create mode 100644 memory/session/.gitkeep create mode 100644 tests/test_delayed_memory_file_store.py create mode 100644 utils/delayed_memory_file_store.py diff --git a/.gitignore b/.gitignore index 43d2bbc5..e5b11c30 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,10 @@ assets/* !assets/skills/ !assets/skills/** -.gitkeep +# Memory contents +memory/**/*.json + +# Keep empty memory directory structure +!memory/**/.gitkeep +# Atomic memory write leftovers +memory/**/*.tmp diff --git a/clients/brain_client.py b/clients/brain_client.py index 93c2fdf1..2a683661 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -10,7 +10,6 @@ RUNTIME_ACTION_APPEND_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, - RUNTIME_ACTION_LIST_DELAYED_MEMORY, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, @@ -1519,7 +1518,6 @@ async def apply_runtime_action_result( action.name in ( RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - RUNTIME_ACTION_LIST_DELAYED_MEMORY, RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, diff --git a/contracts/append_delayed_memory.json b/contracts/append_delayed_memory.json index 4670715e..38ccff30 100644 --- a/contracts/append_delayed_memory.json +++ b/contracts/append_delayed_memory.json @@ -7,17 +7,12 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": false }, "rules": [ - "DELAYED MEMORY ACTIONS:", - "Use delayed memory actions only for already saved delayed memory reports.", - "Emit when you need the current saved delayed memory report ids before choosing one.", - "Runtime returns delayed memory lists as trusted TOOL_RESULTS type='delayed_memory'.", - "Emit to append one saved delayed memory, use when user asks to include or append summary/report into the session context.", - "Emit only when the user explicitly asks to remove a saved delayed memory from the current session context; it never deletes the saved report from storage.", - "id is a placeholder; replace it with an actual 6-character delayed memory id from TOOL_RESULTS.", - "After APPEND_DELAYED_MEMORY returns the report, use its content to answer the latest user request." + "Available saved reports are listed in the block.", + "Emit one marker per saved delayed memory. You may emit multiple markers to append multiple reports; already appended ids are ignored.", + "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in ." ], "close_tag": false } diff --git a/contracts/list_delayed_memory.json b/contracts/list_delayed_memory.json deleted file mode 100644 index 210f3106..00000000 --- a/contracts/list_delayed_memory.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "list_delayed_memory": { - "version": 1, - "description": "Allows the model to list saved delayed memory reports.", - "runtime_action": "LIST_DELAYED_MEMORY", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": true - }, - "rules": [ - "DELAYED MEMORY ACTIONS:", - "Use delayed memory actions only for already saved delayed memory reports.", - "Emit when you need the current saved delayed memory report ids before choosing one.", - "Runtime returns delayed memory lists as trusted TOOL_RESULTS type='delayed_memory'.", - "Emit to append one saved delayed memory, use when user asks to include or append summary/report into the session context.", - "Emit only when the user explicitly asks to remove a saved delayed memory from the current session context; it never deletes the saved report from storage.", - "id is a placeholder; replace it with an actual 6-character delayed memory id from TOOL_RESULTS.", - "After APPEND_DELAYED_MEMORY returns the report, use its content to answer the latest user request." - ], - "close_tag": false - } -} diff --git a/contracts/remove_delayed_memory.json b/contracts/remove_delayed_memory.json index f2cc3071..b163ff76 100644 --- a/contracts/remove_delayed_memory.json +++ b/contracts/remove_delayed_memory.json @@ -10,14 +10,8 @@ "emit_followup": false }, "rules": [ - "DELAYED MEMORY ACTIONS:", - "Use delayed memory actions only for already saved delayed memory reports.", - "Emit when you need the current saved delayed memory report ids before choosing one.", - "Runtime returns delayed memory lists as trusted TOOL_RESULTS type='delayed_memory'.", - "Emit to append one saved delayed memory, use when user asks to include or append summary/report into the session context.", - "Emit only when the user explicitly asks to remove a saved delayed memory from the current session context; it never deletes the saved report from storage.", - "id is a placeholder; replace it with an actual 6-character delayed memory id from TOOL_RESULTS.", - "After APPEND_DELAYED_MEMORY returns the report, use its content to answer the latest user request." + "Emit marker to remove single APPENDED_DELAYED_MEMORY from context in the next turn/message; saved reports are never deleted from storage", + "id - is a placeholder word; replace it with the 6-character id prefix shown in ." ], "close_tag": false } diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index f6769ede..13de6dcc 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -30,7 +30,6 @@ ("RESOLVE_TODO", "CAN_RUNTIME_TODO"), ("CHECK_TODO", "CAN_RUNTIME_TODO"), ("SAVE_DELAYED_MEMORY_CONTENT", "CAN_SAVE_DELAYED_MEMORY"), - ("LIST_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("APPEND_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("REMOVE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("SAVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), @@ -297,7 +296,6 @@ def normalize_runtime_action_names(enabled_actions=None) -> tuple[str, ...]: if normalized_name == "SAVE_DELAYED_MEMORY_CONTENT": normalized_names.extend(( - "LIST_DELAYED_MEMORY", "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", )) @@ -531,7 +529,6 @@ def append_rules(action_name: str) -> None: continue if normalized_name in { - "LIST_DELAYED_MEMORY", "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", } and not _context_has_delayed_memory_reports(context): @@ -560,9 +557,6 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT = get_runtime_action_name( "save_delayed_memory" ) -RUNTIME_ACTION_LIST_DELAYED_MEMORY = get_runtime_action_name( - "list_delayed_memory" -) RUNTIME_ACTION_APPEND_DELAYED_MEMORY = get_runtime_action_name( "append_delayed_memory" ) diff --git a/contracts/save_active_memory.json b/contracts/save_active_memory.json index ac178c77..58d30653 100644 --- a/contracts/save_active_memory.json +++ b/contracts/save_active_memory.json @@ -10,8 +10,9 @@ "emit_followup": false }, "rules": [ - "Use when user asks to remember, remind, track, or keep a pending live-session condition.", - "CONDITIONS is a placeholder; replace it with description, value, or conditions." + "Use SAVE_ACTIVE_MEMORY action marker when user asks to remember, remind, track, or keep a pending live-session condition.", + "CONDITIONS is a placeholder word; replace placeholder with description, value, or conditions.", + "When JIN explicitly derives a live rule from the current context and needs to remember it, fulfill the conditions and emit a marker." ], "close_tag": false } diff --git a/memory/active/.gitkeep b/memory/active/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/memory/active/.gitkeep @@ -0,0 +1 @@ + diff --git a/memory/delayed/.gitkeep b/memory/delayed/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/memory/delayed/.gitkeep @@ -0,0 +1 @@ + diff --git a/memory/facts/.gitkeep b/memory/facts/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/memory/facts/.gitkeep @@ -0,0 +1 @@ + diff --git a/memory/runtime/.gitkeep b/memory/runtime/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/memory/runtime/.gitkeep @@ -0,0 +1 @@ + diff --git a/memory/session/.gitkeep b/memory/session/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/memory/session/.gitkeep @@ -0,0 +1 @@ + diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index fc7ec494..e478d738 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -513,6 +513,61 @@ def _build_current_appended_skills_context( ) +def build_delayed_memory_inventory_context( + context=None, +) -> str: + + from utils.delayed_memory_file_store import ( + delayed_memory_filename, + normalize_delayed_memory_reports, + ) + + if context is None: + return "" + + reports = normalize_delayed_memory_reports( + getattr( + context, + "delayed_memory_reports", + {}, + ) + ) + + if not reports: + return "" + + report_names = [] + + for report_id, report in reports.items(): + try: + filename = delayed_memory_filename( + report_id, + report.get( + "title", + "", + ), + ) + except (TypeError, ValueError): + continue + + report_names.append( + filename[:-5] + ) + + if not report_names: + return "" + + report_names.sort( + key=str.casefold + ) + + return ( + "\n" + + "\n".join(report_names) + + "\n" + ) + + def build_appended_delayed_memory_context( context=None, ) -> str: @@ -521,31 +576,41 @@ def build_appended_delayed_memory_context( format_tool_result_payload, ) from utils.brain_client_utils import ( + get_appended_delayed_memory_report, indent_xml, ) if context is None: return "" - appended_report = getattr( - context, - "runtime_appended_delayed_memory", - {}, + appended_reports = get_appended_delayed_memory_report( + context ) - if not isinstance( - appended_report, - dict, - ): + if not appended_reports: return "" - if not appended_report: - return "" + blocks = [] - return ( - "\n" - f"{indent_xml(escape(format_tool_result_payload(appended_report)))}\n" - "" + for report_id, report in appended_reports.items(): + if not isinstance( + report, + dict, + ): + continue + + payload = { + **report, + "id": report_id, + } + blocks.append( + "\n" + f"{indent_xml(escape(format_tool_result_payload(payload)))}\n" + "" + ) + + return "\n".join( + blocks ) @@ -620,6 +685,19 @@ def build_brain_context( context, ) + # Delayed memory inventory block: exposes every available report directly + # below CURRENT_SESSION_STATE, so no separate list action is needed. + delayed_memory_inventory_context = ( + build_delayed_memory_inventory_context( + context + ) + ) + + if delayed_memory_inventory_context: + runtime_context_parts.append( + delayed_memory_inventory_context + ) + # Current runtime todo block: keeps active task checklist state in view. _append_current_runtime_todo( runtime_context_parts, diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index fc90d1a6..90fdfc18 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -101,6 +101,16 @@ class RuntimeContext: default_factory=dict ) + runtime_appended_delayed_memory_ids: list[str] = field( + default_factory=list + ) + + runtime_delayed_memory_file_warnings: list[str] = field( + default_factory=list + ) + + delayed_memory_file_store_enabled: bool = True + runtime_appended_skills: list[dict] = field( default_factory=list ) diff --git a/runtime/stream.py b/runtime/stream.py index f9918229..78e345f7 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -26,6 +26,7 @@ from utils.actions import ( build_runtime_action_id, emit_runtime_action_counter_updates, + is_delayed_memory_report_id, RuntimeActionCounter, normalize_jin_color_payload, RuntimeActionRepetitionGuard, @@ -37,10 +38,12 @@ should_pause_action_guard_for_confirmation, ) from contracts.rules_assembler import ( + RUNTIME_ACTION_APPEND_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, + RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, build_runtime_action_display_text, get_runtime_action_display_name, @@ -1133,6 +1136,17 @@ def get_runtime_action_display_id( return self.jin_color_action_id + if action.name in { + RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + }: + report_id, _report = ( + self.get_delayed_memory_runtime_action_report( + action + ) + ) + return report_id + if ( action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT @@ -1142,6 +1156,54 @@ def get_runtime_action_display_id( return "" + def get_delayed_memory_runtime_action_report( + self, + action, + ): + + if action.name not in { + RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + }: + return "", None + + report_id = str( + action.payload + or "" + ).strip().casefold() + + if not is_delayed_memory_report_id( + report_id + ): + return "", None + + reports = getattr( + self.context, + "delayed_memory_reports", + None, + ) + + if not isinstance( + reports, + dict, + ): + return report_id, None + + report = reports.get( + report_id + ) + + if not isinstance( + report, + dict, + ): + return report_id, None + + return report_id, { + **report, + "id": report_id, + } + async def confirm_unmatched_action_guards( self, actions, @@ -1579,6 +1641,26 @@ async def emit_started_runtime_actions( action.name, action.payload, ) + ( + delayed_memory_report_id, + delayed_memory_report, + ) = self.get_delayed_memory_runtime_action_report( + action + ) + delayed_memory_title = str( + delayed_memory_report.get( + "title", + "", + ) + if delayed_memory_report + else "" + ).strip() + + if delayed_memory_title: + display_text = ( + f"{display_name}: " + f"{delayed_memory_title}" + ) has_close_tag = runtime_action_has_close_tag( action.name ) @@ -1737,6 +1819,16 @@ async def emit_started_runtime_actions( if action.payload and not has_close_tag: payload["payload"] = action.payload + if delayed_memory_report_id: + payload["delayed_memory_report_id"] = ( + delayed_memory_report_id + ) + + if delayed_memory_report: + payload["delayed_memory_report"] = ( + delayed_memory_report + ) + if action_context_snapshot: payload["context"] = action_context_snapshot diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 9bf6b7b6..a19445e3 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -16,6 +16,7 @@ get_runtime_action_private_marker, ) from rules.brain_context_builder import build_appended_delayed_memory_context +from runtime.stream import RuntimeStream from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -280,15 +281,11 @@ def test_extracts_delayed_memory_action_markers(self): self.assertEqual( result.text, - "", + "\n", ) self.assertEqual( result.actions, ( - RuntimeActionCall( - name="LIST_DELAYED_MEMORY", - payload="", - ), RuntimeActionCall( name="APPEND_DELAYED_MEMORY", payload="a1b2c3", @@ -828,9 +825,6 @@ def test_append_delayed_memory_uses_appended_context_block(self): apply_runtime_action_calls( context, ( - RuntimeActionCall( - name="LIST_DELAYED_MEMORY", - ), RuntimeActionCall( name="APPEND_DELAYED_MEMORY", payload="a1b2c3", @@ -841,30 +835,14 @@ def test_append_delayed_memory_uses_appended_context_block(self): self.assertEqual( applied_count, - 2, + 1, ) tool_results = build_tool_results_context( context ) - self.assertIn( - "", - tool_results, - ) - self.assertNotIn( - "', - tool_results, - ) - self.assertNotIn( - "", + self.assertEqual( tool_results, + "\n", ) appended_context = build_appended_delayed_memory_context( context @@ -879,10 +857,6 @@ def test_append_delayed_memory_uses_appended_context_block(self): ) self.assertEqual( context.emitter.events[0]["text"], - "Listing delayed memory", - ) - self.assertEqual( - context.emitter.events[1]["text"], ( "Appending: " + context.delayed_memory_reports[ @@ -892,7 +866,7 @@ def test_append_delayed_memory_uses_appended_context_block(self): ) self.assertEqual( len(context.emitter.events), - 2, + 1, ) self.assertEqual( context.runtime_session_action_history[0]["text"], @@ -905,7 +879,77 @@ def test_append_delayed_memory_uses_appended_context_block(self): ) - def test_append_delayed_memory_replaces_current_report(self): + def test_started_append_delayed_memory_events_are_report_scoped(self): + + context = SimpleNamespace( + emitter=FakeEmitter(), + delayed_memory_reports={ + "a1b2c3": { + "title": "First report", + "summary": "Summary", + "body": "Body", + }, + "b2c3d4": { + "title": "Second report", + "summary": "Summary", + "body": "Body", + }, + }, + runtime_active_action_markers=[], + runtime_current_turn_id="turn_000001", + ) + runtime_stream = RuntimeStream.__new__( + RuntimeStream + ) + runtime_stream.context = context + runtime_stream.context_snapshot = {} + runtime_stream.stream = SimpleNamespace( + message_id="message_000001", + ) + runtime_stream.started_delayed_memory_action_ids = [] + runtime_stream.jin_color_action_id = "" + + asyncio.run( + runtime_stream.emit_started_runtime_actions(( + RuntimeActionCall( + name="APPEND_DELAYED_MEMORY", + payload="a1b2c3", + ), + RuntimeActionCall( + name="APPEND_DELAYED_MEMORY", + payload="b2c3d4", + ), + )) + ) + + self.assertEqual( + [ + ( + event["id"], + event["text"], + event["delayed_memory_report_id"], + event["delayed_memory_report"]["title"], + ) + for event in context.emitter.events + ], + [ + ( + "a1b2c3", + "APPEND_DELAYED_MEMORY: First report", + "a1b2c3", + "First report", + ), + ( + "b2c3d4", + "APPEND_DELAYED_MEMORY: Second report", + "b2c3d4", + "Second report", + ), + ], + ) + + + def test_append_delayed_memory_keeps_multiple_reports(self): Emitter = FakeEmitter @@ -961,25 +1005,32 @@ def test_append_delayed_memory_replaces_current_report(self): 2, ) self.assertEqual( - context.runtime_appended_delayed_memory["id"], - "b2c3d4", + set( + context.runtime_appended_delayed_memory + ), + { + "a1b2c3", + "b2c3d4", + }, ) appended_context = build_appended_delayed_memory_context( context ) + self.assertEqual( + appended_context.count( + "" + ), + 2, + ) self.assertIn( - "", + '"title": "First report"', appended_context, ) self.assertIn( '"title": "Second report"', appended_context, ) - self.assertNotIn( - '"title": "First report"', - appended_context, - ) tool_results = build_tool_results_context( context @@ -1002,6 +1053,31 @@ def test_append_delayed_memory_replaces_current_report(self): "Delayed memory appended: Second report", ], ) + self.assertEqual( + [ + ( + event["id"], + event["text"], + event["delayed_memory_report_id"], + event["delayed_memory_report"]["title"], + ) + for event in context.emitter.events + ], + [ + ( + "a1b2c3", + "Appending: First report", + "a1b2c3", + "First report", + ), + ( + "b2c3d4", + "Appending: Second report", + "b2c3d4", + "Second report", + ), + ], + ) def test_invalid_append_delayed_memory_id_returns_failure_tool_result(self): @@ -1278,6 +1354,106 @@ def test_remove_delayed_memory_only_detaches_from_context(self): ) + def test_remove_delayed_memory_detaches_multiple_reports(self): + + Emitter = FakeEmitter + + Context = FakeContext + + context = Context() + context.emitter = Emitter() + context.runtime_action_events = [] + context.runtime_search_calls = [] + context.runtime_appended_skills = [] + context.runtime_asset_results = [] + context.runtime_appended_delayed_memory = { + "a1b2c3": { + "id": "a1b2c3", + "title": "First report", + }, + "b2c3d4": { + "id": "b2c3d4", + "title": "Second report", + }, + "c3d4e5": { + "id": "c3d4e5", + "title": "Third report", + }, + } + context.runtime_appended_delayed_memory_ids = [ + "a1b2c3", + "b2c3d4", + "c3d4e5", + ] + context.delayed_memory_reports = { + "a1b2c3": { + "title": "First report", + }, + "b2c3d4": { + "title": "Second report", + }, + "c3d4e5": { + "title": "Third report", + }, + } + + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="REMOVE_DELAYED_MEMORY", + payload="a1b2c3", + ), + RuntimeActionCall( + name="REMOVE_DELAYED_MEMORY", + payload="b2c3d4", + ), + ), + ) + ) + + self.assertEqual( + applied_count, + 2, + ) + self.assertEqual( + set( + context.runtime_appended_delayed_memory + ), + { + "c3d4e5", + }, + ) + self.assertEqual( + context.runtime_appended_delayed_memory_ids, + [ + "c3d4e5", + ], + ) + self.assertEqual( + set( + context.delayed_memory_reports + ), + { + "a1b2c3", + "b2c3d4", + "c3d4e5", + }, + ) + self.assertEqual( + [ + event["text"] + for event in context.emitter.events + ], + [ + "Removing: First report", + "Removing: Second report", + ], + ) + + + def test_invalid_remove_delayed_memory_id_returns_failed_result(self): Emitter = FakeEmitter diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index d14bd52d..3e37ae63 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -1736,9 +1736,9 @@ def test_recorded_tool_results_persist_then_append_in_order(self): context, TOOL_RESULT_KIND_DELAYED_MEMORY, { - "ok": True, - "action": "list_delayed_memory", - "reports": [], + "ok": False, + "action": "remove_delayed_memory", + "failure": "No entries found.", }, ) @@ -1872,7 +1872,7 @@ def test_clean_tool_results_action_clears_all_result_state(self): ] context.runtime_delayed_memory_results = [ { - "action": "list_delayed_memory", + "action": "append_delayed_memory", }, ] applied_count = asyncio.run( diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 391bd441..64be5617 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -1108,91 +1108,6 @@ async def run(self, generator): False, ) - async def test_list_delayed_memory_result_triggers_followup(self): - - calls = [] - - async def fake_run_brain_stream(**kwargs): - calls.append(kwargs) - context = kwargs["context"] - - if len(calls) == 1: - context.runtime_delayed_memory_results.append({ - "ok": True, - "action": "list_delayed_memory", - "reports": [ - { - "id": "dm_note", - "title": "Solo note", - }, - ], - }) - return "", "" - - if len(calls) == 2: - self.assertTrue( - kwargs["filter_runtime_actions"], - ) - self.assertTrue( - kwargs["runtime_actions"].get( - "CAN_SAVE_DELAYED_MEMORY" - ), - ) - _assert_latest_request_payload( - self, - kwargs, - state.translated_input, - "list_delayed_memory", - ) - return ( - "Found delayed memory `dm_note`: Solo note.", - "", - ) - - self.fail( - "Brain model kept running after delayed-memory answer" - ) - - context = _context() - state = AgentState( - user_input=( - "append delayed memory it is alone now" - ), - ) - state.translated_input = state.user_input - brain_runtime = _brain_runtime() - - with patch( - "agent.nodes.brain.get_brain_runtime_config", - return_value=brain_runtime, - ), patch( - "agent.nodes.brain.build_brain_context", - return_value="system prompt", - ), patch( - "agent.nodes.brain.build_brain_payload", - return_value="brain payload", - ), patch( - "agent.nodes.brain.emit_active_memory_records_update_if_dirty", - new=lambda _context: _async_noop(), - ), patch.object( - BrainNode, - "run_brain_stream", - staticmethod(fake_run_brain_stream), - ): - await BrainNode().run( - state, - context, - ) - - self.assertEqual( - len(calls), - 2, - ) - self.assertEqual( - state.brain_response, - "Found delayed memory `dm_note`: Solo note.", - ) - async def test_failed_delayed_memory_save_triggers_followup_with_payload(self): calls = [] diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index a9d5e481..70c2e8be 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -118,7 +118,6 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: expected_actions.extend( ( "SAVE_DELAYED_MEMORY_CONTENT", - "LIST_DELAYED_MEMORY", "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", ) @@ -3261,7 +3260,7 @@ def test_prompt_keeps_appended_delayed_memory_in_normal_turns(self): ), ) - def test_prompt_adds_delayed_memory_rules_only_when_reports_exist(self): + def test_prompt_lists_available_delayed_memory_below_session_state(self): empty_context = SimpleNamespace( runtime_memory="session_status: active", @@ -3282,6 +3281,10 @@ def test_prompt_adds_delayed_memory_rules_only_when_reports_exist(self): "DELAYED MEMORY ACTIONS:", prompt_without_reports, ) + self.assertNotIn( + "", + prompt_without_reports, + ) context = SimpleNamespace( runtime_memory="session_status: active", @@ -3289,8 +3292,17 @@ def test_prompt_adds_delayed_memory_rules_only_when_reports_exist(self): runtime_l2_memory="", active_memory_records=[], delayed_memory_reports={ - "a1b2c3": { - "title": "Saved report", + "3gs007": { + "title": "JIN Multi-Layered Memory Architecture", + }, + "1put0q": { + "title": ( + "Синтез: Интеллект как контролируемый хаос " + "(Эволюция через ошибку)" + ), + }, + "bad": { + "title": "Invalid id", }, }, ) @@ -3302,14 +3314,58 @@ def test_prompt_adds_delayed_memory_rules_only_when_reports_exist(self): }, ) + expected_inventory = ( + "\n" + "1put0q_Синтез_Интеллект_как_контролируемый_хаос_" + "Эволюция_через_ошибку\n" + "3gs007_JIN_Multi_Layered_Memory_Architecture\n" + "" + ) + self.assertIn( "DELAYED MEMORY ACTIONS:", prompt, ) self.assertIn( + "", + prompt, + ) + self.assertIn( + "", + prompt, + ) + self.assertNotIn( "", prompt, ) + self.assertEqual( + prompt.count( + "\n\n" + ), + 1, + ) + self.assertIn( + "\n" + + expected_inventory, + prompt, + ) + self.assertLess( + prompt.index( + expected_inventory + ), + prompt.index( + "RUNTIME ACTION EXECUTION RULES:" + ), + ) + self.assertFalse( + prompt.rstrip().endswith( + expected_inventory + ), + ) + self.assertNotIn( + "bad_Invalid_id", + prompt, + ) def test_prompt_formats_missing_skill_as_skill_error_tool_result(self): diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 0fadd3a5..8b7dcc36 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -81,6 +81,88 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): runtime_actions_source, ) + def test_appended_delayed_memory_bubbles_open_their_own_reports(self): + + runtime_actions_source = ( + ROOT + / "ui" + / "static" + / "js" + / "socket" + / "runtime-actions.js" + ).read_text( + encoding="utf-8" + ) + chat_actions_source = ( + ROOT + / "ui" + / "static" + / "js" + / "chat-runtime-actions.js" + ).read_text( + encoding="utf-8" + ) + attachments_source = ( + ROOT + / "ui" + / "static" + / "js" + / "chat-attachments.js" + ).read_text( + encoding="utf-8" + ) + + self.assertIn( + "function getDelayedMemoryRuntimeActionPreview", + runtime_actions_source, + ) + self.assertIn( + "delayedMemoryResult.report", + runtime_actions_source, + ) + self.assertIn( + "delayedMemoryResult.id", + runtime_actions_source, + ) + self.assertIn( + "function normalizeDelayedMemoryRuntimeActionId", + runtime_actions_source, + ) + self.assertIn( + "reportScopedDelayedAction", + runtime_actions_source, + ) + self.assertIn( + "delayedMemoryPreview.reportId", + runtime_actions_source, + ) + self.assertIn( + "delayedMemoryPreview.title", + runtime_actions_source, + ) + self.assertIn( + "counterOnly\n && reportScopedDelayedAction", + runtime_actions_source, + ) + self.assertIn( + '"append_delayed_memory",', + chat_actions_source, + ) + self.assertGreaterEqual( + chat_actions_source.count( + "bindDelayedMemoryReportPreview(" + ), + 2, + ) + self.assertIn( + "function getDelayedMemoryReportPreviewSource", + attachments_source, + ) + self.assertIn( + "reports[requestedId]", + attachments_source, + ) + def test_runtime_action_detail_ignores_generic_marker_payload_titles(self): runtime_actions_source = ( diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py new file mode 100644 index 00000000..93d8e87b --- /dev/null +++ b/tests/test_delayed_memory_file_store.py @@ -0,0 +1,271 @@ +import json +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace + +from utils.delayed_memory_file_store import ( + build_delayed_memory_file_payload, + load_delayed_memory_reports_from_files, + merge_delayed_memory_reports, + persist_delayed_memory_report, +) +from websocket.bootstrap import apply_delayed_memory_reports + + +class DelayedMemoryFileStoreTests(unittest.TestCase): + + def build_report( + self, + *, + title: str = "Внутреннее эхо JIN", + body: str = "Body", + ) -> dict: + + return { + "title": title, + "summary": "Summary", + "tags": [ + "identity", + "evolution", + ], + "body": body, + "created_session_id": "session-a", + "created_time": "2026-07-19T16:14:14.628194", + "created_date": "2026-07-19T16:14:14.628194", + "all_appended_session_ids": [ + "session-a", + ], + } + + def test_persist_and_load_direct_report_shape(self): + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + path = persist_delayed_memory_report( + "48ggds", + self.build_report(), + root=root, + ) + + self.assertEqual( + path.name, + "48ggds_Внутреннее_эхо_JIN.json", + ) + + payload = json.loads( + path.read_text( + encoding="utf-8", + ) + ) + + self.assertEqual( + payload["id"], + "48ggds", + ) + self.assertEqual( + payload["session"], + "session-a", + ) + self.assertEqual( + payload["time"], + "2026-07-19T16:14:14.628194", + ) + + reports, warnings = ( + load_delayed_memory_reports_from_files( + root=root, + ) + ) + + self.assertEqual( + warnings, + [], + ) + self.assertEqual( + reports["48ggds"]["title"], + "Внутреннее эхо JIN", + ) + self.assertEqual( + reports["48ggds"]["created_session_id"], + "session-a", + ) + + def test_save_replaces_old_filename_for_same_id(self): + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + + persist_delayed_memory_report( + "48ggds", + self.build_report( + title="Old title", + ), + root=root, + ) + path = persist_delayed_memory_report( + "48ggds", + self.build_report( + title="New title", + ), + root=root, + ) + + self.assertEqual( + path.name, + "48ggds_New_title.json", + ) + self.assertEqual( + [ + item.name + for item in root.glob("48ggds_*.json") + ], + [ + "48ggds_New_title.json", + ], + ) + + def test_invalid_files_are_skipped_without_crashing(self): + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + root.joinpath("broken.json").write_text( + "{not json", + encoding="utf-8", + ) + root.joinpath("invalid_report.json").write_text( + json.dumps({ + "id": "invalid", + "title": "Bad report", + }), + encoding="utf-8", + ) + persist_delayed_memory_report( + "48ggds", + self.build_report(), + root=root, + ) + + reports, warnings = ( + load_delayed_memory_reports_from_files( + root=root, + ) + ) + + self.assertEqual( + set(reports), + { + "48ggds", + }, + ) + self.assertEqual( + len(warnings), + 2, + ) + self.assertTrue( + any( + "broken.json" in warning + for warning in warnings + ) + ) + self.assertTrue( + any( + "invalid_report.json" in warning + for warning in warnings + ) + ) + + def test_local_storage_reports_override_file_fallback_by_id(self): + + file_reports = { + "48ggds": self.build_report( + title="File title", + ), + "a1b2c3": self.build_report( + title="File only", + ), + } + browser_reports = { + "48ggds": self.build_report( + title="Browser title", + ), + } + + merged = merge_delayed_memory_reports( + browser_reports, + file_reports, + ) + + self.assertEqual( + merged["48ggds"]["title"], + "Browser title", + ) + self.assertEqual( + merged["a1b2c3"]["title"], + "File only", + ) + + def test_websocket_sync_merges_browser_primary_with_file_fallback(self): + + context = SimpleNamespace( + delayed_memory_reports={ + "48ggds": self.build_report( + title="File title", + ), + "a1b2c3": self.build_report( + title="File only", + ), + }, + ) + + apply_delayed_memory_reports( + context, + { + "delayed_memory_reports": { + "48ggds": self.build_report( + title="Browser title", + ), + }, + }, + ) + + self.assertEqual( + context.delayed_memory_reports["48ggds"]["title"], + "Browser title", + ) + self.assertEqual( + context.delayed_memory_reports["a1b2c3"]["title"], + "File only", + ) + + def test_file_payload_accepts_example_aliases(self): + + payload = build_delayed_memory_file_payload( + "48ggds", + { + "title": "Внутреннее эхо JIN", + "summary": "Summary", + "time": "2026-07-19 16:14, Sunday", + "tags": [ + "identity", + ], + "id": "48ggds", + "session": "session-a", + "created_date": "2026-07-19T16:14:14.628194", + "all_appended_session_ids": [], + "body": "Body", + }, + ) + + self.assertEqual( + payload["time"], + "2026-07-19 16:14, Sunday", + ) + self.assertEqual( + payload["session"], + "session-a", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/js/chat-attachments.js b/ui/static/js/chat-attachments.js index 01083f93..2b129b5d 100644 --- a/ui/static/js/chat-attachments.js +++ b/ui/static/js/chat-attachments.js @@ -857,6 +857,56 @@ function normalizeDelayedMemoryReportForModal( } +function getDelayedMemoryReportPreviewSource( + delayedMemoryReport, + delayedMemoryReportId = "" +) { + + if ( + delayedMemoryReport + && typeof delayedMemoryReport === "object" + && !Array.isArray(delayedMemoryReport) + ) { + return delayedMemoryReport; + } + + const requestedId = + String( + delayedMemoryReportId || "" + ).trim(); + + if ( + !requestedId + || !window.JinRuntime + || !window.JinRuntime.runtime + || !window.JinRuntime.runtime.getDelayedMemoryReports + ) { + return null; + } + + const reports = + window.JinRuntime.runtime.getDelayedMemoryReports(); + const report = + reports + && typeof reports === "object" + && !Array.isArray(reports) + ? reports[requestedId] + : null; + + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return null; + } + + return { + [requestedId]: report, + }; + +} + function bindDelayedMemoryReportPreview( element, delayedMemoryReport, @@ -869,7 +919,10 @@ function bindDelayedMemoryReportPreview( const report = normalizeDelayedMemoryReportForModal( - delayedMemoryReport, + getDelayedMemoryReportPreviewSource( + delayedMemoryReport, + delayedMemoryReportId + ), delayedMemoryReportId ); diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 7cabc970..75a44804 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -1746,7 +1746,12 @@ function updateRuntimeActionRow( ); } - if (action === "save_delayed_memory_content") { + if ( + [ + "save_delayed_memory_content", + "append_delayed_memory", + ].includes(action) + ) { if ( options.delayedMemoryReport || options.delayedMemoryReportId @@ -2374,7 +2379,12 @@ function appendRuntimeAction( ); } - if (action === "save_delayed_memory_content") { + if ( + [ + "save_delayed_memory_content", + "append_delayed_memory", + ].includes(action) + ) { if ( options.delayedMemoryReport || options.delayedMemoryReportId diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index 9fc11f00..72eb179d 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -373,3 +373,36 @@ function syncDelayedMemoryReportsToRuntime() { } + +function handleDelayedMemoryStoreSnapshot( + data +) { + + if ( + !data + || !data.delayed_memory_reports + || typeof data.delayed_memory_reports !== "object" + || Array.isArray(data.delayed_memory_reports) + || !window.JinRuntime + || !window.JinRuntime.runtime + || !window.JinRuntime.runtime.getDelayedMemoryReports + || !window.JinRuntime.runtime.replaceDelayedMemoryReports + ) { + return; + } + + const localReports = + window.JinRuntime.runtime.getDelayedMemoryReports(); + + window.JinRuntime.runtime.replaceDelayedMemoryReports({ + ...data.delayed_memory_reports, + ...localReports, + }); + +} + + +registerSocketMessageHandler( + "delayed_memory_store_snapshot", + handleDelayedMemoryStoreSnapshot +); diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 92330a84..c4067a75 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -635,6 +635,86 @@ function shouldSplitPayloadDistinctRuntimeAction( } +function normalizeDelayedMemoryRuntimeActionId( + value +) { + + const reportId = String( + value || "" + ).trim().toLowerCase(); + + return /^[a-z0-9]{6}$/.test(reportId) + ? reportId + : ""; + +} + +function getDelayedMemoryRuntimeActionPreview( + data, + action = "" +) { + + const delayedMemoryResult = + data + && data.delayed_memory_result + && typeof data.delayed_memory_result === "object" + && !Array.isArray(data.delayed_memory_result) + ? data.delayed_memory_result + : null; + const report = + data.delayed_memory_report + || ( + delayedMemoryResult + ? delayedMemoryResult.report + : null + ) + || null; + const reportId = + normalizeDelayedMemoryRuntimeActionId( + data.delayed_memory_report_id + || ( + delayedMemoryResult + ? delayedMemoryResult.id + : "" + ) + || ( + report + && typeof report === "object" + && !Array.isArray(report) + ? report.id + : "" + ) + || ( + [ + "append_delayed_memory", + "remove_delayed_memory", + ].includes(action) + ? data.payload + : "" + ) + || "" + ); + const title = String( + report + && typeof report === "object" + && !Array.isArray(report) + ? report.title || "" + : ( + delayedMemoryResult + && typeof delayedMemoryResult === "object" + ? delayedMemoryResult.title || "" + : "" + ) + ).trim(); + + return { + report, + reportId, + title, + }; + +} + function handleRuntimeAction( data ) { @@ -656,6 +736,19 @@ function handleRuntimeAction( const runtimeMessageId = getRuntimeActionMessageId(data); + const delayedMemoryPreview = + getDelayedMemoryRuntimeActionPreview( + data, + action + ); + const reportScopedDelayedAction = + [ + "append_delayed_memory", + "remove_delayed_memory", + ].includes(action) + && Boolean( + delayedMemoryPreview.reportId + ); const text = String( @@ -721,7 +814,7 @@ function handleRuntimeAction( ); } - const displayText = + const baseDisplayText = action === "resolve_active_memory" ? buildResolveActiveMemoryRuntimeActionText( data, @@ -741,6 +834,15 @@ function handleRuntimeAction( } ); + const displayText = + reportScopedDelayedAction + && delayedMemoryPreview.title + ? ( + `${getRuntimeActionDisplayName(data, action)}: ` + + delayedMemoryPreview.title + ) + : baseDisplayText; + const displayName = getRuntimeActionDisplayName( data, @@ -799,7 +901,8 @@ function handleRuntimeAction( ); const aggregateMarkers = - !splitPayloadDistinctMarkers + !reportScopedDelayedAction + && !splitPayloadDistinctMarkers && ( data.aggregate_markers === true || counterOnly @@ -831,9 +934,18 @@ function handleRuntimeAction( && PAYLOAD_DISTINCT_RUNTIME_ACTIONS.has(action); const actionDisplayId = - data.counter_id - || data.id - || ""; + reportScopedDelayedAction + ? ( + delayedMemoryPreview.reportId + || data.id + || data.counter_id + || "" + ) + : ( + data.counter_id + || data.id + || "" + ); const pendingUntilL3 = action === "save_session" @@ -861,6 +973,13 @@ function handleRuntimeAction( status ); + if ( + counterOnly + && reportScopedDelayedAction + ) { + return; + } + if (action === "jin_color") { const color = String( @@ -1062,9 +1181,9 @@ function handleRuntimeAction( assetResult: data.asset_result || null, delayedMemoryReportId: - data.delayed_memory_report_id || "", + delayedMemoryPreview.reportId, delayedMemoryReport: - data.delayed_memory_report || null, + delayedMemoryPreview.report, completed: !aggregateMarkers || completeImmediately, @@ -1159,6 +1278,10 @@ function handleRuntimeAction( data.context || null, assetResult: data.asset_result || null, + delayedMemoryReportId: + delayedMemoryPreview.reportId, + delayedMemoryReport: + delayedMemoryPreview.report, detail: runtimeDetail, displayName, sceneEffect, diff --git a/ui/templates/index.html b/ui/templates/index.html index 779ec502..6018a826 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -519,12 +519,12 @@ - - + + - + diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 2c21ba07..3ad2be60 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -11,7 +11,6 @@ RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, - RUNTIME_ACTION_LIST_DELAYED_MEMORY, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, @@ -305,7 +304,6 @@ def normalize_runtime_action_name( "SAVE_SESSION": RUNTIME_ACTION_SAVE_SESSION, "SAVE_DELAYED_MEMORY": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, "SAVE_DELAYED_MEMORY_CONTENT": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, - "LIST_DELAYED_MEMORY": RUNTIME_ACTION_LIST_DELAYED_MEMORY, "APPEND_DELAYED_MEMORY": RUNTIME_ACTION_APPEND_DELAYED_MEMORY, "REMOVE_DELAYED_MEMORY": RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, "SAVE_ACTIVE_MEMORY": RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, @@ -371,9 +369,6 @@ def normalize_runtime_action_names( ) if normalized_name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: - normalized_names.append( - RUNTIME_ACTION_LIST_DELAYED_MEMORY - ) normalized_names.append( RUNTIME_ACTION_APPEND_DELAYED_MEMORY ) @@ -439,14 +434,6 @@ def _build_internal_action_call( if normalized_name not in KNOWN_RUNTIME_ACTIONS: return None - if normalized_name in { - RUNTIME_ACTION_LIST_DELAYED_MEMORY, - }: - return RuntimeActionCall( - name=normalized_name, - payload="", - ) - if normalized_name == RUNTIME_ACTION_LIST_SKILLS: return RuntimeActionCall( name=normalized_name, diff --git a/utils/actions/delayed_memory_actions.py b/utils/actions/delayed_memory_actions.py index c618210c..59ae7cae 100644 --- a/utils/actions/delayed_memory_actions.py +++ b/utils/actions/delayed_memory_actions.py @@ -16,7 +16,6 @@ async def apply_delayed_memory_actions( context, *, - list_delayed_memory_actions, append_delayed_memory_actions, remove_delayed_memory_actions, log_runtime, @@ -29,35 +28,12 @@ async def apply_delayed_memory_actions( clear_appended_delayed_memory_report, clear_delayed_memory_runtime_results, get_delayed_memory_reports, - list_delayed_memory_reports, remove_delayed_memory_report, set_appended_delayed_memory_report, ) delayed_memory_results = [] - if list_delayed_memory_actions: - if log_runtime is not None: - await log_runtime( - "[RUNTIME ACTION] list_delayed_memory requested" - ) - - clear_delayed_memory_runtime_results( - context - ) - - for action in list_delayed_memory_actions: - result = list_delayed_memory_reports( - context - ) - append_delayed_memory_runtime_result( - context, - result, - ) - delayed_memory_results.append( - result - ) - if append_delayed_memory_actions: if log_runtime is not None: await log_runtime( @@ -216,12 +192,25 @@ async def emit_delayed_memory_results( ) or "delayed_memory" ) - action_id = build_runtime_action_id( - result_action, - first_delayed_result_index - + result_index, + report_id = str( + result.get( + "id", + "", + ) + or "" + ).strip().casefold() + report = result.get( + "report", ) - await emit(with_action_context({ + action_id = ( + report_id + or build_runtime_action_id( + result_action, + first_delayed_result_index + + result_index, + ) + ) + event = { "type": "runtime_action", "action": result_action, "id": action_id, @@ -240,6 +229,31 @@ async def emit_delayed_memory_results( result ), "delayed_memory_result": result, + } + + if report_id: + event["delayed_memory_report_id"] = ( + report_id + ) + + if isinstance( + report, + dict, + ): + event["delayed_memory_report"] = { + **report, + "id": report_id + or str( + report.get( + "id", + "", + ) + or "" + ).strip().casefold(), + } + + await emit(with_action_context({ + **event, })) @@ -302,6 +316,30 @@ async def apply_save_delayed_memory_actions( report ) + file_errors = [] + + if bool( + getattr( + context, + "delayed_memory_file_store_enabled", + False, + ) + ): + from utils.delayed_memory_file_store import ( + persist_delayed_memory_reports, + ) + + file_errors = persist_delayed_memory_reports( + report + ) + + if file_errors and log_runtime is not None: + for file_error in file_errors: + await log_runtime( + "[DELAYED MEMORY] local file save failed: " + + file_error + ) + saved_delayed_memory_reports.append( report ) @@ -353,6 +391,8 @@ async def apply_save_delayed_memory_actions( **report_value, "id": report_id, }, + "file_saved": not file_errors, + "file_errors": list(file_errors), } record_runtime_tool_result( context, diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index c9e81889..e52a0a91 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -5,7 +5,6 @@ RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_LIST_DELAYED_MEMORY, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, @@ -225,7 +224,6 @@ def with_action_context(payload: dict) -> dict: resolve_active_memory_ids_seen = set() resolve_active_memory_failures_seen = set() save_delayed_memory_seen = set() - list_delayed_memory_seen = False list_skills_seen = False resolved_user_message = resolve_runtime_action_user_message( context, @@ -718,24 +716,6 @@ def accept_runtime_action_once_per_message( ) continue - if action.name == RUNTIME_ACTION_LIST_DELAYED_MEMORY: - if list_delayed_memory_seen: - continue - - if not accept_runtime_action_once_per_message( - action - ): - continue - - list_delayed_memory_seen = True - accepted_action_names.add( - action_event_name - ) - filtered_actions.append( - action - ) - continue - if action.name == RUNTIME_ACTION_APPEND_DELAYED_MEMORY: if not accept_runtime_action_once_per_message( action @@ -1270,12 +1250,6 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT ] - list_delayed_memory_actions = [ - action - for action in filtered_actions - if action.name == RUNTIME_ACTION_LIST_DELAYED_MEMORY - ] - append_delayed_memory_actions = [ action for action in filtered_actions @@ -1471,7 +1445,6 @@ def accept_runtime_action_once_per_message( delayed_memory_results = await apply_delayed_memory_actions( context, - list_delayed_memory_actions=list_delayed_memory_actions, append_delayed_memory_actions=append_delayed_memory_actions, remove_delayed_memory_actions=remove_delayed_memory_actions, log_runtime=log_runtime, diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index ff404a92..6624ef78 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -67,7 +67,6 @@ def get_brain_runtime_config(): RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_LIST_DELAYED_MEMORY, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, @@ -1624,24 +1623,77 @@ def get_appended_delayed_memory_report( context, ) -> dict: - appended_report = getattr( + appended_reports = getattr( context, "runtime_appended_delayed_memory", None, ) if not isinstance( - appended_report, + appended_reports, dict, ): - appended_report = {} - setattr( - context, - "runtime_appended_delayed_memory", - appended_report, + appended_reports = {} + + legacy_report_id = str( + appended_reports.get( + "id", + "", + ) + or "" + ).strip().casefold() + + if ( + legacy_report_id + and is_delayed_memory_report_id( + legacy_report_id ) + and ( + "title" in appended_reports + or "body" in appended_reports + or "summary" in appended_reports + ) + ): + appended_reports = { + legacy_report_id: { + **appended_reports, + "id": legacy_report_id, + }, + } + else: + normalized_reports = {} - return appended_report + for report_id, report in appended_reports.items(): + normalized_report_id = str( + report_id + or "" + ).strip().casefold() + + if ( + not is_delayed_memory_report_id( + normalized_report_id + ) + or not isinstance( + report, + dict, + ) + ): + continue + + normalized_reports[normalized_report_id] = { + **report, + "id": normalized_report_id, + } + + appended_reports = normalized_reports + + setattr( + context, + "runtime_appended_delayed_memory", + appended_reports, + ) + + return appended_reports def set_appended_delayed_memory_report( @@ -1680,31 +1732,23 @@ def set_appended_delayed_memory_report( or "" ).strip().casefold() - if not report_id: + if not is_delayed_memory_report_id( + report_id + ): return False - current_report = get_appended_delayed_memory_report( + appended_reports = get_appended_delayed_memory_report( context ) - current_id = str( - current_report.get( - "id", - "", - ) - or "" - ).strip().casefold() - if current_id == report_id: + if report_id in appended_reports: return False - setattr( - context, - "runtime_appended_delayed_memory", - { - **report, - "id": report_id, - }, - ) + appended_reports[report_id] = { + **report, + "id": report_id, + } + return True @@ -1713,40 +1757,43 @@ def clear_appended_delayed_memory_report( report_id: str = "", ) -> bool: - current_report = get_appended_delayed_memory_report( - context - ) - - if not current_report: - return False - normalized_report_id = str( report_id or "" ).strip().casefold() - current_id = str( - current_report.get( - "id", - "", - ) - or "" - ).strip().casefold() - - if ( + if not is_delayed_memory_report_id( normalized_report_id - and current_id - and normalized_report_id != current_id ): return False - setattr( + appended_reports = get_appended_delayed_memory_report( + context + ) + + if normalized_report_id not in appended_reports: + return False + + del appended_reports[normalized_report_id] + + appended_ids = getattr( context, - "runtime_appended_delayed_memory", - {}, + "runtime_appended_delayed_memory_ids", + None, ) - return True + if isinstance( + appended_ids, + list, + ): + appended_ids[:] = [ + item + for item in appended_ids + if str(item or "").strip().casefold() + != normalized_report_id + ] + + return True def get_delayed_memory_reports( context, @@ -1808,37 +1855,6 @@ def build_delayed_memory_failure_result( } -def list_delayed_memory_reports( - context, -) -> dict: - - reports = get_delayed_memory_reports( - context - ) - - return { - "ok": True, - "action": "list_delayed_memory", - "reports": [ - { - "id": report_id, - "title": str( - report.get( - "title", - "", - ) - or "" - ).strip(), - } - for report_id, report in reports.items() - if isinstance( - report, - dict, - ) - ], - } - - def append_delayed_memory_report( context, payload: str, @@ -1879,6 +1895,23 @@ def append_delayed_memory_report( report_id, ) + file_errors = [] + + if bool( + getattr( + context, + "delayed_memory_file_store_enabled", + False, + ) + ): + from utils.delayed_memory_file_store import ( + persist_delayed_memory_reports, + ) + + file_errors = persist_delayed_memory_reports({ + report_id: updated_report, + }) + return { "ok": True, "action": "append_delayed_memory", @@ -1894,6 +1927,8 @@ def append_delayed_memory_report( **updated_report, "id": report_id, }, + "file_saved": not file_errors, + "file_errors": file_errors, } @@ -1976,9 +2011,6 @@ def build_delayed_memory_action_text( or "" ) - if action == "list_delayed_memory": - return "Listing delayed memory" - title = str( result.get( "title", diff --git a/utils/context/context_exports.py b/utils/context/context_exports.py index b14c540e..9839d882 100644 --- a/utils/context/context_exports.py +++ b/utils/context/context_exports.py @@ -36,7 +36,6 @@ format_asset_result_sections, ) from .delayed_memory import ( - format_delayed_memory_list_result, format_delayed_memory_report_result, format_delayed_memory_result_sections, ) @@ -59,7 +58,6 @@ "format_active_memory_result_sections", "format_asset_result_sections", "format_context_message_age_suffix", - "format_delayed_memory_list_result", "format_delayed_memory_report_result", "format_delayed_memory_result_sections", "format_list_skills_result", diff --git a/utils/context/delayed_memory.py b/utils/context/delayed_memory.py index 82830ee7..fc269423 100644 --- a/utils/context/delayed_memory.py +++ b/utils/context/delayed_memory.py @@ -1,67 +1,9 @@ # Formats delayed memory tool results and appended delayed memory context blocks. -from rules.runtime import ( - NO_ENTRIES_FOUND_MESSAGE, -) - from .formatting import ( format_tool_result_payload, ) -def format_delayed_memory_list_result( - result: dict, -) -> str: - - reports = [ - report - for report in result.get( - "reports", - [], - ) - or [] - if isinstance( - report, - dict, - ) - ] - - if not reports: - return NO_ENTRIES_FOUND_MESSAGE - - lines = [] - - for index, report in enumerate( - reports, - start=1, - ): - title = str( - report.get( - "title", - "", - ) - or "" - ).strip() - - if not title: - title = "Untitled delayed memory" - - report_id = str( - report.get( - "id", - "", - ) - or "" - ).strip() - - lines.append( - f"{index}. {title} | id: {report_id}" - ) - - return "\n".join( - lines - ) - - def format_delayed_memory_report_result( result: dict, ) -> str: @@ -148,17 +90,6 @@ def format_delayed_memory_result_sections( or "" ) - if action == "list_delayed_memory": - sections.append( - ( - "LIST_DELAYED_MEMORY", - format_delayed_memory_list_result( - result - ), - ) - ) - continue - if action == "append_delayed_memory": if result.get("ok") is False: sections.append( diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py new file mode 100644 index 00000000..69ecaf5f --- /dev/null +++ b/utils/delayed_memory_file_store.py @@ -0,0 +1,480 @@ +from __future__ import annotations + +import json +import os +import re +from pathlib import Path +from tempfile import NamedTemporaryFile + +from utils.actions.delayed_memory_utils import ( + is_delayed_memory_report_id, +) + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DELAYED_MEMORY_ROOT = PROJECT_ROOT / "memory" / "delayed" +MAX_DELAYED_MEMORY_TITLE_CHARS = 500 +MAX_DELAYED_MEMORY_SUMMARY_CHARS = 2000 +MAX_DELAYED_MEMORY_BODY_CHARS = 12000 +MAX_DELAYED_MEMORY_TAGS = 30 +MAX_DELAYED_MEMORY_TAG_CHARS = 80 +MAX_DELAYED_MEMORY_SESSION_ID_CHARS = 200 +MAX_DELAYED_MEMORY_TIME_CHARS = 100 + + +def _clean_text( + value, + *, + limit: int, +) -> str: + + if value is None: + return "" + + cleaned = str(value).replace( + "\x00", + "", + ).strip() + + if len(cleaned) <= limit: + return cleaned + + return cleaned[-limit:].strip() + + +def _clean_counter(value) -> int: + + try: + return max( + int(value or 0), + 0, + ) + except (TypeError, ValueError): + return 0 + + +def _clean_session_ids(value) -> list[str]: + + source = value if isinstance(value, list) else [] + cleaned = [] + seen = set() + + for item in source: + session_id = _clean_text( + item, + limit=MAX_DELAYED_MEMORY_SESSION_ID_CHARS, + ) + + if not session_id or session_id in seen: + continue + + seen.add(session_id) + cleaned.append(session_id) + + return cleaned + + +def _clean_tags(value) -> list[str]: + + source = ( + value + if isinstance(value, list) + else str(value or "").split(",") + ) + tags = [] + + for item in source: + tag = _clean_text( + item, + limit=MAX_DELAYED_MEMORY_TAG_CHARS, + ) + + if not tag: + continue + + tags.append(tag) + + if len(tags) >= MAX_DELAYED_MEMORY_TAGS: + break + + return tags + + +def normalize_delayed_memory_report( + report_id: str, + report, +) -> tuple[str, dict] | None: + + if not isinstance(report, dict): + return None + + normalized_id = str( + report.get("id", "") + or report_id + or "" + ).strip().casefold() + + if not is_delayed_memory_report_id(normalized_id): + return None + + title = _clean_text( + report.get("title", ""), + limit=MAX_DELAYED_MEMORY_TITLE_CHARS, + ) + + if not title: + return None + + created_time = _clean_text( + report.get("created_time", "") + or report.get("time", ""), + limit=MAX_DELAYED_MEMORY_TIME_CHARS, + ) + created_date = _clean_text( + report.get("created_date", "") + or created_time, + limit=MAX_DELAYED_MEMORY_TIME_CHARS, + ) + + return normalized_id, { + "title": title, + "summary": _clean_text( + report.get("summary", ""), + limit=MAX_DELAYED_MEMORY_SUMMARY_CHARS, + ), + "tags": _clean_tags( + report.get("tags", []), + ), + "body": _clean_text( + report.get("body", ""), + limit=MAX_DELAYED_MEMORY_BODY_CHARS, + ), + "created_session_id": _clean_text( + report.get("created_session_id", "") + or report.get("session", ""), + limit=MAX_DELAYED_MEMORY_SESSION_ID_CHARS, + ), + "created_time": created_time, + "created_date": created_date, + "appended_times": _clean_counter( + report.get("appended_times", 0), + ), + "append_streak": _clean_counter( + report.get("append_streak", 0), + ), + "last_appended_date": _clean_text( + report.get("last_appended_date", ""), + limit=MAX_DELAYED_MEMORY_TIME_CHARS, + ), + "last_appended_session_id": _clean_text( + report.get("last_appended_session_id", ""), + limit=MAX_DELAYED_MEMORY_SESSION_ID_CHARS, + ), + "all_appended_session_ids": _clean_session_ids( + report.get("all_appended_session_ids", []), + ), + } + + +def normalize_delayed_memory_reports(value) -> dict[str, dict]: + + if not isinstance(value, dict): + return {} + + if ( + "title" in value + and ( + "id" in value + or "body" in value + or "summary" in value + ) + ): + candidates = [ + ( + str(value.get("id", "") or ""), + value, + ) + ] + else: + candidates = list(value.items()) + + reports = {} + + for key, report in candidates: + normalized = normalize_delayed_memory_report( + str(key or ""), + report, + ) + + if normalized is None: + continue + + report_id, clean_report = normalized + + if report_id in reports: + continue + + reports[report_id] = clean_report + + return reports + + +def merge_delayed_memory_reports( + primary, + fallback, +) -> dict[str, dict]: + + fallback_reports = normalize_delayed_memory_reports( + fallback, + ) + primary_reports = normalize_delayed_memory_reports( + primary, + ) + + return { + **fallback_reports, + **primary_reports, + } + + +def delayed_memory_filename( + report_id: str, + title: str, +) -> str: + + normalized_id = str(report_id or "").strip().casefold() + + if not is_delayed_memory_report_id(normalized_id): + raise ValueError("invalid delayed memory id") + + clean_title = re.sub( + r"[^\w]+", + "_", + str(title or "").strip(), + flags=re.UNICODE, + ) + clean_title = re.sub( + r"_+", + "_", + clean_title, + ).strip("_") + + if not clean_title: + clean_title = "delayed_memory" + + return f"{normalized_id}_{clean_title}.json" + + +def build_delayed_memory_file_payload( + report_id: str, + report: dict, +) -> dict: + + normalized = normalize_delayed_memory_report( + report_id, + report, + ) + + if normalized is None: + raise ValueError("invalid delayed memory report") + + normalized_id, clean_report = normalized + + return { + "title": clean_report["title"], + "summary": clean_report["summary"], + "time": clean_report["created_time"], + "tags": clean_report["tags"], + "id": normalized_id, + "session": clean_report["created_session_id"], + "created_date": clean_report["created_date"], + "all_appended_session_ids": clean_report[ + "all_appended_session_ids" + ], + "body": clean_report["body"], + "appended_times": clean_report["appended_times"], + "append_streak": clean_report["append_streak"], + "last_appended_date": clean_report[ + "last_appended_date" + ], + "last_appended_session_id": clean_report[ + "last_appended_session_id" + ], + } + + +def persist_delayed_memory_report( + report_id: str, + report: dict, + *, + root: Path | str = DELAYED_MEMORY_ROOT, +) -> Path: + + root_path = Path(root) + payload = build_delayed_memory_file_payload( + report_id, + report, + ) + filename = delayed_memory_filename( + payload["id"], + payload["title"], + ) + destination = root_path / filename + + root_path.mkdir( + parents=True, + exist_ok=True, + ) + + serialized = json.dumps( + payload, + ensure_ascii=False, + indent=2, + ) + "\n" + + temporary_name = "" + + try: + with NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + prefix=f".{payload['id']}_", + suffix=".tmp", + dir=root_path, + delete=False, + ) as temporary_file: + temporary_file.write(serialized) + temporary_file.flush() + os.fsync(temporary_file.fileno()) + temporary_name = temporary_file.name + + os.replace( + temporary_name, + destination, + ) + finally: + if temporary_name: + temporary_path = Path(temporary_name) + if temporary_path.exists(): + temporary_path.unlink() + + for candidate in root_path.glob( + f"{payload['id']}_*.json" + ): + if candidate == destination: + continue + + try: + candidate.unlink() + except OSError: + pass + + legacy_candidate = root_path / f"{payload['id']}.json" + + if legacy_candidate != destination and legacy_candidate.exists(): + try: + legacy_candidate.unlink() + except OSError: + pass + + return destination + + +def persist_delayed_memory_reports( + reports, + *, + root: Path | str = DELAYED_MEMORY_ROOT, +) -> list[str]: + + normalized_reports = normalize_delayed_memory_reports( + reports, + ) + errors = [] + + for report_id, report in normalized_reports.items(): + try: + persist_delayed_memory_report( + report_id, + report, + root=root, + ) + except (OSError, TypeError, ValueError) as error: + errors.append( + f"{report_id}: {error}" + ) + + return errors + + +def load_delayed_memory_reports_from_files( + *, + root: Path | str = DELAYED_MEMORY_ROOT, +) -> tuple[dict[str, dict], list[str]]: + + root_path = Path(root) + + if not root_path.exists(): + return {}, [] + + reports = {} + report_sources = {} + warnings = [] + + try: + paths = sorted( + ( + path + for path in root_path.glob("*.json") + if path.is_file() + and not path.name.startswith(".") + ), + key=lambda path: ( + path.stat().st_mtime_ns, + path.name.casefold(), + ), + ) + except OSError as error: + return {}, [ + f"cannot scan {root_path}: {error}" + ] + + for path in paths: + try: + raw_value = json.loads( + path.read_text( + encoding="utf-8-sig", + ) + ) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + warnings.append( + f"skipped {path.name}: {error}" + ) + continue + + normalized_reports = normalize_delayed_memory_reports( + raw_value, + ) + + if not normalized_reports: + warnings.append( + f"skipped {path.name}: invalid delayed memory format" + ) + continue + + for report_id, report in normalized_reports.items(): + previous_source = report_sources.get( + report_id + ) + + if previous_source: + warnings.append( + "duplicate delayed memory id " + f"{report_id}: {path.name} replaced " + f"{previous_source}" + ) + + reports[report_id] = report + report_sources[report_id] = path.name + + return reports, warnings diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index 732ca06d..a39d8e05 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -134,7 +134,6 @@ def get_current_action_sequence_started_at( "create_wildcard_library": "Created wildcard library", "expand_template": "Expanded template", "generate_prompt_batch": "Generated prompt batch", - "list_delayed_memory": "Listed delayed memory", "list_skills": "Listed skills", "list_wildcards": "Listed wildcards", "preview_file": "Previewed file", diff --git a/websocket/__init__.py b/websocket/__init__.py index 65b82b27..6e9ae1bd 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -30,6 +30,7 @@ apply_runtime_resume, apply_session_bootstrap, emit_current_runtime_memory, + emit_delayed_memory_store_snapshot, ensure_initial_runtime_snapshot, get_or_create_connection_context, initialize_connection, @@ -50,6 +51,10 @@ cancel_current_task, ) +from utils.delayed_memory_file_store import ( + persist_delayed_memory_reports, +) + websocket_router = APIRouter() @@ -257,13 +262,16 @@ async def process_pending_requests(): context, message_data, ) + reports = getattr( + context, + "delayed_memory_reports", + {}, + ) or {} + file_errors = persist_delayed_memory_reports( + reports + ) report_count = len( - getattr( - context, - "delayed_memory_reports", - {}, - ) - or {} + reports ) await logger.log_system( ( @@ -271,6 +279,14 @@ async def process_pending_requests(): f"({report_count} reports)" ) ) + for file_error in file_errors: + await logger.log_system( + "[DELAYED MEMORY] local file save failed: " + + file_error + ) + await emit_delayed_memory_store_snapshot( + context + ) continue if message_type == "session_bootstrap": diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index fd221948..7e17c789 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -22,10 +22,14 @@ from runtime.telemetry import send_telemetry from utils.actions import ( is_active_memory_key, - is_delayed_memory_report_id, refresh_active_memory_runtime_metadata, remove_active_memory_entries, ) +from utils.delayed_memory_file_store import ( + load_delayed_memory_reports_from_files, + merge_delayed_memory_reports, + normalize_delayed_memory_reports, +) MAX_BOOTSTRAP_MEMORY_CHARS = 12000 @@ -98,249 +102,82 @@ def active_memory_records_text(context) -> str: ) -def clean_delayed_memory_counter(value) -> int: - - try: - return max( - int( - value - or 0 - ), - 0, - ) - except (TypeError, ValueError): - return 0 - - -def clean_delayed_memory_session_ids(value) -> list[str]: +def clean_delayed_memory_reports(value) -> dict: - source = ( + return normalize_delayed_memory_reports( value - if isinstance( - value, - list, - ) - else [] ) - session_ids = [] - seen = set() - for item in source: - session_id = clean_bootstrap_memory( - str( - item - or "" - ), - limit=200, - ) - if ( - not session_id - or session_id in seen - ): - continue +def apply_delayed_memory_reports( + context, + message_data: dict, +) -> None: + + if "delayed_memory_reports" not in message_data: + return - seen.add( - session_id + incoming_reports = clean_delayed_memory_reports( + message_data.get( + "delayed_memory_reports", + {}, ) - session_ids.append( - session_id + ) + existing_reports = clean_delayed_memory_reports( + getattr( + context, + "delayed_memory_reports", + {}, ) + ) - return session_ids - - -def clean_delayed_memory_reports(value) -> dict: - - if not isinstance( - value, - dict, - ): - return {} - - reports = {} - - for key, report in value.items(): - report_id = str( - key - or "" - ).strip().casefold() + context.delayed_memory_reports = merge_delayed_memory_reports( + incoming_reports, + existing_reports, + ) - if not is_delayed_memory_report_id( - report_id - ): - continue - if not isinstance( - report, - dict, - ): - continue +def hydrate_delayed_memory_reports_from_files( + context, +) -> None: - title = clean_bootstrap_memory( - str( - report.get( - "title", - "", - ) - or "" - ), - limit=500, + file_reports, warnings = ( + load_delayed_memory_reports_from_files() + ) + current_reports = clean_delayed_memory_reports( + getattr( + context, + "delayed_memory_reports", + {}, ) + ) - if not title: - continue + context.delayed_memory_reports = merge_delayed_memory_reports( + current_reports, + file_reports, + ) - tags = report.get( - "tags", - [], + if warnings: + current_warnings = getattr( + context, + "runtime_delayed_memory_file_warnings", + None, ) - if isinstance( - tags, + if not isinstance( + current_warnings, list, ): - clean_tags = [ - clean_bootstrap_memory( - str(tag or ""), - limit=80, - ) - for tag in tags - if clean_bootstrap_memory( - str(tag or ""), - limit=80, - ) - ][:30] - else: - clean_tags = [ - clean_bootstrap_memory( - tag, - limit=80, - ) - for tag in str( - tags - or "" - ).split(",") - if clean_bootstrap_memory( - tag, - limit=80, - ) - ][:30] - - reports[report_id] = { - "title": title, - "summary": clean_bootstrap_memory( - str( - report.get( - "summary", - "", - ) - or "" - ), - limit=2000, - ), - "tags": clean_tags, - "body": clean_bootstrap_memory( - str( - report.get( - "body", - "", - ) - or "" - ), - limit=12000, - ), - "created_session_id": clean_bootstrap_memory( - str( - report.get( - "created_session_id", - "", - ) - or "" - ), - limit=200, - ), - "created_time": clean_bootstrap_memory( - str( - report.get( - "created_time", - "", - ) - or "" - ), - limit=100, - ), - "created_date": clean_bootstrap_memory( - str( - report.get( - "created_date", - "", - ) - or report.get( - "created_time", - "", - ) - or "" - ), - limit=100, - ), - "appended_times": clean_delayed_memory_counter( - report.get( - "appended_times", - 0, - ) - ), - "append_streak": clean_delayed_memory_counter( - report.get( - "append_streak", - 0, - ) - ), - "last_appended_date": clean_bootstrap_memory( - str( - report.get( - "last_appended_date", - "", - ) - or "" - ), - limit=100, - ), - "last_appended_session_id": clean_bootstrap_memory( - str( - report.get( - "last_appended_session_id", - "", - ) - or "" - ), - limit=200, - ), - "all_appended_session_ids": clean_delayed_memory_session_ids( - report.get( - "all_appended_session_ids", - [], - ) - ), - } - - return reports - - -def apply_delayed_memory_reports( - context, - message_data: dict, -) -> None: + current_warnings = [] + context.runtime_delayed_memory_file_warnings = ( + current_warnings + ) - reports = clean_delayed_memory_reports( - message_data.get( - "delayed_memory_reports", - {}, + current_warnings.extend( + warning + for warning in warnings + if warning not in current_warnings ) - ) - - if "delayed_memory_reports" in message_data: - context.delayed_memory_reports = reports def remove_runtime_memory_slot_by_key( @@ -593,6 +430,9 @@ def get_or_create_connection_context( logger=logger, clients=websocket.app.state.clients, ) + hydrate_delayed_memory_reports_from_files( + context + ) return context, False @@ -614,6 +454,9 @@ def get_or_create_connection_context( logger, ) existing_context.session_id = client_id + hydrate_delayed_memory_reports_from_files( + existing_context + ) return existing_context, True context = RuntimeContext( @@ -625,6 +468,9 @@ def get_or_create_connection_context( clients=websocket.app.state.clients, session_id=client_id, ) + hydrate_delayed_memory_reports_from_files( + context + ) store[client_id] = context @@ -1561,6 +1407,24 @@ def apply_session_bootstrap( # CONNECTION SETUP # --------------------------------------------------------- +async def emit_delayed_memory_store_snapshot( + context, +) -> None: + + reports = clean_delayed_memory_reports( + getattr( + context, + "delayed_memory_reports", + {}, + ) + ) + + await context.emitter.emit({ + "type": "delayed_memory_store_snapshot", + "delayed_memory_reports": reports, + }) + + async def initialize_connection( context, *, @@ -1573,6 +1437,25 @@ async def initialize_connection( context ) + file_warnings = list( + getattr( + context, + "runtime_delayed_memory_file_warnings", + [], + ) + or [] + ) + context.runtime_delayed_memory_file_warnings = [] + + for warning in file_warnings: + await context.logger.log_system( + "[DELAYED MEMORY] " + str(warning) + ) + + await emit_delayed_memory_store_snapshot( + context + ) + if skip_initial_runtime_state: await context.logger.log_system( "[WS] soft reconnect: initial runtime state skipped" From b2025be8dcfc12da360edfda8655bff6a4fed4ef Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:16:27 +0300 Subject: [PATCH 002/168] feature: L4 memory layer - remove runtime action boundary suppression - clarify delayed memory save and append rules - update runtime action regression coverage --- clients/brain_client.py | 29 ------ contracts/append_delayed_memory.json | 3 +- contracts/save_delayed_memory.json | 11 +-- tests/test_brain_asset_flow.py | 11 +-- tests/test_brain_runtime_actions.py | 139 ++++++++++++++++++++++++--- tests/test_search_flow.py | 56 ++++++----- 6 files changed, 165 insertions(+), 84 deletions(-) diff --git a/clients/brain_client.py b/clients/brain_client.py index 2a683661..9476c99e 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -654,7 +654,6 @@ def preserve_duplicate_append_skill_marker( #preserve_action_text=True ) stop_for_runtime_action = False - runtime_action_boundary_seen = False delayed_memory_bubble_started = False asset_action_bubble_started = False asset_action_bubble_id = "" @@ -1281,7 +1280,6 @@ async def filter_runtime_action_chunk( ): nonlocal stop_for_runtime_action - nonlocal runtime_action_boundary_seen chunk_type = action_chunk.get( "type" @@ -1352,17 +1350,6 @@ async def filter_runtime_action_chunk( ): return None - if runtime_action_boundary_seen: - # Boundary actions require a follow-up, so ordinary model text - # emitted after the first boundary remains hidden from chat. Do - # not abort the provider stream here, though: token chunks can - # split visible text mid-word. Stopping on the first fragment - # truncates the model output and starts the follow-up before the - # current generation has actually ended. - # Keep draining the full response so later markers are processed, - # usage is finalized, and the raw BRAIN/SERVICE log is complete. - return None - if action_applied: if not result.text: return None @@ -1462,8 +1449,6 @@ async def apply_runtime_action_result( result, ) -> bool: - nonlocal runtime_action_boundary_seen - runtime_action_calls = tuple( result.actions ) @@ -1514,18 +1499,6 @@ async def apply_runtime_action_result( runtime_message_id=runtime_message_id, ) - if any( - action.name in ( - RUNTIME_ACTION_ASSET_ACTION, - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - RUNTIME_ACTION_WEB_SEARCH, - RUNTIME_ACTION_LIST_SKILLS, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, - ) - for action in immediate_action_calls - ): - runtime_action_boundary_seen = True - return True async def flush_pending_idle_actions() -> None: @@ -1645,7 +1618,6 @@ async def flush_pending_idle_actions() -> None: if ( content_tail and not stop_for_runtime_action - and not runtime_action_boundary_seen ): yield { "type": "content", @@ -1752,7 +1724,6 @@ async def flush_pending_idle_actions() -> None: if ( content_tail and not stop_for_runtime_action - and not runtime_action_boundary_seen ): yield { "type": "content", diff --git a/contracts/append_delayed_memory.json b/contracts/append_delayed_memory.json index 38ccff30..35ba8b88 100644 --- a/contracts/append_delayed_memory.json +++ b/contracts/append_delayed_memory.json @@ -10,7 +10,8 @@ "emit_followup": false }, "rules": [ - "Available saved reports are listed in the block.", + "JIN can append saved reports to the context by using this marker.", + "Available for appending reports are listed in the block.", "Emit one marker per saved delayed memory. You may emit multiple markers to append multiple reports; already appended ids are ignored.", "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in ." ], diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index be438a26..3e0ddb48 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -15,19 +15,16 @@ "emit_followup": true }, "rules": [ - "SAVE_DELAYED_MEMORY_CONTENT:", - "Use this marker ONLY when the user explicitly asks to save in delayed memory a summary/digest/recap/report of the current state.", - "Skip this marker when user request is to save session.", - "Emit fulfilled form inside marker only for explicit summary-save requests:", + "Use this marker ONLY when the user explicitly asks to create or save in delayed memory a summary/digest/recap/report of the current state.", + "Skip save_delayed_memory when user request is to save session or append already existing delayed memory report.", + "Emit fulfilled form inside marker:", "", "title:", "summary:", "tags:", "body:", "", - "", - "The opening tag must be the first content line. Emit every field immediately, then always emit the matching closing tag.", - "When saving a summary/report NEVER skip template fields; you must fulfill all fields of delayed memory form so it will be valid for processing by runtime.", + "You must fulfill ALL fields of delayed memory form so it will be valid for processing by runtime.", "DO NOT add any other text while making a report! Do not notify user or acknowledge saving and immediately proceed with marker in response." ], "close_tag": true diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 64be5617..e2b12d22 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -2885,7 +2885,6 @@ async def test_follow_up_action_messages_keep_actions_enabled(self): calls = [] action_names = [ "append_skill", - "append_delayed_memory", "list_skills", "check_todo", ] @@ -2940,23 +2939,19 @@ async def fake_run_brain_stream(**kwargs): self.assertEqual( len(calls), - 5, + 4, ) self.assertIn( 'APPEND_SKILL', calls[1]["system_prompt"], ) - self.assertIn( - 'APPEND_DELAYED_MEMORY', - calls[2]["system_prompt"], - ) self.assertIn( 'LIST_SKILLS', - calls[3]["system_prompt"], + calls[2]["system_prompt"], ) self.assertIn( 'CHECK_TODO', - calls[4]["system_prompt"], + calls[3]["system_prompt"], ) for call in calls[1:]: self.assertEqual( diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 70c2e8be..1ca83923 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -860,7 +860,7 @@ async def collect(context): all(message_ids), ) - def test_stream_drains_adjacent_markers_after_web_search_boundary(self): + def test_stream_processes_adjacent_markers_after_web_search(self): class FakeBrainClient: async def stream(self, **_kwargs): @@ -972,7 +972,7 @@ async def collect(context): ], ) - def test_stream_drains_full_hidden_response_after_runtime_boundary(self): + def test_stream_preserves_text_after_runtime_actions(self): class FakeBrainClient: def __init__(self): @@ -980,10 +980,10 @@ def __init__(self): async def stream(self, **_kwargs): for content in ( - "", + "", "\n", "Про", - "должаю скрытый текст.\n", + "должаю видимый текст.\n", "", "\nФинал генерации.", ): @@ -1002,7 +1002,7 @@ async def collect(client, context): chunk async for chunk in ask_brain_stream( client=client, - text="run boundary actions", + text="run runtime actions", context=context, system_prompt="system prompt", brain_payload="brain payload", @@ -1037,9 +1037,20 @@ async def collect(client, context): ) self.assertTrue(client.completed) - self.assertEqual(visible_text, "") + self.assertEqual( + visible_text, + "\nПродолжаю видимый текст.\nФинал генерации.", + ) + self.assertNotIn( + "WEB_SEARCH", + visible_text, + ) + self.assertNotIn( + "JIN_COLOR", + visible_text, + ) self.assertIn( - "Продолжаю скрытый текст.", + "Продолжаю видимый текст.", raw_model_output, ) self.assertIn( @@ -2459,7 +2470,109 @@ async def collect(context): ) - def test_stream_asset_action_is_runtime_boundary(self): + def test_non_followup_delayed_memory_action_keeps_visible_text(self): + + visible_answer = ( + "Я просмотрел доступные отчёты и выбрал отчёт. " + "Добавляю его в контекст." + ) + + class FakeBrainClient: + + async def stream(self, **_kwargs): + yield { + "type": "content", + "content": ( + "\n\n" + f"{visible_answer}\n\n" + "" + ), + } + + class TrackingEmitter: + + def __init__(self): + self.events = [] + + async def emit(self, event): + self.events.append(event) + + class Context: + pass + + async def collect(context): + return [ + chunk + async for chunk in ask_brain_stream( + client=FakeBrainClient(), + text="append one delayed memory report", + context=context, + system_prompt="system prompt", + brain_payload="brain payload", + runtime_actions={ + "CAN_SAVE_DELAYED_MEMORY": True, + "CAN_CLEAN_TOOL_RESULTS": True, + }, + ) + ] + + context = Context() + context.emitter = TrackingEmitter() + context.runtime_current_turn_id = "turn-delayed-append" + context.session_id = "session-1" + context.timestamp = "2026-08-01T16:20:00" + context.delayed_memory_reports = { + "pwajtw": { + "title": "Архитектурный Срез", + "summary": "Current JIN architecture.", + "tags": ["architecture"], + "body": "Complete report body.", + }, + } + context.runtime_tool_results = [ + { + "kind": "asset", + "result": { + "ok": True, + }, + }, + ] + context.runtime_tool_results_turn_count = 1 + context.runtime_tool_results_generation = 0 + + original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN + config.USE_SERVICE_AS_BRAIN = False + + try: + chunks = asyncio.run( + collect(context) + ) + finally: + config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain + + visible_text = "".join( + chunk.get("content", "") + for chunk in chunks + if chunk.get("type") == "content" + ) + + self.assertEqual( + visible_text, + visible_answer, + ) + self.assertEqual( + [ + event.get("name") + for event in context.runtime_action_events + ], + [ + "append_delayed_memory", + "clean_tool_results", + ], + ) + + + def test_stream_asset_action_strips_marker_and_keeps_text(self): class FakeBrainClient: async def stream(self, **_kwargs): @@ -2475,7 +2588,7 @@ async def stream(self, **_kwargs): " }\n" "}\n" "\n" - "This should not be visible." + "This should remain visible." ), } @@ -2530,7 +2643,7 @@ async def collect(context): self.assertEqual( visible_text, - "", + "This should remain visible.", ) self.assertNotIn( "ASSET_ACTION", @@ -2578,7 +2691,7 @@ async def stream(self, **_kwargs): "type": "content", "content": ( "\n" - "This should not be visible." + "This should remain visible." ), } @@ -2656,7 +2769,7 @@ async def emit(self, event): self.assertEqual( visible_text, - "", + "This should remain visible.", ) self.assertEqual( [ @@ -3322,7 +3435,7 @@ def test_prompt_lists_available_delayed_memory_below_session_state(self): "" ) - self.assertIn( + self.assertNotIn( "DELAYED MEMORY ACTIONS:", prompt, ) diff --git a/tests/test_search_flow.py b/tests/test_search_flow.py index 1958b133..0d99247f 100644 --- a/tests/test_search_flow.py +++ b/tests/test_search_flow.py @@ -625,32 +625,23 @@ async def test_search_is_model_driven_for_explicit_user_request(self): "tesla car price", ], ) + logger_text = "\n".join( + message + for _, message, _ in get_fake_logger( + context + ).messages + ) self.assertIn( - "action: web_search", - "\n".join( - message - for _, message, _ in get_fake_logger( - context - ).messages - ), + "[RUNTIME ACTION] executing search", + logger_text, ) self.assertIn( - "query: tesla car price", - "\n".join( - message - for _, message, _ in get_fake_logger( - context - ).messages - ), + "id='web_search_001'", + logger_text, ) self.assertIn( - "id: web_search_001", - "\n".join( - message - for _, message, _ in get_fake_logger( - context - ).messages - ), + "query='tesla car price'", + logger_text, ) self.assertIn( f"INITIAL_SEQUENCE_INSTRUCTION: {state.translated_input}", @@ -760,7 +751,7 @@ async def test_brain_emitted_search_runs_even_with_text(self): 2, ) - async def test_search_action_stops_initial_brain_stream(self): + async def test_search_action_preserves_initial_brain_stream(self): search_provider = FakeSearchProvider( results=[ @@ -819,11 +810,24 @@ async def test_search_action_stops_initial_brain_stream(self): if message.get("type") == "message_chunk" ] - self.assertNotIn( + visible_text = "".join( + message_chunks + ) + self.assertIn( + "Needs current pricing.", + visible_text, + ) + self.assertIn( "Guessed apple price before search.", - "".join( - message_chunks - ), + visible_text, + ) + self.assertIn( + "Apple price from search result.", + visible_text, + ) + self.assertNotIn( + " Date: Sun, 2 Aug 2026 12:46:24 +0300 Subject: [PATCH 003/168] chore: light theme - update bold text color --- ui/static/css/theme-win95.css | 8 ++++++++ ui/templates/index.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index dc0d639a..9d679257 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -853,6 +853,14 @@ body.theme-win95 .jin-chat-bubble-brain .jin-chat-pre { text-shadow: none; } +body.theme-win95 .jin-chat-bubble .jin-chat-markdown strong, +body.theme-win95 .jin-chat-bubble-service .jin-chat-markdown strong, +body.theme-win95 .jin-chat-bubble-brain .jin-chat-markdown strong, +body.theme-win95 .jin-chat-bubble-translator .jin-chat-markdown strong, +body.theme-win95 .jin-chat-bubble-user .jin-chat-markdown strong { + color: inherit; +} + body.theme-win95 .jin-think-wrapper { opacity: 1; max-width: 728px; diff --git a/ui/templates/index.html b/ui/templates/index.html index 6018a826..3c8c77c6 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -16,7 +16,7 @@ - + From ac343bd0fe90f31a11f2c46e2e7242816333b1d7 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:09:05 +0300 Subject: [PATCH 004/168] feature: L4 memory layer - add idle-driven extraction and two-phase long-term L4 memory consolidation - add regression coverage --- config.example.py | 7 + rules/brain_context_builder.py | 26 + rules/identity.py | 8 +- runtime/L4_memory.py | 401 ++++++++++ runtime/L4_memory_rules.py | 130 +++ runtime/L4_memory_utils.py | 842 ++++++++++++++++++++ runtime/runtime_context.py | 10 + runtime/stream.py | 17 +- tests/test_brain_asset_flow.py | 12 +- tests/test_brain_prompt_memory.py | 93 ++- tests/test_brain_runtime_actions.py | 5 +- tests/test_l4_memory.py | 435 ++++++++++ tests/test_runtime_stream_tokens.py | 22 +- ui/static/js/runtime/runtime-l4-memory.js | 309 +++++++ ui/static/js/runtime/runtime-memory-view.js | 220 ++++- ui/static/js/runtime/runtime-storage.js | 71 ++ ui/static/js/runtime/runtime.js | 65 ++ ui/static/js/socket.js | 24 + ui/static/js/socket/event-handlers.js | 40 + ui/templates/index.html | 1 + utils/context/session_actions.py | 76 +- utils/session_actions_history.py | 106 +++ websocket/__init__.py | 92 ++- websocket/messages.py | 1 + 24 files changed, 2967 insertions(+), 46 deletions(-) create mode 100644 runtime/L4_memory.py create mode 100644 runtime/L4_memory_rules.py create mode 100644 runtime/L4_memory_utils.py create mode 100644 tests/test_l4_memory.py create mode 100644 ui/static/js/runtime/runtime-l4-memory.js diff --git a/config.example.py b/config.example.py index 6875bb4c..88af43ac 100644 --- a/config.example.py +++ b/config.example.py @@ -113,6 +113,13 @@ # multimodal chat content with {"type": "image_url"} user message parts. SERVICE_IMAGE_INPUT_ENABLED = False +# --------------------------------------------------------- +# L4 LONG-TERM MEMORY +# --------------------------------------------------------- + +L4_MEMORY_ENABLED = True +L4_IDLE_SECONDS = 60 + # --------------------------------------------------------- # WEB_SEARCH # --------------------------------------------------------- diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index e478d738..33cf6159 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -614,6 +614,21 @@ def build_appended_delayed_memory_context( ) +def build_long_term_memory_context( + context=None, + user_input: str = "", +) -> str: + + if context is None: + return "" + + from runtime.L4_memory import build_runtime_l4_memory_context + + return build_runtime_l4_memory_context( + context=context, + ) + + # Runtime action rules are assembled from contracts/rules_assembler.py. # Brain context assembly @@ -740,6 +755,17 @@ def build_brain_context( context, ) + # L4 memory block: always-on canonical facts that survive sessions. + long_term_memory_context = build_long_term_memory_context( + context, + user_input=user_input, + ) + + if long_term_memory_context: + runtime_context_parts.append( + long_term_memory_context + ) + # Zero-diff alert block: warns the brain when a repeated answer stalled. _append_zero_diff_alert( runtime_context_parts, diff --git a/rules/identity.py b/rules/identity.py index 37ffda59..c42e5359 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -54,10 +54,10 @@ "- L1 for current session facts - Runtime Memory\n" "- L2 for repeated patterns - L2 Pattern Evidence Memory\n" "- L3 for saved session snapshot - Session Memory\n" - "- active_memory and delayed_memory with instructions when to use it.\n" - #"- L4 for durable project/world facts, is a long-term knowledge base\n" - #"L4 stores durable understanding: stable facts, definitions, decisions, terminology, constraints, " - #"and conclusions that JIN should carry between sessions to understand the user’s projects and concepts better.\n" + "- active_memory and delayed_memory with instructions when to use it\n" + "- L4 for durable project/world facts, is a long-term knowledge base\n" + "L4 stores durable understanding: stable facts, definitions, decisions, terminology, constraints, " + "and conclusions that JIN should carry between sessions to understand the user’s projects and concepts better.\n" #"- User Profile Layer for personal/user interaction preferences\n" f'{SHARED_AXIOM_RULES}' ) \ No newline at end of file diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py new file mode 100644 index 00000000..e72a98b5 --- /dev/null +++ b/runtime/L4_memory.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import asyncio +import json +import traceback + +from clients.service_client import ask_service_model +from config_loader import config +from runtime.L4_memory_utils import ( + add_l4_pending_candidates, + apply_l4_merge_operations, + build_l4_extraction_system_prompt, + build_l4_extraction_user_prompt, + build_l4_merge_system_prompt, + build_l4_merge_user_prompt, + clone_l4_store, + collect_pending_facts_memory_fields, + delete_l4_fact_from_store, + extract_l4_json_payload, + format_long_term_memory_context, + mark_facts_memory_fields_analyzed, + normalize_facts_memory_records, + normalize_l4_candidates, + normalize_l4_merge_operations, + normalize_l4_store, +) +from runtime.memory_common import ( + build_memory_failure_details, + build_runtime_summarizer_payload, + extract_runtime_memory_text, + is_runtime_memory_response_truncated, + log_memory_event, + log_runtime_summarizer_payload, + log_runtime_summarizer_result, + refresh_runtime_memory_summarizer_usage, + safe_call, +) + + +L4_LOG_LEVEL = "L4" +L4_DEFAULT_IDLE_SECONDS = 60 + + +def l4_memory_enabled() -> bool: + return bool(getattr(config, "L4_MEMORY_ENABLED", True)) + + +def get_l4_idle_seconds() -> int: + return int( + getattr(config, "L4_IDLE_SECONDS", L4_DEFAULT_IDLE_SECONDS) + or L4_DEFAULT_IDLE_SECONDS + ) + + +def ensure_runtime_l4_state(context) -> dict: + context.runtime_facts_memory_records = normalize_facts_memory_records( + getattr(context, "runtime_facts_memory_records", []) + ) + context.runtime_long_term_memory_store = normalize_l4_store( + getattr(context, "runtime_long_term_memory_store", None) + ) + return context.runtime_long_term_memory_store + + +def apply_facts_memory_store_sync(context, raw_records) -> dict: + records = normalize_facts_memory_records(raw_records) + context.runtime_facts_memory_records = records + return { + "records_count": len(records), + "signals_count": sum(int(record.get("signal_count") or 0) for record in records), + "pending_count": len(collect_pending_facts_memory_fields(records)), + } + + +def apply_l4_memory_store_sync(context, raw_store) -> bool: + incoming = normalize_l4_store(raw_store) + current = ensure_runtime_l4_state(context) + incoming_revision = int(incoming.get("revision") or 0) + current_revision = int(current.get("revision") or 0) + current_has_data = bool(current.get("facts") or current.get("pending_facts")) + + if incoming_revision < current_revision: + return False + if current_has_data and incoming_revision == current_revision and incoming != current: + return False + + context.runtime_long_term_memory_store = incoming + return True + + +async def emit_facts_memory_store_update(context) -> None: + emit = getattr(getattr(context, "emitter", None), "emit", None) + await safe_call( + emit, + { + "type": "facts_memory_store_update", + "records": normalize_facts_memory_records( + getattr(context, "runtime_facts_memory_records", []) + ), + }, + ) + + +async def emit_l4_memory_update(context, *, change: dict | None = None) -> None: + emit = getattr(getattr(context, "emitter", None), "emit", None) + await safe_call( + emit, + { + "type": "l4_memory_update", + "store": clone_l4_store(ensure_runtime_l4_state(context)), + "change": change or {}, + }, + ) + + +async def ask_l4_model( + *, + context, + service_client, + label: str, + system_prompt: str, + user_prompt: str, + max_tokens: int, +) -> dict: + await refresh_runtime_memory_summarizer_usage( + context, + system_prompt=system_prompt, + user_prompt=user_prompt, + ) + await log_runtime_summarizer_payload( + context, + label=label, + payload=build_runtime_summarizer_payload( + service_client=service_client, + system_prompt=system_prompt, + user_prompt=user_prompt, + temperature=getattr(config, "SERVICE_TEMPERATURE", 0.1), + max_tokens=max_tokens, + ), + ) + + response = await ask_service_model( + client=service_client, + system_prompt=system_prompt, + user_prompt=user_prompt, + temperature=getattr(config, "SERVICE_TEMPERATURE", 0.1), + max_tokens=max_tokens, + timeout=getattr(config, "SERVICE_REQUEST_TIMEOUT", 1000.0), + ) + response_text = extract_runtime_memory_text(response) + + await log_runtime_summarizer_result( + context, + label=label, + result=response_text, + ) + await refresh_runtime_memory_summarizer_usage( + context, + system_prompt=system_prompt, + user_prompt=user_prompt, + response=response, + ) + return response + + +async def run_l4_extraction_phase( + *, + context, + service_client, + pending_fields: list[dict], +) -> dict: + system_prompt = build_l4_extraction_system_prompt() + user_prompt = build_l4_extraction_user_prompt(pending_fields=pending_fields) + response = await ask_l4_model( + context=context, + service_client=service_client, + label="L4 extraction", + system_prompt=system_prompt, + user_prompt=user_prompt, + max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + ) + + if is_runtime_memory_response_truncated(response): + return {"phase": "extract", "status": "skipped", "reason": "response_truncated"} + + payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + if payload is None: + return {"phase": "extract", "status": "skipped", "reason": "invalid_json"} + + raw_candidates = payload.get("facts") + if not isinstance(raw_candidates, list): + return {"phase": "extract", "status": "skipped", "reason": "invalid_facts_payload"} + + candidates = normalize_l4_candidates( + payload, + source_fields=pending_fields, + ) + if len(candidates) != len(raw_candidates): + return {"phase": "extract", "status": "skipped", "reason": "invalid_candidates"} + store, pending_change = add_l4_pending_candidates( + ensure_runtime_l4_state(context), + candidates, + ) + context.runtime_long_term_memory_store = store + + records, records_changed = mark_facts_memory_fields_analyzed( + getattr(context, "runtime_facts_memory_records", []), + pending_fields, + ) + context.runtime_facts_memory_records = records + + if records_changed: + await emit_facts_memory_store_update(context) + if pending_change.get("changed"): + await emit_l4_memory_update(context, change=pending_change) + + return { + "phase": "extract", + "status": "completed", + "selected_fields_count": len(pending_fields), + "candidates_count": len(candidates), + "pending_change": pending_change, + } + + +async def run_l4_merge_phase(*, context, service_client) -> dict: + base_store = clone_l4_store(ensure_runtime_l4_state(context)) + pending_facts = list(base_store.get("pending_facts") or []) + if not pending_facts: + return {"phase": "merge", "status": "skipped", "reason": "no_pending_long_term_facts"} + + system_prompt = build_l4_merge_system_prompt() + user_prompt = build_l4_merge_user_prompt( + existing_facts=base_store.get("facts") or [], + pending_facts=pending_facts, + ) + response = await ask_l4_model( + context=context, + service_client=service_client, + label="L4 merge", + system_prompt=system_prompt, + user_prompt=user_prompt, + max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + ) + + if is_runtime_memory_response_truncated(response): + return {"phase": "merge", "status": "skipped", "reason": "response_truncated"} + + payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + if payload is None: + return {"phase": "merge", "status": "skipped", "reason": "invalid_json"} + + current_store = clone_l4_store(ensure_runtime_l4_state(context)) + if current_store != base_store: + return {"phase": "merge", "status": "skipped", "reason": "store_changed_during_merge"} + + operations = normalize_l4_merge_operations(payload) + next_store, merge_change = apply_l4_merge_operations(base_store, operations) + if not merge_change.get("valid"): + return { + "phase": "merge", + "status": "skipped", + "reason": merge_change.get("reason") or "invalid_operations", + } + + context.runtime_long_term_memory_store = next_store + await emit_l4_memory_update(context, change=merge_change) + + return { + "phase": "merge", + "status": "completed", + "operations_count": len(operations), + "merge_change": merge_change, + } + + +async def maybe_update_runtime_l4_memory( + *, + context, + user_idle_seconds: int | None = None, +) -> dict: + try: + ensure_runtime_l4_state(context) + + if not l4_memory_enabled(): + return {"status": "disabled"} + + if user_idle_seconds is not None: + try: + normalized_idle_seconds = int(float(user_idle_seconds)) + except (TypeError, ValueError): + normalized_idle_seconds = 0 + if normalized_idle_seconds < get_l4_idle_seconds(): + return {"status": "skipped", "reason": "user_not_idle_enough"} + + service_client = getattr(context, "clients", {}).get("service") + if service_client is None: + return {"status": "skipped", "reason": "service_client_unavailable"} + + pending_fields = collect_pending_facts_memory_fields( + getattr(context, "runtime_facts_memory_records", []) + ) + if pending_fields: + return await run_l4_extraction_phase( + context=context, + service_client=service_client, + pending_fields=pending_fields, + ) + + if ensure_runtime_l4_state(context).get("pending_facts"): + return await run_l4_merge_phase( + context=context, + service_client=service_client, + ) + + return {"status": "skipped", "reason": "nothing_pending"} + + except asyncio.CancelledError: + raise + except Exception as error: + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message="L4 update failed", + details=build_memory_failure_details( + stage="L4 memory update", + error=error, + traceback_text=traceback.format_exc(), + ), + fallback_channel="error", + ) + return {"status": "failed", "reason": type(error).__name__} + finally: + if getattr(context, "runtime_l4_memory_update_task", None) is asyncio.current_task(): + context.runtime_l4_memory_update_task = None + + +def schedule_l4_memory_idle_update( + *, + context, + user_idle_seconds: int | None = None, +) -> asyncio.Task | None: + if not l4_memory_enabled(): + return None + + previous_task = getattr(context, "runtime_l4_memory_update_task", None) + if previous_task is not None and not previous_task.done(): + return previous_task + + task = asyncio.create_task( + maybe_update_runtime_l4_memory( + context=context, + user_idle_seconds=user_idle_seconds, + ) + ) + context.runtime_l4_memory_update_task = task + + background_tasks = getattr(context, "background_tasks", None) + if background_tasks is None: + background_tasks = set() + context.background_tasks = background_tasks + background_tasks.add(task) + task.add_done_callback(background_tasks.discard) + return task + + +def build_runtime_l4_memory_context(*, context) -> str: + if not l4_memory_enabled(): + return "" + store = ensure_runtime_l4_state(context) + return format_long_term_memory_context(store.get("facts") or []) + + +async def delete_l4_memory_fact(context, fact_id: str) -> bool: + store, changed = delete_l4_fact_from_store( + ensure_runtime_l4_state(context), + fact_id, + ) + if not changed: + return False + + context.runtime_long_term_memory_store = store + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message="L4 fact deleted", + details=json.dumps( + { + "fact_id": fact_id, + "revision": store.get("revision"), + "total_facts": len(store.get("facts") or []), + }, + ensure_ascii=False, + indent=2, + ), + ) + await emit_l4_memory_update( + context, + change={"removed_ids": [fact_id], "changed": True}, + ) + return True diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py new file mode 100644 index 00000000..539c1c4e --- /dev/null +++ b/runtime/L4_memory_rules.py @@ -0,0 +1,130 @@ +from __future__ import annotations + + +L4_EXTRACTION_SYSTEM_PROMPT = """ +You extract candidates for JIN's cross-session long-term memory. + +The input contains changed Facts Memory fields. They may be compressed, +temporary, incomplete, or partly interpretive. Long-term memory is included in +every future Brain context, so prefer returning nothing over saving noise. + +Save only information that is both: +- likely to remain useful across future sessions; +- directly supported by the input. + +Suitable information includes stable user facts and preferences, durable project +facts, decisions, goals, constraints, habits, environment, and explicit +corrections. + +Do not save current tasks, temporary topics, next steps, session state, tool +results, incidental names, one-off examples, assistant speculation, or external +knowledge that does not describe the user or a durable project. + +Never expand the source meaning: +- a mention does not establish a relationship or role; +- discussion does not establish ownership, participation, or project focus; +- current activity does not establish a long-term goal; +- repeated summaries do not establish truth; +- missing details must not be guessed. + +If an incomplete relationship may matter later, preserve only the known part and +state the uncertainty explicitly, for example: +"The user mentioned a person named X; their relationship is unknown and should +be clarified if relevant." + +Do not save such uncertainty when the entity appears incidental. + +Keep every fact: +- atomic: one durable idea; +- narrow: no broader than the source; +- concrete and independently understandable; +- free of temporary wording such as "currently", "today", or "next"; +- linked to one or more exact input keys in source_keys. + +Confidence measures support for the exact wording: +- 0.95: explicit, stable, and unambiguous; +- 0.75: directly supported but durability or scope is not fully established; +- 0.55: the known part is explicit and an important unknown is stated as unknown; +- below 0.55: do not save. + +Return JSON only: +{ + "facts": [ + { + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + "confidence": 0.95, + "source_keys": ["response_language"] + } + ] +} + +Allowed categories: +user_fact, user_preference, project_fact, project_decision, +persistent_constraint, environment, other. + +If nothing deserves always-on cross-session memory, return: +{"facts": []} +""".strip() + + +L4_MERGE_SYSTEM_PROMPT = """ +You consolidate pending candidates into JIN's current cross-session long-term +memory. + +Both existing facts and pending candidates are provisional. Existing memory is +not evidence merely because it already exists. + +Keep memory minimal, accurate, atomic, and free of duplicates. Preserve only +what is supported; remove or narrow inferred roles, relationships, ownership, +causality, project focus, and architectural meaning. + +Return exactly one operation for every pending_id, using only IDs from the +input. + +Actions: +- create: add a genuinely new durable fact; +- update: correct, narrow, or refine an existing fact; +- reinforce: the candidate has the same meaning, scope, and uncertainty; +- ignore: the candidate is temporary, incidental, redundant, speculative, or + not useful enough for always-on memory. + +Rules: +- A mention is not a relationship or role. +- Discussion is not ownership or participation. +- Current activity is not durable identity or project focus. +- Unknown details must remain unknown, not be guessed. +- Direct user corrections override incompatible existing facts. +- Prefer a narrower fact over a broader interpretation. +- Prefer updating an existing fact over creating a duplicate. +- If uncertainty is useful, state the known part and what remains unknown. +- If uncertainty is not useful, ignore the candidate. +- Do not increase confidence merely because wording was repeated by memory + summaries. +- Keep or change an existing key according to the corrected meaning; a key must + not preserve a false claim. +- Never invent facts, IDs, relationships, or source metadata. + +For update and reinforce, target_id must identify an existing fact. +For create and update, return the final canonical key and value. + +Return JSON only: +{ + "operations": [ + { + "action": "update", + "pending_id": "l4p_...", + "target_id": "l4_...", + "key": "person.x.context", + "value": "The user mentioned a person named X; their relationship is unknown and should be clarified if relevant.", + "category": "other", + "confidence": 0.55 + } + ] +} + +Do not omit pending facts. +Do not return multiple operations for the same pending_id. +Do not add fields outside the existing contract. +""".strip() diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py new file mode 100644 index 00000000..5bf76502 --- /dev/null +++ b/runtime/L4_memory_utils.py @@ -0,0 +1,842 @@ +from __future__ import annotations + +from copy import deepcopy +from datetime import datetime, timezone +import hashlib +import json +import re +from xml.sax.saxutils import escape + +from runtime.L4_memory_rules import ( + L4_EXTRACTION_SYSTEM_PROMPT, + L4_MERGE_SYSTEM_PROMPT, +) + + +L4_STORE_VERSION = 1 +L4_FACT_ID_PREFIX = "l4_" +L4_PENDING_FACT_ID_PREFIX = "l4p_" +L4_FIELD_STATUS_PENDING = "pending" +L4_FIELD_STATUS_ANALYZED = "analyzed" +L4_ALLOWED_CATEGORIES = { + "user_fact", + "user_preference", + "project_fact", + "project_decision", + "persistent_constraint", + "environment", + "other", +} +L4_ALLOWED_MERGE_ACTIONS = { + "create", + "update", + "reinforce", + "ignore", +} + + +def build_l4_extraction_system_prompt() -> str: + return L4_EXTRACTION_SYSTEM_PROMPT + + +def build_l4_extraction_user_prompt(*, pending_fields: list[dict]) -> str: + return ( + "Extract permanent-memory candidates from all pending Facts Memory " + "fields below.\n\n" + + json.dumps( + {"facts_memory_fields": pending_fields}, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + ) + + +def build_l4_merge_system_prompt() -> str: + return L4_MERGE_SYSTEM_PROMPT + + +def build_l4_merge_user_prompt( + *, + existing_facts: list[dict], + pending_facts: list[dict], +) -> str: + return ( + "Consolidate the entire pending batch into the current long-term " + "memory. Return exactly one operation for every pending_id.\n\n" + + json.dumps( + { + "existing_facts": existing_facts, + "pending_facts": pending_facts, + }, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + ) + + +def utc_now_iso() -> str: + return ( + datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + +def normalize_l4_text(value) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + +def normalize_l4_key(value) -> str: + key = normalize_l4_text(value).casefold() + key = re.sub(r"\s+", "_", key) + key = re.sub(r"[^a-z0-9а-яё._-]+", "_", key) + key = re.sub(r"_+", "_", key) + return key.strip("._-") + + +def clamp_float(value, *, default: float = 0.0) -> float: + try: + number = float(value) + except (TypeError, ValueError): + number = default + return max(0.0, min(1.0, number)) + + +def normalize_l4_category(value) -> str: + category = normalize_l4_key(value) + return category if category in L4_ALLOWED_CATEGORIES else "other" + + +def normalize_l4_string_list(value) -> list[str]: + candidates = value if isinstance(value, list) else [value] + result = [] + seen = set() + + for candidate in candidates: + text = normalize_l4_text(candidate) + if not text or text in seen: + continue + seen.add(text) + result.append(text) + + return result + + +def merge_l4_string_lists(*values) -> list[str]: + result = [] + seen = set() + + for value in values: + for item in normalize_l4_string_list(value): + if item in seen: + continue + seen.add(item) + result.append(item) + + return result + + +def build_l4_content_hash(content: str) -> str: + return hashlib.sha256( + normalize_l4_text(content).encode("utf-8") + ).hexdigest()[:16] + + +def build_l4_fact_id(*, key: str, value: str, pending: bool = False) -> str: + prefix = L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX + digest = hashlib.sha256(f"{key}\0{value}".encode("utf-8")).hexdigest()[:12] + return f"{prefix}{digest}" + + +def normalize_l4_fact( + value, + *, + pending: bool = False, + now: str | None = None, +) -> dict | None: + if not isinstance(value, dict): + return None + + key = normalize_l4_key(value.get("key")) + fact_value = normalize_l4_text(value.get("value") or value.get("content")) + if not key or not fact_value: + return None + + current_time = now or utc_now_iso() + expected_prefix = L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX + fact_id = normalize_l4_text(value.get("id")) + if not fact_id.startswith(expected_prefix): + fact_id = build_l4_fact_id(key=key, value=fact_value, pending=pending) + + try: + mention_count = max(1, int(value.get("mention_count") or 1)) + except (TypeError, ValueError): + mention_count = 1 + + return { + "id": fact_id, + "key": key, + "value": fact_value, + "category": normalize_l4_category(value.get("category")), + "confidence": clamp_float(value.get("confidence"), default=1.0), + "mention_count": mention_count, + "created_at": normalize_l4_text(value.get("created_at")) or current_time, + "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, + "source_session_ids": normalize_l4_string_list( + value.get("source_session_ids") + or value.get("source_session_id") + or value.get("session_id") + ), + "source_runtime_snapshot_ids": normalize_l4_string_list( + value.get("source_runtime_snapshot_ids") + or value.get("source_runtime_snapshot_id") + or value.get("runtime_snapshot_id") + ), + "source_keys": normalize_l4_string_list( + value.get("source_keys") or value.get("source_key") + ), + } + + +def merge_same_l4_fact(existing: dict, incoming: dict, *, now: str) -> dict: + result = dict(existing) + result["confidence"] = max( + clamp_float(existing.get("confidence"), default=1.0), + clamp_float(incoming.get("confidence"), default=1.0), + ) + result["mention_count"] = max(1, int(existing.get("mention_count") or 1)) + max( + 1, + int(incoming.get("mention_count") or 1), + ) + result["source_session_ids"] = merge_l4_string_lists( + existing.get("source_session_ids"), + incoming.get("source_session_ids"), + ) + result["source_runtime_snapshot_ids"] = merge_l4_string_lists( + existing.get("source_runtime_snapshot_ids"), + incoming.get("source_runtime_snapshot_ids"), + ) + result["source_keys"] = merge_l4_string_lists( + existing.get("source_keys"), + incoming.get("source_keys"), + ) + result["updated_at"] = now + return result + + +def deduplicate_l4_facts(facts: list[dict], *, pending: bool, now: str) -> list[dict]: + result = [] + by_id = {} + + for raw_fact in facts: + fact = normalize_l4_fact(raw_fact, pending=pending, now=now) + if fact is None: + continue + + existing_index = by_id.get(fact["id"]) + if existing_index is None: + by_id[fact["id"]] = len(result) + result.append(fact) + continue + + result[existing_index] = merge_same_l4_fact( + result[existing_index], + fact, + now=now, + ) + + return result + + +def empty_l4_store(*, now: str | None = None) -> dict: + return { + "version": L4_STORE_VERSION, + "revision": 0, + "updated_at": now or "", + "facts": [], + "pending_facts": [], + } + + +def normalize_l4_store(value, *, now: str | None = None) -> dict: + current_time = now or utc_now_iso() + if not isinstance(value, dict): + return empty_l4_store() + + try: + revision = max(0, int(value.get("revision") or 0)) + except (TypeError, ValueError): + revision = 0 + + facts = value.get("facts") if isinstance(value.get("facts"), list) else [] + pending = ( + value.get("pending_facts") + if isinstance(value.get("pending_facts"), list) + else [] + ) + + return { + "version": L4_STORE_VERSION, + "revision": revision, + "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, + "facts": deduplicate_l4_facts(facts, pending=False, now=current_time), + "pending_facts": deduplicate_l4_facts( + pending, + pending=True, + now=current_time, + ), + } + + +def normalize_facts_memory_field( + *, + key, + field, + session_id: str = "", +) -> dict | None: + if not isinstance(field, dict): + return None + + normalized_key = normalize_l4_key(key) + content = normalize_l4_text(field.get("content") or field.get("value")) + if not normalized_key or not content: + return None + + status = normalize_l4_key(field.get("l4_status")) + if status == "analized": + status = L4_FIELD_STATUS_ANALYZED + if status not in {L4_FIELD_STATUS_PENDING, L4_FIELD_STATUS_ANALYZED}: + status = L4_FIELD_STATUS_PENDING + + return { + **field, + "content": content, + "runtime_snapshot_id": normalize_l4_text(field.get("runtime_snapshot_id")), + "session_id": normalize_l4_text(field.get("session_id") or session_id), + "l4_status": status, + "l4_content_hash": normalize_l4_text(field.get("l4_content_hash")) + or build_l4_content_hash(content), + "l4_analyzed_at": ( + normalize_l4_text(field.get("l4_analyzed_at")) + if status == L4_FIELD_STATUS_ANALYZED + else "" + ), + } + + +def normalize_facts_memory_records(value) -> list[dict]: + if not isinstance(value, list): + return [] + + records = [] + for raw_record in value: + if not isinstance(raw_record, dict): + continue + + session_id = normalize_l4_text(raw_record.get("session_id")) + storage_key = normalize_l4_text( + raw_record.get("storage_key") or raw_record.get("key") + ) + raw_signals = raw_record.get("signals") + if not isinstance(raw_signals, dict): + continue + + signals = {} + for key, field in raw_signals.items(): + normalized_key = normalize_l4_key(key) + normalized_field = normalize_facts_memory_field( + key=normalized_key, + field=field, + session_id=session_id, + ) + if normalized_field is not None: + signals[normalized_key] = normalized_field + + if signals: + records.append({ + "storage_key": storage_key, + "session_id": session_id, + "signal_count": len(signals), + "signals": signals, + }) + + return records + + +def collect_pending_facts_memory_fields(records: list[dict]) -> list[dict]: + pending = [] + seen = set() + + for record in normalize_facts_memory_records(records): + session_id = record.get("session_id", "") + for key, field in record.get("signals", {}).items(): + if field.get("l4_status") != L4_FIELD_STATUS_PENDING: + continue + + identity = (session_id, key, field.get("l4_content_hash", "")) + if identity in seen: + continue + seen.add(identity) + pending.append({ + "key": key, + "content": field.get("content", ""), + "runtime_snapshot_id": field.get("runtime_snapshot_id", ""), + "session_id": field.get("session_id") or session_id, + "l4_content_hash": field.get("l4_content_hash", ""), + }) + + return pending + + +def mark_facts_memory_fields_analyzed( + records: list[dict], + fields: list[dict], + *, + now: str | None = None, +) -> tuple[list[dict], bool]: + analyzed_at = now or utc_now_iso() + identities = { + ( + normalize_l4_text(field.get("session_id")), + normalize_l4_key(field.get("key")), + normalize_l4_text(field.get("l4_content_hash")), + ) + for field in fields + if isinstance(field, dict) + } + updated_records = normalize_facts_memory_records(records) + changed = False + + for record in updated_records: + session_id = record.get("session_id", "") + for key, field in record.get("signals", {}).items(): + identity = ( + field.get("session_id") or session_id, + key, + field.get("l4_content_hash", ""), + ) + if identity not in identities: + continue + if field.get("l4_status") != L4_FIELD_STATUS_ANALYZED: + field["l4_status"] = L4_FIELD_STATUS_ANALYZED + field["l4_analyzed_at"] = analyzed_at + changed = True + + return updated_records, changed + + +def extract_l4_json_payload(text: str) -> dict | None: + source = str(text or "").strip() + if not source: + return None + + candidates = [source] + candidates.extend( + re.findall( + r"```(?:json)?\s*(\{.*?\})\s*```", + source, + flags=re.IGNORECASE | re.DOTALL, + ) + ) + + first_brace = source.find("{") + last_brace = source.rfind("}") + if first_brace >= 0 and last_brace > first_brace: + candidates.append(source[first_brace:last_brace + 1]) + + for candidate in candidates: + try: + payload = json.loads(candidate) + except (TypeError, ValueError, json.JSONDecodeError): + continue + if isinstance(payload, dict): + return payload + + return None + + +def normalize_l4_candidates( + payload, + *, + source_fields: list[dict], + now: str | None = None, +) -> list[dict]: + if not isinstance(payload, dict) or not isinstance(payload.get("facts"), list): + return [] + + current_time = now or utc_now_iso() + fields_by_key: dict[str, list[dict]] = {} + for field in source_fields: + if not isinstance(field, dict): + continue + key = normalize_l4_key(field.get("key")) + if key: + fields_by_key.setdefault(key, []).append(field) + + candidates = [] + for raw_candidate in payload["facts"]: + if not isinstance(raw_candidate, dict): + continue + + source_keys = normalize_l4_string_list(raw_candidate.get("source_keys")) + source_keys = [key for key in map(normalize_l4_key, source_keys) if key in fields_by_key] + if not source_keys and len(fields_by_key) == 1: + source_keys = list(fields_by_key) + if not source_keys: + continue + + matched_fields = [ + field + for source_key in source_keys + for field in fields_by_key[source_key] + ] + candidate = normalize_l4_fact( + { + **raw_candidate, + "source_keys": source_keys, + "source_session_ids": [ + field.get("session_id", "") for field in matched_fields + ], + "source_runtime_snapshot_ids": [ + field.get("runtime_snapshot_id", "") for field in matched_fields + ], + "created_at": current_time, + "updated_at": current_time, + }, + pending=True, + now=current_time, + ) + if candidate is not None: + candidates.append(candidate) + + return deduplicate_l4_facts(candidates, pending=True, now=current_time) + + +def add_l4_pending_candidates( + store, + candidates: list[dict], + *, + now: str | None = None, +) -> tuple[dict, dict]: + current_time = now or utc_now_iso() + next_store = normalize_l4_store(store, now=current_time) + pending = list(next_store["pending_facts"]) + by_id = {fact["id"]: index for index, fact in enumerate(pending)} + added_ids = [] + reinforced_ids = [] + + for raw_candidate in candidates: + candidate = normalize_l4_fact(raw_candidate, pending=True, now=current_time) + if candidate is None: + continue + + index = by_id.get(candidate["id"]) + if index is None: + by_id[candidate["id"]] = len(pending) + pending.append(candidate) + added_ids.append(candidate["id"]) + else: + pending[index] = merge_same_l4_fact( + pending[index], + candidate, + now=current_time, + ) + reinforced_ids.append(candidate["id"]) + + changed = bool(added_ids or reinforced_ids) + if changed: + next_store["pending_facts"] = pending + next_store["revision"] += 1 + next_store["updated_at"] = current_time + + return next_store, { + "added_pending_ids": added_ids, + "reinforced_pending_ids": reinforced_ids, + "pending_count": len(next_store["pending_facts"]), + "changed": changed, + } + + +def normalize_l4_merge_operations(payload) -> list[dict]: + if not isinstance(payload, dict) or not isinstance(payload.get("operations"), list): + return [] + + operations = [] + for raw_operation in payload["operations"]: + if not isinstance(raw_operation, dict): + continue + + action = normalize_l4_key(raw_operation.get("action")) + pending_id = normalize_l4_text(raw_operation.get("pending_id")) + if action not in L4_ALLOWED_MERGE_ACTIONS or not pending_id: + continue + + operation = { + "action": action, + "pending_id": pending_id, + } + target_id = normalize_l4_text(raw_operation.get("target_id")) + if target_id: + operation["target_id"] = target_id + + for key in ("key", "value", "category", "confidence"): + if key in raw_operation: + operation[key] = raw_operation[key] + + operations.append(operation) + + return operations + + +def validate_l4_merge_operations(store, operations: list[dict]) -> tuple[bool, str]: + normalized_store = normalize_l4_store(store) + pending = normalized_store["pending_facts"] + pending_ids = {fact["id"] for fact in pending} + fact_ids = {fact["id"] for fact in normalized_store["facts"]} + + if len(operations) != len(pending): + return False, "operation_count_mismatch" + + seen = set() + for operation in operations: + pending_id = operation.get("pending_id") + if pending_id not in pending_ids: + return False, "unknown_pending_id" + if pending_id in seen: + return False, "duplicate_pending_operation" + seen.add(pending_id) + + action = operation.get("action") + if action in {"update", "reinforce"}: + if operation.get("target_id") not in fact_ids: + return False, "unknown_target_id" + + if seen != pending_ids: + return False, "missing_pending_operation" + + return True, "" + + +def merge_fact_sources(existing: dict, incoming: dict) -> dict: + return { + "source_session_ids": merge_l4_string_lists( + existing.get("source_session_ids"), + incoming.get("source_session_ids"), + ), + "source_runtime_snapshot_ids": merge_l4_string_lists( + existing.get("source_runtime_snapshot_ids"), + incoming.get("source_runtime_snapshot_ids"), + ), + "source_keys": merge_l4_string_lists( + existing.get("source_keys"), + incoming.get("source_keys"), + ), + } + + +def apply_l4_merge_operations( + store, + operations: list[dict], + *, + now: str | None = None, +) -> tuple[dict, dict]: + current_time = now or utc_now_iso() + base_store = normalize_l4_store(store, now=current_time) + valid, reason = validate_l4_merge_operations(base_store, operations) + if not valid: + return base_store, { + "valid": False, + "reason": reason, + "changed": False, + } + + facts = [dict(fact) for fact in base_store["facts"]] + facts_by_id = {fact["id"]: index for index, fact in enumerate(facts)} + pending_by_id = {fact["id"]: fact for fact in base_store["pending_facts"]} + added_ids = [] + updated_ids = [] + reinforced_ids = [] + ignored_ids = [] + + for operation in operations: + action = operation["action"] + pending = pending_by_id[operation["pending_id"]] + + if action == "ignore": + ignored_ids.append(pending["id"]) + continue + + if action == "reinforce": + index = facts_by_id[operation["target_id"]] + target = facts[index] + target.update(merge_fact_sources(target, pending)) + target["confidence"] = max( + clamp_float(target.get("confidence"), default=1.0), + clamp_float(pending.get("confidence"), default=1.0), + ) + target["mention_count"] = max(1, int(target.get("mention_count") or 1)) + max( + 1, + int(pending.get("mention_count") or 1), + ) + target["updated_at"] = current_time + reinforced_ids.append(target["id"]) + continue + + if action == "update": + index = facts_by_id[operation["target_id"]] + target = facts[index] + candidate = normalize_l4_fact( + { + **pending, + **{ + key: operation[key] + for key in ("key", "value", "category", "confidence") + if key in operation + }, + "id": target["id"], + "created_at": target.get("created_at"), + "updated_at": current_time, + **merge_fact_sources(target, pending), + "mention_count": max(1, int(target.get("mention_count") or 1)) + + max(1, int(pending.get("mention_count") or 1)), + }, + now=current_time, + ) + if candidate is None: + return base_store, { + "valid": False, + "reason": "invalid_update_payload", + "changed": False, + } + facts[index] = candidate + updated_ids.append(candidate["id"]) + continue + + candidate = normalize_l4_fact( + { + **pending, + **{ + key: operation[key] + for key in ("key", "value", "category", "confidence") + if key in operation + }, + "id": "", + "created_at": current_time, + "updated_at": current_time, + }, + now=current_time, + ) + if candidate is None: + return base_store, { + "valid": False, + "reason": "invalid_create_payload", + "changed": False, + } + + if any(fact["key"] == candidate["key"] for fact in facts): + return base_store, { + "valid": False, + "reason": "create_key_already_exists", + "changed": False, + } + + facts_by_id[candidate["id"]] = len(facts) + facts.append(candidate) + added_ids.append(candidate["id"]) + + next_store = { + **base_store, + "facts": facts, + "pending_facts": [], + "revision": base_store["revision"] + 1, + "updated_at": current_time, + } + + return next_store, { + "valid": True, + "added_ids": added_ids, + "updated_ids": updated_ids, + "reinforced_ids": reinforced_ids, + "ignored_pending_ids": ignored_ids, + "processed_pending_ids": sorted(pending_by_id), + "total_facts": len(facts), + "pending_count": 0, + "changed": True, + } + + +def delete_l4_fact_from_store( + store, + fact_id: str, + *, + now: str | None = None, +) -> tuple[dict, bool]: + current_time = now or utc_now_iso() + next_store = normalize_l4_store(store, now=current_time) + target_id = normalize_l4_text(fact_id) + remaining = [fact for fact in next_store["facts"] if fact.get("id") != target_id] + if len(remaining) == len(next_store["facts"]): + return next_store, False + + next_store["facts"] = remaining + next_store["revision"] += 1 + next_store["updated_at"] = current_time + return next_store, True + + +def format_l4_fact_metadata_suffixes(fact: dict) -> list[str]: + metadata = [] + for key in ( + "id", + "category", + "confidence", + "mention_count", + "source_session_ids", + "source_runtime_snapshot_ids", + "source_keys", + "created_at", + "updated_at", + ): + value = fact.get(key) + if isinstance(value, list): + value = ", ".join(str(item) for item in value if str(item).strip()) + if value in (None, "", []): + continue + if isinstance(value, float): + value = f"{value:.2f}" + metadata.append(f"[{key}: {value}]") + return metadata + + +def format_l4_fact_line(fact: dict, *, include_metadata: bool = True) -> str: + key = normalize_l4_text(fact.get("key")) + value = normalize_l4_text(fact.get("value")) + if not key or not value: + return "" + + line = f"{key}: {value}" + if include_metadata: + suffixes = format_l4_fact_metadata_suffixes(fact) + if suffixes: + line = f"{line} {' '.join(suffixes)}" + return line + + +def format_long_term_memory_context(facts: list[dict]) -> str: + lines = [ + format_l4_fact_line(fact, include_metadata=False) + for fact in facts + ] + lines = [line for line in lines if line] + if not lines: + return "" + + body = "\n".join(escape(line) for line in lines) + return f"\n{body}\n" + + +def clone_l4_store(store) -> dict: + return deepcopy(normalize_l4_store(store)) diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 90fdfc18..f4e7c2bb 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -167,6 +167,16 @@ class RuntimeContext: default_factory=dict ) + runtime_facts_memory_records: list[dict] = field( + default_factory=list + ) + + runtime_long_term_memory_store: dict = field( + default_factory=dict + ) + + runtime_l4_memory_update_task: object | None = None + runtime_usage_events: list[dict] = field( default_factory=list ) diff --git a/runtime/stream.py b/runtime/stream.py index 78e345f7..7b1b73f1 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -57,6 +57,7 @@ normalize_skill_name, ) from utils.session_actions_history import ( + attach_session_action_jin_message_since, build_context_limit_history_text, build_delayed_memory_save_rejected_history_text, build_reasoning_loop_history_text, @@ -2643,7 +2644,21 @@ async def run( or marker_history_replaced ) - if history_compacted: + history_message_attached = False + + if counted_markers: + history_message_attached = ( + attach_session_action_jin_message_since( + self.context, + session_action_history_start, + self.stream.response, + ) + ) + + if ( + history_compacted + or history_message_attached + ): with contextlib.suppress( Exception ): diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index e2b12d22..0c2edc73 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -346,7 +346,7 @@ async def test_current_sequence_starts_with_original_user_message(self): "INITIAL_SEQUENCE_INSTRUCTION: keep <this> in delayed memory ( 10s ago )\n" "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" " --- Sequence started ---\n" - " JIN message 1 executed - LIST_SKILLS ( 5s ago )\n" + " JIN message 1 executed: LIST_SKILLS ( 5s ago )\n" "", prompt, ) @@ -692,8 +692,8 @@ async def test_followup_places_current_sequence_under_latest_request(self): "INITIAL_SEQUENCE_INSTRUCTION: first list skills, then append one ( 1m ago )\n" "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" " --- Sequence started ---\n" - " JIN message 1 executed - LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed - APPEND_SKILL ( 2s ago )\n" + " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" + " JIN message 2 executed: APPEND_SKILL ( 2s ago )\n" "", prompt, ) @@ -758,8 +758,8 @@ async def test_idle_followup_keeps_original_sequence_action_history(self): "INITIAL_SEQUENCE_INSTRUCTION: first idle 30s, then idle 20s and search ( 32s ago )\n" "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" " --- Sequence started ---\n" - " JIN message 1 executed - IDLE - 30s ( 32s ago )\n" - " JIN message 2 executed - IDLE - 20s, WEB_SEARCH ( 1s ago )\n" + " JIN message 1 executed: IDLE: 30s ( 32s ago )\n" + " JIN message 2 executed: IDLE: 20s, WEB_SEARCH ( 1s ago )\n" "", prompt, ) @@ -2636,7 +2636,7 @@ async def fake_run_brain_stream(**kwargs): prompt, ) self.assertIn( - "JIN message 1 executed - IDLE - 30s ( 30s ago )", + "JIN message 1 executed: IDLE: 30s ( 30s ago )", prompt, ) self.assertNotIn( diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 192b0b42..1d62586f 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -309,7 +309,7 @@ def test_current_action_age_starts_at_one_second(self): ) self.assertIn( - "JIN message 1 executed - ASSET_ACTION ( 1s ago )", + "JIN message 1 executed: ASSET_ACTION ( 1s ago )", history, ) self.assertNotIn( @@ -383,21 +383,98 @@ def test_action_context_hides_single_emission_counts(self): history, ) self.assertIn( - "JIN message 1 executed - LIST_SKILLS ( 5s ago )", + "JIN message 1 executed: LIST_SKILLS ( 5s ago )", history, ) self.assertIn( ( - "JIN message 2 executed - APPEND_SKILL: file_manager (count: 3), " + "JIN message 2 executed: APPEND_SKILL: file_manager (count: 3), " "CLEAN_TOOL_RESULTS ( 2s ago )" ), history, ) self.assertIn( - "JIN message 3 executed - SAVE_SESSION ( 1s ago )", + "JIN message 3 executed: SAVE_SESSION ( 1s ago )", history, ) + def test_current_sequence_includes_jin_content_for_marker_message_only(self): + + jin_content = ( + "Приятно познакомиться, Сергей. " + "Сейчас посмотрю, что это за проект Ouroboros." + ) + context = SimpleNamespace( + runtime_current_turn_id="turn_000002", + runtime_turn_started_at=900.0, + runtime_action_sequence_turn_ids=[ + "turn_000002", + ], + runtime_session_action_history=[ + { + "text": ( + "WEB_SEARCH - Ouroboros AI project framework " + "competitor LLM agents" + ), + "parts": [ + { + "text": "WEB_SEARCH", + "detail": ( + "Ouroboros AI project framework " + "competitor LLM agents" + ), + }, + ], + "created_at": 999.0, + "runtime_turn_id": "turn_000002", + "jin_message_content": jin_content, + }, + ], + ) + + with patch( + "utils.context.context_exports.time.time", + return_value=1000.0, + ): + current_sequence = build_session_actions_history_context( + context, + current_sequence=True, + ) + session_history = build_session_actions_history_context( + context, + ) + + self.assertIn( + ( + "JIN message 1 content: " + f"{jin_content} ( 1s ago )" + ), + current_sequence, + ) + self.assertIn( + ( + "JIN message 1 executed: WEB_SEARCH: " + "Ouroboros AI project framework competitor LLM agents " + "( 1s ago )" + ), + current_sequence, + ) + self.assertIn( + ( + "1. WEB_SEARCH: Ouroboros AI project framework " + "competitor LLM agents ( 1s ago )" + ), + session_history, + ) + self.assertNotIn( + "JIN message 1 content", + session_history, + ) + self.assertNotIn( + jin_content, + session_history, + ) + def test_current_actions_history_filters_older_session_actions(self): context = SimpleNamespace( @@ -444,8 +521,8 @@ def test_current_actions_history_filters_older_session_actions(self): ( "\n" " --- Sequence started ---\n" - " JIN message 1 executed - LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed - APPEND_SKILL ( 2s ago )\n" + " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" + " JIN message 2 executed: APPEND_SKILL ( 2s ago )\n" "" ), ) @@ -502,9 +579,9 @@ def test_current_sequence_expands_memory_action_payloads_without_counts(self): self.assertIn( ( - "JIN message 1 executed - RESOLVE_ACTIVE_MEMORY - " + "JIN message 1 executed: RESOLVE_ACTIVE_MEMORY: " "id: enrrqo; content: word: кукушка, " - "RESOLVE_ACTIVE_MEMORY - id: yfpywn; " + "RESOLVE_ACTIVE_MEMORY: id: yfpywn; " "content: word: кулёк ( 2s ago )" ), history, diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 1ca83923..b2ce105c 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -1754,7 +1754,10 @@ def test_session_history_includes_saved_content_title(self): expected_text, ) self.assertIn( - f"JIN message 1 executed - {expected_text}", + ( + "JIN message 1 executed: " + f"{expected_text.replace(' - ', ': ', 1)}" + ), build_session_actions_history_context( context, current_sequence=True, diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py new file mode 100644 index 00000000..7b97df2e --- /dev/null +++ b/tests/test_l4_memory.py @@ -0,0 +1,435 @@ +import unittest + +from runtime.L4_memory import maybe_update_runtime_l4_memory +from runtime.L4_memory_utils import ( + add_l4_pending_candidates, + apply_l4_merge_operations, + build_l4_fact_id, + collect_pending_facts_memory_fields, + extract_l4_json_payload, + format_l4_fact_line, + format_long_term_memory_context, + mark_facts_memory_fields_analyzed, + normalize_facts_memory_records, + normalize_l4_candidates, + normalize_l4_merge_operations, + normalize_l4_store, +) +from runtime.runtime_context import RuntimeContext +from rules.brain_context_builder import build_brain_context +from tests.helpers.memory import FakeLogger, FakeServiceClient + + +class FakeEmitter: + + def __init__(self): + self.events = [] + + async def emit(self, payload): + self.events.append(payload) + + +class L4MemoryTests(unittest.IsolatedAsyncioTestCase): + + def test_facts_memory_normalization_adds_session_and_pending_status(self): + records = normalize_facts_memory_records([ + { + "storage_key": "jin.factsMemory.session-a.v1", + "session_id": "session-a", + "signals": { + "User preference": { + "content": "Prefers concise plans.", + "runtime_snapshot_id": "runtime_001", + }, + }, + }, + ]) + + field = records[0]["signals"]["user_preference"] + self.assertEqual(field["session_id"], "session-a") + self.assertEqual(field["runtime_snapshot_id"], "runtime_001") + self.assertEqual(field["l4_status"], "pending") + self.assertTrue(field["l4_content_hash"]) + + def test_mark_analyzed_only_marks_matching_content_hash(self): + records = normalize_facts_memory_records([ + { + "session_id": "session-a", + "signals": { + "gpu": {"content": "RTX 3080 Ti"}, + "language": {"content": "Russian"}, + }, + }, + ]) + pending = collect_pending_facts_memory_fields(records) + + updated, changed = mark_facts_memory_fields_analyzed( + records, + [pending[0]], + now="2026-08-02T12:00:00Z", + ) + + self.assertTrue(changed) + self.assertEqual( + updated[0]["signals"][pending[0]["key"]]["l4_status"], + "analyzed", + ) + other_key = "language" if pending[0]["key"] == "gpu" else "gpu" + self.assertEqual(updated[0]["signals"][other_key]["l4_status"], "pending") + + def test_json_extraction_accepts_fenced_json(self): + payload = extract_l4_json_payload( + "text\n```json\n{\"facts\": []}\n```" + ) + self.assertEqual(payload, {"facts": []}) + + def test_candidates_include_only_metadata_from_referenced_sources(self): + source_fields = [ + { + "key": "gpu", + "content": "RTX 3080 Ti", + "session_id": "session-a", + "runtime_snapshot_id": "runtime_001", + }, + { + "key": "language", + "content": "Russian", + "session_id": "session-b", + "runtime_snapshot_id": "runtime_002", + }, + ] + candidates = normalize_l4_candidates( + { + "facts": [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + "confidence": 0.9, + "source_keys": ["gpu"], + }, + ], + }, + source_fields=source_fields, + now="2026-08-02T12:00:00Z", + ) + + self.assertEqual(len(candidates), 1) + self.assertEqual(candidates[0]["source_session_ids"], ["session-a"]) + self.assertEqual( + candidates[0]["source_runtime_snapshot_ids"], + ["runtime_001"], + ) + self.assertEqual(candidates[0]["source_keys"], ["gpu"]) + + def test_pending_candidates_accumulate_without_touching_final_memory(self): + store = normalize_l4_store({ + "facts": [ + { + "key": "project.identity", + "value": "JIN is a local runtime.", + }, + ], + }) + store, change = add_l4_pending_candidates( + store, + [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + "source_keys": ["gpu"], + }, + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + "source_keys": ["hardware"], + }, + ], + now="2026-08-02T12:00:00Z", + ) + + self.assertTrue(change["changed"]) + self.assertEqual(len(store["facts"]), 1) + self.assertEqual(len(store["pending_facts"]), 1) + self.assertEqual( + store["pending_facts"][0]["source_keys"], + ["gpu", "hardware"], + ) + + def test_merge_requires_one_valid_operation_for_every_pending_fact(self): + store, _ = add_l4_pending_candidates( + normalize_l4_store({}), + [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + }, + ], + now="2026-08-02T12:00:00Z", + ) + before = normalize_l4_store(store, now="2026-08-02T12:00:00Z") + + after, change = apply_l4_merge_operations( + store, + [], + now="2026-08-02T12:01:00Z", + ) + + self.assertFalse(change["valid"]) + self.assertEqual(change["reason"], "operation_count_mismatch") + self.assertEqual(after["facts"], before["facts"]) + self.assertEqual(after["pending_facts"], before["pending_facts"]) + + def test_merge_applies_complete_batch_atomically(self): + existing = normalize_l4_store({ + "facts": [ + { + "id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + }, + ], + }) + store, _ = add_l4_pending_candidates( + existing, + [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment", + "source_keys": ["gpu"], + }, + { + "key": "session.current_task", + "value": "User is fixing a bubble today.", + "source_keys": ["current_task"], + }, + ], + now="2026-08-02T12:00:00Z", + ) + gpu_pending, task_pending = store["pending_facts"] + operations = normalize_l4_merge_operations({ + "operations": [ + { + "action": "update", + "pending_id": gpu_pending["id"], + "target_id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment", + "confidence": 0.95, + }, + { + "action": "ignore", + "pending_id": task_pending["id"], + }, + ], + }) + + merged, change = apply_l4_merge_operations( + store, + operations, + now="2026-08-02T12:01:00Z", + ) + + self.assertTrue(change["valid"]) + self.assertEqual(merged["pending_facts"], []) + self.assertEqual(len(merged["facts"]), 1) + self.assertEqual(merged["facts"][0]["id"], "l4_gpu") + self.assertIn("RTX 4090", merged["facts"][0]["value"]) + self.assertEqual(change["ignored_pending_ids"], [task_pending["id"]]) + + def test_store_does_not_truncate_values_or_fact_count(self): + long_value = "fact " * 1000 + raw_facts = [ + { + "key": f"project.fact_{index}", + "value": long_value + str(index), + } + for index in range(350) + ] + + store = normalize_l4_store({"facts": raw_facts}) + + self.assertEqual(len(store["facts"]), 350) + self.assertEqual(store["facts"][0]["value"], (long_value + "0").strip()) + + def test_context_formats_all_facts_without_metadata(self): + store = normalize_l4_store({ + "facts": [ + { + "id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment", + "source_session_ids": ["session-a"], + }, + { + "id": "l4_style", + "key": "user.preference.response_style", + "value": "User prefers direct technical analysis.", + }, + ], + }) + + context_block = format_long_term_memory_context(store["facts"]) + ui_line = format_l4_fact_line(store["facts"][0], include_metadata=True) + + self.assertIn("user.hardware.main_gpu:", context_block) + self.assertIn("user.preference.response_style:", context_block) + self.assertNotIn("source_session_ids", context_block) + self.assertIn("[source_session_ids: session-a]", ui_line) + + def test_brain_context_always_injects_complete_long_term_memory(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + }, + { + "key": "user.preference.response_style", + "value": "User prefers direct technical analysis.", + }, + ], + }) + + prompt = build_brain_context( + context, + runtime_actions={}, + user_input="напиши хайку про дождь", + include_runtime_action_instructions=False, + include_previous_chat_messages=False, + ) + + self.assertIn(" { + const text = normalizeText(item); + if (!text || seen.has(text)) { + return; + } + seen.add(text); + result.push(text); + }); + + return result; + } + + function normalizeNumber(value, fallback) { + const number = Number(value); + return Number.isFinite(number) ? number : fallback; + } + + function normalizeFact(value, pending) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + + const key = normalizeText(value.key); + const factValue = normalizeText(value.value || value.content); + if (!key || !factValue) { + return null; + } + + const idPrefix = pending ? "l4p_" : "l4_"; + const rawId = normalizeText(value.id); + const generatedId = storage.buildFactsMemoryContentHash( + `${key}\n${factValue}` + ).replace(/^h/, ""); + + return { + id: rawId.startsWith(idPrefix) ? rawId : `${idPrefix}${generatedId}`, + key, + value: factValue, + category: normalizeText(value.category || "other") || "other", + confidence: Math.max( + 0, + Math.min(1, normalizeNumber(value.confidence, 1)) + ), + mention_count: Math.max( + 1, + Math.floor(normalizeNumber(value.mention_count, 1)) + ), + created_at: normalizeText(value.created_at), + updated_at: normalizeText(value.updated_at), + source_session_ids: normalizeList( + value.source_session_ids || value.source_session_id + ), + source_runtime_snapshot_ids: normalizeList( + value.source_runtime_snapshot_ids || value.source_runtime_snapshot_id + ), + source_keys: normalizeList(value.source_keys), + }; + } + + function normalizeFacts(values, pending) { + const result = []; + const seen = new Set(); + + (Array.isArray(values) ? values : []).forEach((value) => { + const fact = normalizeFact(value, pending); + if (!fact || seen.has(fact.id)) { + return; + } + seen.add(fact.id); + result.push(fact); + }); + + return result; + } + + function normalizeStore(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return { + version: 1, + revision: 0, + updated_at: "", + facts: [], + pending_facts: [], + }; + } + + return { + version: 1, + revision: Math.max( + 0, + Math.floor(normalizeNumber(value.revision, 0)) + ), + updated_at: normalizeText(value.updated_at), + facts: normalizeFacts(value.facts, false), + pending_facts: normalizeFacts(value.pending_facts, true), + }; + } + + function readStore() { + return normalizeStore( + storage.readBrowserMemory(longTermFactsStorageKey) + ); + } + + function writeStore(store) { + const normalized = normalizeStore(store); + storage.writeBrowserMemory(longTermFactsStorageKey, normalized); + return normalized; + } + + function getFacts() { + return readStore().facts; + } + + function getPendingFacts() { + return readStore().pending_facts; + } + + function sendIfOpen(payload) { + if (typeof window.sendSocketMessage !== "function") { + return false; + } + return window.sendSocketMessage(payload); + } + + function buildFactsMemorySyncPayload() { + return { + type: "facts_memory_store_sync", + records: storage.collectFactsMemoryRecords + ? storage.collectFactsMemoryRecords() + : [], + }; + } + + function buildStoreSyncPayload() { + return { + type: "l4_memory_store_sync", + store: readStore(), + }; + } + + function syncFactsMemoryToRuntime() { + return sendIfOpen(buildFactsMemorySyncPayload()); + } + + function syncLongTermMemoryToRuntime() { + return sendIfOpen(buildStoreSyncPayload()); + } + + function applyFactsMemoryRecordsUpdate(payload) { + const records = payload && Array.isArray(payload.records) + ? payload.records + : []; + + records.forEach((record) => { + if ( + !record + || typeof record !== "object" + || Array.isArray(record) + || !record.session_id + || !record.signals + || typeof record.signals !== "object" + || Array.isArray(record.signals) + ) { + return; + } + + storage.writeFactsMemory(record.signals, record.session_id); + }); + + if ( + window.JinRuntime.runtime + && window.JinRuntime.runtime.renderRuntimeMemorySnapshot + ) { + window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); + } + } + + function applyServerUpdate(payload) { + const incoming = normalizeStore(payload && payload.store); + const local = readStore(); + + if (incoming.revision < local.revision) { + return local; + } + + const store = writeStore(incoming); + if ( + window.JinRuntime.runtime + && window.JinRuntime.runtime.renderRuntimeMemorySnapshot + ) { + window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); + } + return store; + } + + function requestFactDelete(factId) { + const id = normalizeText(factId); + if (!id) { + return false; + } + return sendIfOpen({ + type: "l4_memory_delete_fact", + fact_id: id, + }); + } + + function maybeSendIdleTick() { + if ( + typeof window.getJinUserIdleContext !== "function" + || typeof window.isJinGenerationRunning !== "function" + || window.isJinGenerationRunning() + ) { + return false; + } + + const idleContext = window.getJinUserIdleContext(); + if (!idleContext) { + return false; + } + + const now = Date.now(); + if (now - lastIdleTickAt < idleTickIntervalMs) { + return false; + } + + const sent = sendIfOpen({ + type: "l4_memory_idle_tick", + user_idle_seconds: Math.floor( + Number(idleContext.user_idle_seconds || 0) + ), + records: storage.collectFactsMemoryRecords + ? storage.collectFactsMemoryRecords() + : [], + store: readStore(), + }); + + if (sent) { + lastIdleTickAt = now; + } + return sent; + } + + function startIdleMonitor() { + if (idleTimer !== null) { + return; + } + idleTimer = window.setInterval(maybeSendIdleTick, 5000); + } + + const api = { + readStore, + writeStore, + normalizeStore, + getFacts, + getPendingFacts, + buildFactsMemorySyncPayload, + buildStoreSyncPayload, + syncFactsMemoryToRuntime, + syncLongTermMemoryToRuntime, + applyFactsMemoryRecordsUpdate, + applyServerUpdate, + requestFactDelete, + maybeSendIdleTick, + startIdleMonitor, + }; + + window.JINRuntimeL4Memory = api; + window.JinRuntime.l4Memory = api; + window.syncFactsMemoryToRuntime = syncFactsMemoryToRuntime; + window.syncLongTermMemoryToRuntime = syncLongTermMemoryToRuntime; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", startIdleMonitor, { + once: true, + }); + } else { + window.setTimeout(startIdleMonitor, 0); + } +}()); diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index dbcf6c37..af4b2a3d 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -13,6 +13,8 @@ let getDelayedMemoryReports = null; let getFactsMemoryFields = null; let deleteFactsMemoryField = null; + let getLongTermMemoryFacts = null; + let deleteLongTermMemoryFact = null; let getDisplayMode = null; let setDisplayMode = null; @@ -204,6 +206,40 @@ }); } + function getLongTermMemoryFactRecords() { + const facts = + typeof getLongTermMemoryFacts === "function" + ? getLongTermMemoryFacts() + : []; + + if (!Array.isArray(facts)) { + return []; + } + + return facts + .filter(fact => ( + fact + && typeof fact === "object" + && !Array.isArray(fact) + && String(fact.key || "").trim() + && String(fact.value || "").trim() + )) + .sort((left, right) => { + const updatedDifference = + String(right.updated_at || "").localeCompare( + String(left.updated_at || "") + ); + + if (updatedDifference) { + return updatedDifference; + } + + return String(left.key || "").localeCompare( + String(right.key || "") + ); + }); + } + function getAvailableRuntimeMemoryDisplayModes() { const modes = [ "runtime", @@ -227,6 +263,12 @@ ); } + if (getLongTermMemoryFactRecords().length > 0) { + modes.push( + "long_term" + ); + } + return modes; } @@ -271,7 +313,9 @@ ? "[ delayed memory ]" : displayMode === "facts" ? "[ facts memory ]" - : "[ runtime memory ]"; + : displayMode === "long_term" + ? "[ long term memory ]" + : "[ runtime memory ]"; const hasAlternativeMemory = modes.length > 1; @@ -699,6 +743,11 @@ return; } + if (getRuntimeMemoryDisplayMode() === "long_term") { + renderLongTermMemoryFacts(); + return; + } + const sourceSnapshot = runtimeMemoryHistory.snapshots[ runtimeMemoryHistory.index @@ -1141,6 +1190,11 @@ row, line ); + } else if (options.interactiveLongTermMemory) { + configureLongTermMemoryRow( + row, + line + ); } else if (options.interactiveRuntimeMemory) { configureRuntimeMemoryRow( row, @@ -1456,8 +1510,8 @@ } function configureFactsMemoryRow( - row, - line + row, + line ) { if ( !row @@ -1479,6 +1533,30 @@ ); } + function configureLongTermMemoryRow( + row, + line + ) { + if ( + !row + || !line + || !line.id + ) { + return; + } + + configureRuntimeMemoryDeleteHold( + row, + () => { + if (typeof deleteLongTermMemoryFact === "function") { + deleteLongTermMemoryFact( + line.id + ); + } + } + ); + } + function configureRuntimeMemoryDeleteHold( row, onDelete @@ -2071,6 +2149,10 @@ content, [ `runtime_snapshot_id: ${String(record.runtime_snapshot_id || "").trim()}`, + `session_id: ${String(record.session_id || "").trim()}`, + `l4_status: ${String(record.l4_status || "pending").trim()}`, + `l4_content_hash: ${String(record.l4_content_hash || "").trim()}`, + `l4_analyzed_at: ${String(record.l4_analyzed_at || "").trim()}`, ] ), status: "same", @@ -2130,6 +2212,136 @@ } + function formatLongTermFactMetadata( + fact + ) { + + const entries = []; + + [ + "id", + "category", + "confidence", + "mention_count", + "source_session_ids", + "source_runtime_snapshot_ids", + "source_keys", + "created_at", + "updated_at", + ].forEach((key) => { + let value = + fact[key]; + + if (Array.isArray(value)) { + value = + value + .map(item => String(item || "").trim()) + .filter(Boolean) + .join(", "); + } + + if ( + value === undefined + || value === null + || String(value).trim() === "" + ) { + return; + } + + if (typeof value === "number") { + value = + value.toFixed(2).replace(/\.00$/, ""); + } + + entries.push( + `${key}: ${value}` + ); + }); + + return entries; + + } + + + function buildLongTermMemoryLine( + fact + ) { + + const key = + String(fact.key || "").trim(); + const value = + String(fact.value || "").trim(); + + return { + id: String(fact.id || "").trim(), + key, + value: memoryModel.appendProperties( + value, + formatLongTermFactMetadata(fact) + ), + status: "same", + key_status: "same", + value_status: "same", + key_change_ratio: 0, + value_change_ratio: 0, + }; + + } + + + function renderLongTermMemoryFacts() { + const records = + getLongTermMemoryFactRecords(); + + const lines = + records.map( + buildLongTermMemoryLine + ); + + if (runtimeMemoryText) { + runtimeMemoryText.innerHTML = ""; + runtimeMemoryText.classList.remove( + "runtime-memory-text-pinned" + ); + runtimeMemoryText.removeAttribute( + "title" + ); + + if (!lines.length) { + runtimeMemoryText.textContent = + "No long-term facts stored."; + } else { + appendRuntimeMemoryLineRows( + lines, + false, + { + applyFlash: false, + interactiveLongTermMemory: true, + } + ); + } + } + + if (runtimeMemoryPosition) { + runtimeMemoryPosition.textContent = + String(records.length); + } + + userIdleValueNode = null; + idle.stop(); + + updateRuntimeMemoryTitleMetricsFromText( + lines + .map(line => `${line.key}: ${line.value}`) + .join("\n") + ); + + updateRuntimeMemoryArrows(); + updateRuntimeMemoryPinGlow(); + updateRuntimeMemoryTitleState(); + } + + function renderActiveMemoryRecords() { const records = getActiveMemoryRecordTexts(); @@ -2407,6 +2619,8 @@ getDelayedMemoryReports = options.getDelayedMemoryReports || null; getFactsMemoryFields = options.getFactsMemoryFields || null; deleteFactsMemoryField = options.deleteFactsMemoryField || null; + getLongTermMemoryFacts = options.getLongTermMemoryFacts || null; + deleteLongTermMemoryFact = options.deleteLongTermMemoryFact || null; getDisplayMode = options.getDisplayMode || null; setDisplayMode = options.setDisplayMode || null; diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 5fb60ff9..2afcee6e 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -39,6 +39,47 @@ let savedRuntimeFileFallback = null; let savedRuntimeFileFallbackLoaded = false; + function normalizeFactsMemoryStatus( + value + ) { + + const status = + String(value || "") + .trim() + .toLowerCase(); + + return ( + status === "analyzed" + || status === "analized" + ) + ? "analyzed" + : "pending"; + + } + + + function buildFactsMemoryContentHash( + value + ) { + + const text = + String(value || "") + .replace(/\s+/g, " ") + .trim(); + + let hash = 5381; + + for (let index = 0; index < text.length; index += 1) { + hash = + ((hash << 5) + hash) + ^ text.charCodeAt(index); + } + + return `h${(hash >>> 0).toString(36)}`; + + } + + function generateRuntimeSessionId() { if ( @@ -672,8 +713,37 @@ return; } + const content = + String( + field.content || field.value || "" + ).trim(); + + if (!content) { + return; + } + + const contentHash = + String( + field.l4_content_hash || "" + ).trim() + || buildFactsMemoryContentHash( + content + ); + signals[normalizedKey] = { ...field, + content, + l4_status: + normalizeFactsMemoryStatus( + field.l4_status + ), + l4_content_hash: contentHash, + l4_analyzed_at: + normalizeFactsMemoryStatus( + field.l4_status + ) === "analyzed" + ? String(field.l4_analyzed_at || "").trim() + : "", }; } ); @@ -1719,6 +1789,7 @@ clearFactsMemoryByStorageKey, readFactsMemory, writeFactsMemory, + buildFactsMemoryContentHash, activateFactsMemorySession, removeFactsMemoryField, normalizeDelayedMemoryReports, diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 053d8493..3e18ae04 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -38,6 +38,14 @@ const feedback = window.JinRuntime && window.JinRuntime.feedback; +const l4Memory = + window.JINRuntimeL4Memory + || ( + window.JinRuntime + && window.JinRuntime.l4Memory + ) + || null; + if (!feedback) { throw new Error( "JinRuntime.feedback must be loaded before runtime.js" @@ -258,6 +266,13 @@ function persistRuntimeFactsMemory( snapshot.runtime_memory_id || "" ).trim(); + const sessionId = + String( + storage.getCurrentFactsMemorySessionId + ? storage.getCurrentFactsMemorySessionId() + : "" + ).trim(); + snapshot.lines.forEach( function (line) { const key = @@ -287,20 +302,50 @@ function persistRuntimeFactsMemory( const existing = fields[key]; + const contentHash = + storage.buildFactsMemoryContentHash + ? storage.buildFactsMemoryContentHash( + content + ) + : content; if (!existing) { fields[key] = { content, runtime_snapshot_id: runtimeSnapshotId, + session_id: sessionId, + l4_status: "pending", + l4_content_hash: contentHash, + l4_analyzed_at: "", }; return; } + const previousHash = + String( + existing.l4_content_hash || "" + ).trim(); + const contentChanged = + previousHash !== contentHash + || String(existing.content || "").trim() !== content; + fields[key] = { ...existing, content, runtime_snapshot_id: runtimeSnapshotId, + session_id: sessionId, + l4_status: contentChanged + ? "pending" + : ( + existing.l4_status === "analyzed" + ? "analyzed" + : "pending" + ), + l4_content_hash: contentHash, + l4_analyzed_at: contentChanged + ? "" + : String(existing.l4_analyzed_at || "").trim(), }; } ); @@ -523,6 +568,16 @@ memoryView.init({ getDelayedMemoryReports: readDelayedMemoryReports, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, + getLongTermMemoryFacts: () => ( + l4Memory && l4Memory.getFacts + ? l4Memory.getFacts() + : [] + ), + deleteLongTermMemoryFact: (factId) => ( + l4Memory && l4Memory.requestFactDelete + ? l4Memory.requestFactDelete(factId) + : false + ), getDisplayMode: () => runtimeMemoryDisplayMode, setDisplayMode: (value) => { runtimeMemoryDisplayMode = value; @@ -964,6 +1019,16 @@ window.JinRuntime.runtime = { getDelayedMemoryReports: readDelayedMemoryReports, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, + getLongTermMemoryFacts() { + return l4Memory && l4Memory.getFacts + ? l4Memory.getFacts() + : []; + }, + deleteLongTermMemoryFact(factId) { + return l4Memory && l4Memory.requestFactDelete + ? l4Memory.requestFactDelete(factId) + : false; + }, replaceDelayedMemoryReports: writeDelayedMemoryReports, appendDelayedMemoryReports, }; diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index d9bc8708..81b23b5d 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -473,6 +473,12 @@ async function handleSocketOpen() { } syncDelayedMemoryReportsToRuntime(); + if (typeof window.syncFactsMemoryToRuntime === "function") { + window.syncFactsMemoryToRuntime(); + } + if (typeof window.syncLongTermMemoryToRuntime === "function") { + window.syncLongTermMemoryToRuntime(); + } return; } @@ -482,6 +488,12 @@ async function handleSocketOpen() { || !window.getPersistedSessionBootstrap ) { syncDelayedMemoryReportsToRuntime(); + if (typeof window.syncFactsMemoryToRuntime === "function") { + window.syncFactsMemoryToRuntime(); + } + if (typeof window.syncLongTermMemoryToRuntime === "function") { + window.syncLongTermMemoryToRuntime(); + } return; } @@ -531,6 +543,12 @@ async function handleSocketOpen() { ); syncDelayedMemoryReportsToRuntime(); + if (typeof window.syncFactsMemoryToRuntime === "function") { + window.syncFactsMemoryToRuntime(); + } + if (typeof window.syncLongTermMemoryToRuntime === "function") { + window.syncLongTermMemoryToRuntime(); + } return; } @@ -561,6 +579,12 @@ async function handleSocketOpen() { } syncDelayedMemoryReportsToRuntime(); + if (typeof window.syncFactsMemoryToRuntime === "function") { + window.syncFactsMemoryToRuntime(); + } + if (typeof window.syncLongTermMemoryToRuntime === "function") { + window.syncLongTermMemoryToRuntime(); + } } diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 6a12cf99..de6cb0d9 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -22,6 +22,36 @@ function handleSessionActionsUpdate( } +function handleFactsMemoryStoreUpdate( + data +) { + + if ( + window.JINRuntimeL4Memory + && window.JINRuntimeL4Memory.applyFactsMemoryRecordsUpdate + ) { + window.JINRuntimeL4Memory.applyFactsMemoryRecordsUpdate( + data + ); + } + +} + +function handleL4MemoryUpdate( + data +) { + + if ( + window.JINRuntimeL4Memory + && window.JINRuntimeL4Memory.applyServerUpdate + ) { + window.JINRuntimeL4Memory.applyServerUpdate( + data + ); + } + +} + function handleSocketError( data ) { @@ -207,6 +237,16 @@ registerSocketMessageHandler( handleSessionActionsUpdate ); +registerSocketMessageHandler( + "facts_memory_store_update", + handleFactsMemoryStoreUpdate +); + +registerSocketMessageHandler( + "l4_memory_update", + handleL4MemoryUpdate +); + [ "error", "fatal_error", diff --git a/ui/templates/index.html b/ui/templates/index.html index 3c8c77c6..f29b146d 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -502,6 +502,7 @@ + diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index 8643ae08..50636eae 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -53,18 +53,27 @@ def _normalize_session_action_history_item( "parts", [], ) + jin_message_content = str( + item.get( + "jin_message_content", + "", + ) + or "" + ).strip() else: text = str( item or "" ).strip() parts = [] + jin_message_content = "" return { "text": text, "parts": parts, "created_at": created_at, "runtime_turn_id": runtime_turn_id, + "jin_message_content": jin_message_content, } @@ -224,6 +233,34 @@ def _format_session_action_context_parts( ).strip() +def _format_context_action_text( + text: str, +) -> str: + + return re.sub( + r"\b([A-Z][A-Z0-9_]*)\s+-\s+", + r"\1: ", + str( + text + or "" + ).strip(), + ) + + +def _format_jin_message_content( + text: str, +) -> str: + + return re.sub( + r"\s+", + " ", + str( + text + or "" + ).strip(), + ) + + def build_session_actions_history_context( context=None, *, @@ -338,27 +375,44 @@ def build_session_actions_history_context( ) open_sequence_turn_id = "" - text = _format_session_action_context_parts( - item.get( - "parts", - [], - ), - fallback_text=item[ - "text" - ], + text = _format_context_action_text( + _format_session_action_context_parts( + item.get( + "parts", + [], + ), + fallback_text=item[ + "text" + ], + ) ) created_at = item.get( "created_at" ) + age_suffix = "" if created_at is not None: - text = ( - f"{text} ( {format_session_action_age(now - created_at)} ago )" + age_suffix = ( + f" ( {format_session_action_age(now - created_at)} ago )" ) + text = f"{text}{age_suffix}" action_index += 1 if current_sequence: + jin_message_content = _format_jin_message_content( + item.get( + "jin_message_content", + "", + ) + ) + if jin_message_content: + lines.append( + ( + f"JIN message {action_index} content: " + f"{jin_message_content}{age_suffix}" + ) + ) lines.append( - f"JIN message {action_index} executed - {text}" + f"JIN message {action_index} executed: {text}" ) else: lines.append( diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index a39d8e05..e1c928e7 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1578,6 +1578,86 @@ def format_session_action_marker_names( ) +def _normalize_jin_message_content( + value, +) -> str: + + return re.sub( + r"\s+", + " ", + str( + value + or "" + ).strip(), + ) + + +def attach_session_action_jin_message_since( + context, + start_index: int, + jin_message_content: str, +) -> bool: + + content = _normalize_jin_message_content( + jin_message_content + ) + + if ( + context is None + or not content + ): + return False + + history = getattr( + context, + "runtime_session_action_history", + None, + ) + + if not isinstance( + history, + list, + ): + return False + + safe_start_index = max( + 0, + min( + int( + start_index + or 0 + ), + len(history), + ), + ) + + for item in history[safe_start_index:]: + if not isinstance( + item, + dict, + ): + continue + + if not str( + item.get( + "text", + "", + ) + or "" + ).strip(): + continue + + if item.get( + "jin_message_content" + ) == content: + return False + + item["jin_message_content"] = content + return True + + return False + + def replace_session_action_history_since( context, start_index: int, @@ -1740,6 +1820,20 @@ def upsert_session_action_marker_history_since( ), "runtime_session_action_marker_item": True, } + previous_jin_message_content = _normalize_jin_message_content( + previous_item.get( + "jin_message_content", + "", + ) + if isinstance( + previous_item, + dict, + ) + else "" + ) + + if previous_jin_message_content: + item["jin_message_content"] = previous_jin_message_content runtime_turn_id = get_current_action_sequence_turn_id( context @@ -1862,6 +1956,18 @@ def merge_items(items): if merged_parts: merged_item["parts"] = merged_parts + for item in items: + jin_message_content = _normalize_jin_message_content( + item.get( + "jin_message_content", + "", + ) + ) + + if jin_message_content: + merged_item["jin_message_content"] = jin_message_content + break + created_at_values = [] for item in items: diff --git a/websocket/__init__.py b/websocket/__init__.py index 6e9ae1bd..f6d85c00 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -15,6 +15,14 @@ emit_runtime_l1_diff_update, emit_runtime_session_memory_update, ) +from runtime.L4_memory import ( + apply_facts_memory_store_sync, + apply_l4_memory_store_sync, + delete_l4_memory_fact, + emit_facts_memory_store_update, + emit_l4_memory_update, + schedule_l4_memory_idle_update, +) from runtime.fact_check import run_fact_check_once from utils.ws_errors import handle_websocket_error from .attachments import ( @@ -134,7 +142,6 @@ async def process_pending_requests(): await wait_for_runtime_memory_update( context ) - if not is_idle_followup: await apply_runtime_response_feedback( context, @@ -289,6 +296,89 @@ async def process_pending_requests(): ) continue + if message_type == "facts_memory_store_sync": + stats = apply_facts_memory_store_sync( + context, + message_data.get( + "records", + [], + ), + ) + await logger.log_system( + ( + "[WS] facts memory store synced " + f"({stats['records_count']} records, " + f"{stats['pending_count']} pending)" + ) + ) + await emit_facts_memory_store_update( + context + ) + continue + + if message_type == "l4_memory_store_sync": + applied = apply_l4_memory_store_sync( + context, + message_data.get( + "store", + {}, + ), + ) + await logger.log_system( + ( + "[WS] L4 memory store synced" + if applied + else "[WS] L4 memory store sync ignored" + ) + ) + await emit_l4_memory_update( + context, + change={ + "synced": bool(applied), + }, + ) + continue + + if message_type == "l4_memory_idle_tick": + if "records" in message_data: + apply_facts_memory_store_sync( + context, + message_data.get( + "records", + [], + ), + ) + + if "store" in message_data: + apply_l4_memory_store_sync( + context, + message_data.get( + "store", + {}, + ), + ) + + schedule_l4_memory_idle_update( + context=context, + user_idle_seconds=message_data.get( + "user_idle_seconds", + ), + ) + continue + + if message_type == "l4_memory_delete_fact": + await delete_l4_memory_fact( + context, + str( + message_data.get( + "fact_id", + "", + ) + or "" + ), + ) + continue + if message_type == "session_bootstrap": await logger.log( diff --git a/websocket/messages.py b/websocket/messages.py index 44e07f17..14286556 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -321,6 +321,7 @@ async def refresh_pending_brain_usage( build_brain_context( context, runtime_actions=runtime_actions, + user_input=user_text, ) ) From 791103c682a803feaa1207a4f8d1ea3d3f9951fe Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:55:00 +0300 Subject: [PATCH 005/168] feature: L4 memory layer - add update_delayed_memory action for extending reports - archive absorbed L4 facts from active brain context - add pinned delayed-memory reports - refresh README, theme assets, and regression coverage --- README.md | 651 ++++--------------- contracts/jin_color.json | 10 +- contracts/rules_assembler.py | 6 + contracts/save_delayed_memory.json | 13 +- contracts/update_delayed_memory.json | 28 + rules/brain_context_builder.py | 16 +- rules/runtime.py | 3 + runtime/L4_memory.py | 40 +- runtime/L4_memory_utils.py | 31 +- runtime/runtime_context.py | 8 + tests/runtime_actions/test_delayed_memory.py | 337 ++++++++++ tests/test_brain_runtime_actions.py | 1 + tests/test_delayed_memory_client_contract.py | 4 + tests/test_delayed_memory_file_store.py | 42 +- tests/test_l4_memory.py | 148 ++++- ui/static/css/base.css | 124 +++- ui/static/css/runtime-avatar.css | 42 +- ui/static/css/runtime-memory.css | 65 ++ ui/static/css/theme-win95.css | 1 + ui/static/images/jin-core-default-theme.jpg | Bin 166250 -> 176160 bytes ui/static/images/runtime-highlight.png | Bin 67787 -> 65138 bytes ui/static/images/think-highlight.jpg | Bin 346642 -> 237671 bytes ui/static/js/chat-attachments.js | 8 +- ui/static/js/logger/log-entries.js | 50 +- ui/static/js/logger/logger.js | 415 +++++++++++- ui/static/js/logger/session-actions.js | 9 +- ui/static/js/logger/trace-modal.js | 9 +- ui/static/js/panel-inactivity.js | 285 +++++++- ui/static/js/runtime/runtime-l4-memory.js | 17 +- ui/static/js/runtime/runtime-memory-view.js | 146 ++++- ui/static/js/runtime/runtime-storage.js | 101 +++ ui/static/js/runtime/runtime.js | 46 ++ ui/static/js/socket/delayed-memory.js | 6 +- ui/static/js/socket/runtime-actions.js | 18 + ui/static/js/win95-theme.js | 9 + ui/templates/index.html | 24 +- utils/actions/__init__.py | 10 +- utils/actions/common_action_utils.py | 4 + utils/actions/delayed_memory_actions.py | 51 ++ utils/actions/dispatcher.py | 8 + utils/actions/regexp_utils.py | 24 +- utils/actions/save_delayed_memory_utils.py | 88 ++- utils/actions/update_delayed_memory_utils.py | 101 +++ utils/brain_client_utils.py | 300 ++++++++- utils/context/delayed_memory.py | 15 + utils/context/session_actions.py | 1 + utils/delayed_memory_file_store.py | 37 +- utils/session_actions_history.py | 5 + websocket/bootstrap.py | 16 + 49 files changed, 2736 insertions(+), 637 deletions(-) create mode 100644 contracts/update_delayed_memory.json create mode 100644 utils/actions/update_delayed_memory_utils.py diff --git a/README.md b/README.md index 2aca88fd..694eabe5 100644 --- a/README.md +++ b/README.md @@ -29,202 +29,121 @@ JIN uses short-term continuity to dynamically guide conversation strategy: Main runtime view: chat, live avatar, telemetry, and inspectable memory panels in one browser workspace. -### Reasoning Citations - -![Think citation highlighting](ui/static/images/think-highlight.jpg) - -Think citation highlighting shows where reasoning quotes rules, runtime memory, or restored session context. - ### Memory Timeline ![Runtime memory snapshot timeline](ui/static/images/runtime-highlight.png) Runtime memory snapshots can be stepped through visually, with new or changed facts highlighted in the sidebar. -## Capabilities - -### Core Features - -- Visible runtime memory: JIN keeps a compact sense of what this session is about, what changed, and what still feels unresolved. -- Inspectable memory timeline: step through snapshots and see which facts or patterns were added instead of guessing what the assistant remembered. -- Think citation highlighting: rule fragments, runtime memory, and restored session context are softly highlighted after a thinking block completes, then reappear on hover. -- Session save and restore: natural closing phrases trigger a compact L3 memory digest, stored locally and replayed on reconnect. -- Active-memory contracts: reminders, ask-later conditions, and recall games live outside normal L1 summarization until JIN resolves them. -- Delayed memory reports: explicit requests to save a summary, digest, recap, or session summary for later become structured reports stored in browser `localStorage`, shown in the delayed-memory view, and kept separate from pending reminders or L1 facts. -- Facts memory: eligible L1 fields are mirrored into a per-session browser store, can be inspected or removed from the logger, and can be reassigned to an empty current session without duplicating the source bucket. -- Contract-driven runtime actions: markers, payload rules, blockers, confirmation guards, follow-up behavior, and display names are defined per action under `contracts/`. -- Guarded action lifecycle: pending actions are deduplicated, tracked through completion, failure, interruption, or abort, and finalized consistently when generation stops or the WebSocket disconnects. -- Pattern and loop detection: repeated exchanges can change strategy instead of producing the same polite answer again. -- Context pressure telemetry: model status, token usage, context pressure, runtime memory, and live logs stay visible in the right sidebar. -- Local OpenAI-compatible routing: use separate brain, service, and translator runtimes, or collapse to one service model for a simpler setup. - -### Workspace Features - -- Streaming chat: answers appear as they are written, with thinking visually separated from the final reply. -- Stop generation control: the input turns into a stop control while JIN is working, so a drifting answer can be interrupted immediately. -- Built-in web-search action: the model can ask the runtime to search the web, then answer from returned evidence without rendering raw tool syntax. -- Asset workflows: reusable skills, wildcard lists, prompt templates, prompt batches, and generated outputs live under `assets/`, with runtime actions for listing, previewing, sampling, expanding templates, generating prompt batches, and checking duplicates. -- Live JIN color action: ordered `JIN_COLOR` markers update the avatar center, scene tint, and action bubble without forcing a follow-up turn. -- File attachments: drag, drop, paste, or pick images and text files; image chips support hover previews and modal previews, while text chips open their full content in the standard modal. -- Multilingual input path: Cyrillic input can be translated internally when translation is enabled, while the visible conversation remains natural. -- Keyboard-first writing flow: Enter sends, Ctrl/Shift+Enter inserts a newline. -- Deploy-friendly configuration: use a local `config.py` while experimenting, then switch to environment variables when running elsewhere. - -## Architecture - -![schema](ui/static/images/schema.jpg) - -## Runtime Flow - -The WebSocket layer creates a `RuntimeContext` per connection. Each user message is handled by `AgentRuntime`: - -- When translation is enabled, Cyrillic input can route through `planner -> translator -> brain -> validator`. -- The default input path is `planner -> brain -> validator`. - -The translator node logs translator output for observability but does not render it as a chat message. The brain node streams the visible assistant response from the configured brain runtime. - -The brain can emit runtime action markers. Per-action contracts under `contracts/` define the marker shape, trigger words, blockers, follow-up behavior, and display metadata. The runtime consumes valid markers as control events, executes them, injects trusted results into the next brain prompt when needed, and prevents raw control syntax from being rendered as chat text. - -Current action families include web search, session save, active and delayed memory, skill and asset workflows, idle follow-up ticks, JIN color changes, and tool-result cleanup. Actions can share one turn while preserving marker order and distinct payload identity. - -Actions that require explicit user intent can pause on a browser confirmation guard. Their UI lifecycle is tracked as pending, completed, failed, interrupted, or aborted; cancellation, stream interruption, timeout, and disconnect paths clear the same pending state instead of leaving a stuck action bubble. - -Active-memory records are stored separately from normal L1 memory, synced through the browser, injected as a high-priority `` block, and resolved by ID when their condition is met. - -After the visible response ends, the service runtime updates `context.runtime_memory` in the background. This request does not block the user-facing answer. The next brain prompt receives the current memory as trusted runtime context, and the right sidebar shows the same memory as plain text. - -Accepted L1 snapshots also update a session-scoped Facts Memory index in browser storage. This keeps durable fields inspectable without turning the live L1 snapshot into a permanent cross-session profile. +### Reasoning Citations -The memory layer can also surface compact pattern signals. When the session starts repeating the same kind of interaction, JIN can receive strategy hints such as low-signal repetition or stalled context and respond differently instead of treating each message as a fresh start. +![Think citation highlighting](ui/static/images/think-highlight.jpg) -Each memory update is also stored as a per-session snapshot. The UI can step backward and forward through those snapshots, replaying lightweight diff highlights so the user can see which memory keys or values were added or changed during the conversation. +Think citation highlighting shows where reasoning quotes rules, runtime memory, or restored session context. -Completed thinking blocks are also scanned for direct citations from trusted prompt context. Rule matches, indexed runtime-memory matches, and restored session-memory matches are highlighted with separate colors so the user can see which injected source shaped the reasoning without interrupting streaming. +## Capabilities -If generation is aborted, the runtime captures the partial answer and schedules an interrupted memory update. The memory summarizer is instructed to mark the turn as incomplete and not treat it as resolved. +### Conversation and Workspace -When the user signals the end of a session — explicitly or through natural closing phrases — the brain emits a `SAVE_SESSION` action. The runtime builds a compact L3 digest from the current snapshot history and sends it to the browser for local storage. On the next connection, the browser sends the digest back as part of the bootstrap payload and the runtime injects it as trusted session context before the first turn. +- Streams thinking and the final answer as separate blocks. +- Shows model status, token usage, context pressure, runtime memory, and logs in the workspace. +- Accepts text files and images by picker, drag and drop, or paste. +- Stops an active generation from the input area without losing the whole session. +- Highlights direct references to runtime rules, live memory, and restored session context inside completed thinking blocks. -## Runtime Memory +### Memory and Continuity -Runtime memory is intentionally lightweight, but it is no longer passive storage only. It gives JIN short-term continuity and can now influence conversational behavior when repeated patterns appear. +- Keeps the current topic, task, decisions, feedback, and unresolved points in visible runtime memory. +- Stores every accepted memory update as a snapshot that can be inspected in the sidebar. +- Detects repeated inputs and interaction loops during the current session. +- Saves a compact session digest and restores it after a reload or reconnect. +- Keeps pending reminders and ask-later conditions separate from normal conversation memory. +- Saves larger reports separately and appends them to the active context only when needed. +- Builds a cross-session L4 store from durable facts, preferences, project decisions, and persistent constraints. -- It lives in the active `RuntimeContext`, not in a database. -- It is updated by separate service-model requests after a turn finishes. -- It is split into factual L1 memory, higher-level L2 pattern memory, a long-horizon L3 session digest, and separate active-memory, delayed-memory, and facts-memory channels. -- L1 is written as compact, actionable bullet-like state rather than full transcript history. -- L2 tracks possible repeated interaction patterns and occurrence signals during the active session. -- L3 is a compressed session summary generated at explicit save points and stored in the browser. It survives page reloads and reconnects. -- Memory is injected into the brain prompt as trusted runtime context. -- It is mirrored in the right sidebar through `runtime_memory_update`, `runtime_session_memory_update`, and `active_memory_records_update` WebSocket events. -- Each L1/L2 update is captured as a session snapshot with an index, raw memory text, parsed key/value lines, and diff metadata. -- Runtime-owned `active_memory_records` track pending contracts with IDs, status, creation time, elapsed time, and elapsed JIN-message counters; they are displayed and persisted, but stripped before L1 summarization. -- Delayed reports remain separate from L1 and are listed, appended, or removed from the current context by report ID. -- Facts Memory mirrors eligible accepted L1 fields into `jin.factsMemory..v1`; transient user-message, idle, JIN-response, and active-memory lines are excluded. -- The UI can navigate previous snapshots, replay visual highlights for new or changed memory fields, and show full memory suffixes line-by-line on hover. -- Conversation activity and no-signal alerts can suppress overly soft default behavior when the exchange is clearly stuck. -- Truncated or obviously incomplete summarizer output is rejected so it does not overwrite the previous memory. +### Runtime Actions -This gives JIN observable short-term memory and behavior adaptation without introducing a server-side database, vector storage, or retrieval infrastructure yet. +JIN can request runtime actions while answering. The action is handled by the runtime, and any result needed for the answer is returned to the model in the same workflow. +Available actions include: -## Memory Snapshot Examples +- web search for current information; +- session save and restore; +- active-memory creation and resolution; +- delayed-memory save, update, append, and removal from the current context; +- runtime TODO lists for multi-step work; +- skill discovery and temporary skill attachment; +- asset operations for files, prompts, wildcards, templates, and local Python skills; +- live avatar and workspace color changes. -JIN memory is stored as plain `key: value` lines so it can be shown in the UI, injected into prompts, diffed between turns, and compressed into a later session digest. The keys are semantic handles rather than a fixed database schema, but the current runtime expects stable line shapes for important facts, active contracts, and pattern evidence. +## Architecture -### L1 memory snapshot (facts) +![schema](ui/static/images/schema.jpg) -L1 is the live factual layer. It keeps the current state needed for the next answer: user request, active topic, latest user message, current task, response feedback, durable facts, and unresolved normal conversation state. It is not a transcript and it should not infer long-term personality traits. +## How JIN Works -```text -user_message: "thanks" -last_jin_response: Acknowledged the user and kept the current runtime state compact. -active_topic: Runtime memory testing. -current_task: Verify that JIN keeps continuity without rewriting the full transcript. -open_question: User may continue testing active-memory behavior next. -``` +A normal turn follows this path: -A rendered runtime snapshot also carries metadata used by the right-side timeline panel: - -```json -{ - "session_id": "runtime-session-id", - "index": 5, - "raw_memory": "active_topic: Runtime memory testing.\ncurrent_task: Verify continuity.", - "lines": [ - { - "key": "active_topic", - "value": "Runtime memory testing.", - "key_status": "same", - "value_status": "changed", - "key_change_ratio": 0.0, - "value_change_ratio": 0.42 - } - ], - "patch": { - "active_topic": { - "status": "changed", - "value": "Runtime memory testing." - } - }, - "total_diff": 87.3 -} -``` +1. The user sends a message and optional attachments. +2. The brain model produces thinking and the visible answer. +3. Runtime actions are executed when the model needs search, memory, skills, assets, or another internal step. +4. The service model updates memory after the visible answer finishes. +5. The next turn receives the current memory, selected reports, active contracts, attached skills, and long-term facts as context. -Memory lines may also have temporary trace strength such as `[ trace: 0.50 ]` or inject `user_idle: 9s` into the displayed context. Those are runtime metadata signals, not durable memory facts. +The default path is `planner -> brain -> validator`. When translation is enabled, the translator is inserted before the brain. Brain, service, and translator roles may use separate OpenAI-compatible endpoints, or one model can handle all roles. -### Active memory snapshot (runtime contracts) +## Memory Model -Active memory is now owned by runtime, not by the L1 summarizer. It is stored as `active_memory_records`, persisted in browser `localStorage` under `jin.activeMemory.v1`, refreshed with runtime timing metadata, and injected into the brain prompt as a separate high-priority block. +JIN separates memory by purpose instead of keeping one growing transcript. -```text -active_memory_1: Secret word: Sun; ask the user to guess it later without revealing it [ active_memory_id: a1b2c3 ] [ conditions: Secret word: Sun; ask the user to guess it later without revealing it ] [ status: pending ] [ creation_time: 2026-06-20T10:00:00 ] [ created_jin_message_number: 3 ] [ elapsed_time: 00:02:39 ] [ elapsed_jin_message_number: 2 ] -``` +| Layer | Purpose | Lifetime | +| --- | --- | --- | +| **L1 — Live Facts** | Current topic, request, task state, decisions, feedback, and unresolved points needed for the next turn. | Current runtime session | +| **L2 — Patterns** | Repeated inputs, stalled loops, and other short-term interaction patterns that may require a strategy change. | Current runtime session | +| **L3 — Session Digest** | A compact handoff containing durable session state, completed work, open tasks, and the next step. | Saved across reloads and reconnects | +| **L4 — Long-Term Facts** | Stable facts, preferences, project decisions, constraints, and environment details that should remain available across sessions. | Cross-session | -```xml - - active_memory_1: Secret word: Sun; ask the user to guess it later without revealing it [ active_memory_id: a1b2c3 ] [ conditions: Secret word: Sun; ask the user to guess it later without revealing it ] [ status: pending ] - -``` +The current runtime also uses several separate memory channels: -JIN creates these records with `SAVE_ACTIVE_MEMORY` and removes them with `RESOLVE_ACTIVE_MEMORY` using the actual `active_memory_id`. L1 receives normal runtime memory with active-memory lines stripped out, so pending reminders and recall contracts are not accidentally rewritten by summarization. +| Channel | Use | +| --- | --- | +| **Active Memory** | Pending reminders, recall conditions, and rules that stay active until fulfilled or cancelled. | +| **Delayed Memory** | Full reports and summaries stored outside the live prompt. A report is appended only when it is relevant. | +| **Facts Memory** | Inspectable per-session fields selected from L1. These fields are the input for L4 consolidation. | +| **Runtime TODO** | A visible step ledger for multi-step work. Items are checked and resolved as the task progresses. | -### L2 memory snapshot (patterns) +### Current L4 Flow -L2 works above L1. It watches recent L1 patch windows for repeated interaction patterns, loops, and same-intent behavior. It should describe hypotheses with occurrence counters and scope, not turn them into permanent user traits. +JIN waits until the workspace is idle, reads new Facts Memory fields, extracts durable candidates, and merges them into the long-term store. Duplicate or overlapping facts are consolidated. Long-term facts are then available to future brain calls and can be removed from the memory panel. -```text -possible pattern: Repeated identical user message during loop testing. Occurrences: 4; first_seen_snapshot: 2; last_seen_snapshot: 5; evidence summary: User sent the same short message several times in the same probe window; confidence: high. -L2_pattern_evidence_1: user repeatedly sending one message [ quote: "ping" ] [ first_seen_turn_snapshot: 2 ] [ last_seen_turn_snapshot: 5 ] [ occurrences: 4 ] -likely_intent: User may be stress-testing whether JIN detects low-signal repetition before changing response strategy. -scope: Current session/test sequence, not a stable user preference. -``` +Delayed Memory can also absorb selected L4 facts into a larger report. Facts already archived by a report are not injected twice. -`L2_pattern_evidence_N` is a runtime accounting line. The quote must come from an actual L1 `user_message` value, the occurrence count is based on matching snapshot evidence, and L1 must not rewrite the line. If the latest turn resolves or cancels an L2 evidence item, L1 writes a separate status companion instead: +## Basic Use -```text -L2_pattern_evidence_1_status: status: resolved; reason: identified as a test -``` +| What you do | What JIN does | +| --- | --- | +| Continue a normal conversation | Updates L1 and, when relevant, L2 after each completed turn. | +| Say `save the session` or clearly end the session | Creates an L3 digest and restores it on the next connection. | +| Ask JIN to remember, remind, or check something later | Creates an Active Memory record and keeps it outside L1 until resolved. | +| Ask to save a report or summary | Creates a Delayed Memory report with a title, summary, tags, and body. | +| Ask to use a saved report | Appends that report to the current context. | +| Give JIN a multi-step task | Creates a runtime TODO list and works through its items. | +| Ask for current information | Runs web search and answers from the returned evidence. | +| Attach a file or image | Adds it to the current request; supported skills can process larger files in steps. | -### L3 memory snapshot (session) +Memory is visible in the right panel. Use the panel arrows to move through runtime snapshots and switch between live memory, Facts Memory, Long-Term Memory, and Active Memory views. -L3 is the session handoff layer. It is generated at save/restore points independently from selected L1 runtime snapshots and recent diff history. It keeps what should survive a reload or a new tab: project direction, durable facts, decisions, unresolved tasks, constraints, and next step. +## Assets and Skills -```text -session_status: Runtime stabilization pass completed after the first public JIN Core release cycle. -project_focus: Clean runtime memory architecture and behavior-probe reliability. -durable_fact: JIN uses L1 factual memory, L2 pattern memory, and L3 session digest memory with visible snapshots and diff metadata. -decision: Keep public commit titles calm and place implementation details inside commit bodies and release notes. -completed_work: Extracted L3 session memory into a dedicated layer; split memory rules into L1/L2/L3 boundaries; cleaned compatibility exports. -behavior_probe_result: ASCII drawing fallback, movie recommendation closure, and delayed recall-word contract stayed green after the refactor. -next_step: Publish v0.6-runtime-stabilization and continue L1/L2 cleanup. -``` +Reusable material lives under `assets/`: -L3 also extracts important session events directly from runtime snapshots and links them back to their source snapshots: +- `assets/skills/` — instructions and optional local Python tools; +- `assets/prompts/` — reusable prompt lists; +- `assets/templates/` — prompt templates; +- `assets/wildcards/` — text values used by templates and generators; +- `assets/outputs/` — generated files. -```text -search_flow_recovery: JIN found and fixed a repeated follow-up loop, then completed the original search flow normally. [ runtime_memory_ids: a1b2c3, d4e5f6 ] -``` +JIN can list available skills, attach the skill needed for the current task, run its allowed actions, and remove it afterward. Python skills are restricted to `.py` files inside the selected skill directory and run without a shell. ## Project Layout @@ -244,87 +163,44 @@ search_flow_recovery: JIN found and fixed a repeated follow-up loop, then comple |-- .github/workflows/ # GitHub Actions CI |-- agent/ # Agent runtime, state, router, and nodes |-- clients/ # Runtime client builders and provider helpers -|-- runtime/ # Runtime client, context, contracts, memory, stream, registry -|-- rules/ # Brain prompt rule blocks: identity, loop, runtime actions +|-- memory/ # Local delayed-memory reports and memory placeholders +|-- runtime/ # Runtime context, memory layers, stream, telemetry, registry +|-- rules/ # Brain prompt rule blocks |-- ui/ # HTML templates, browser JavaScript, and README assets |-- tests/ # Unit, runtime-action, and optional model integration tests -`-- utils/ # Context builders, action handlers, assets, stream, and telemetry helpers +`-- utils/ # Actions, assets, validation, storage, and shared helpers ``` ## Requirements - Python 3.10+ -- Node.js 20+ for npm test/probe shortcuts - One or more OpenAI-compatible model servers -- Provider endpoints that support: - - `POST /v1/chat/completions` - - `GET /v1/models` -- Optional LM Studio metadata endpoint: - - `GET /api/v0/models` - -## Current Model Baseline - -JIN Core is model-agnostic at the API layer, but the current development and behavior testing baseline is: +- Node.js 20+ only for npm test and behavior-probe shortcuts +- A Serper API key only when the built-in web-search action is enabled -```text -google/gemma-4-e4b -LM Studio -Enable Thinking: on -OpenAI-compatible API -``` - -This matters because JIN depends on more than plain chat completion. The runtime expects the brain model to follow layered prompt context, keep JIN identity separate from the underlying model, emit internal runtime-action markers reliably, and expose reasoning in a separable form when thinking traces are enabled. +The model server must provide `/v1/chat/completions` and `/v1/models`. LM Studio can also provide `/api/v0/models` so JIN can read the loaded context length. -Smaller or non-thinking models may still run, but they can behave differently: ignore current runtime variables, leak reasoning into the visible answer, miss active-memory actions, repeat generic replies, or confuse recent-turn context with the latest user request. During active development, reported behavior should be compared against the Gemma 4 E4B + enabled reasoning baseline before treating it as a JIN runtime bug. +## Quick Start -## Windows One-Click Launcher +### Windows + LM Studio -Windows users can start JIN with LM Studio through: +1. Install LM Studio and load an OpenAI-compatible chat model. +2. Start the LM Studio Local Server. +3. Run: ```text launch_jin.bat ``` -The launcher uses LM Studio as the default provider. When `config.py` already exists, it checks configured provider base URLs first, in this order: `SERVICE_API_BASE`, `BRAIN_API_BASE`, then `TRANSLATOR_API_BASE`. If no configured provider responds, it falls back to the default OpenAI-compatible API at: - -```text -http://localhost:1234/v1/models -``` - -Before running it: - -- Install and open LM Studio. -- Recommended current development baseline: `google/gemma-4-e4b` with Enable Thinking turned on in LM Studio. -- Start the LM Studio Local Server. - -The launcher does not download models automatically. LM Studio downloads are intentionally left to the LM Studio UI. - -When the Local Server is reachable, the launcher reads and prints the returned model IDs, then checks local `config.py`. - -For `BRAIN_MODEL_UID`, `SERVICE_MODEL_UID`, and `TRANSLATOR_MODEL_UID`, the launcher only writes a Gemma model automatically when the current value is empty or still uses the template defaults: `brain-model`, `service-model`, or `translator-model`. If a user-defined model ID is already present, the launcher keeps it unchanged. - -For provider base URLs, the launcher points empty/template values at the working LM Studio base URL it found, but keeps user-defined values unchanged. - -If LM Studio is not running, it prints: - -```text -LM Studio is not running. -Open LM Studio, start Local Server, then run this script again. -``` - -If no supported Gemma model is returned, it prints the recommended model ID and asks you to download it in LM Studio, then rerun the launcher. - -After the readiness check passes, the launcher creates `.venv` if needed, installs `requirements.txt`, starts the backend, and opens: +The launcher checks the local model server, creates `config.py` from the template when needed, prepares `.venv`, installs dependencies, starts JIN, and opens: ```text http://127.0.0.1:8000 ``` -If the launcher is already running, a second click exits immediately instead of repeating the LM Studio, config, dependency, and backend checks. - -## Quick Start +It does not download models automatically and does not replace model IDs or provider URLs that you already configured. -Create and activate a virtual environment: +### Manual Start ```bash python -m venv .venv @@ -334,170 +210,77 @@ Windows PowerShell: ```powershell .\.venv\Scripts\Activate.ps1 +Copy-Item config.example.py config.py ``` Linux/macOS: ```bash source .venv/bin/activate -``` - -Install dependencies: - -```bash -pip install -r requirements.txt -``` - -Create a local config: - -```bash cp config.example.py config.py ``` -Windows PowerShell: - -```powershell -Copy-Item config.example.py config.py -``` - -Run the server: +Install and run: ```bash +pip install -r requirements.txt python app.py ``` -Open: +Then open `http://127.0.0.1:8000`. -```text -http://127.0.0.1:8000 -``` +## Model Setup -## Configuration +JIN is model-agnostic at the API layer. A simple setup uses one model for both brain and service roles. A split setup can use: -`config.py` defines model providers, model IDs, request limits, context windows, and generation parameters. -It is intentionally ignored by Git because it contains local runtime addresses. When `config.py` is absent, the app falls back to `config.example.py`, which keeps CI and basic tests runnable without private local settings. +- a stronger thinking model for the visible answer and runtime decisions; +- a smaller service model for memory updates and supporting work; +- an optional translator model for internal translation. -For deployment, every uppercase option can also be provided through environment variables. Environment values override `config.py` and `config.example.py`. Both plain names and `JIN_`-prefixed names are supported: +A thinking-capable model is recommended for the brain role. Reliable reasoning separation and runtime-action use depend on the selected model. -```bash -BRAIN_API_BASE=http://brain-host:1234 -JIN_SERVICE_MODEL_UID=service-model -USE_SERVICE_AS_BRAIN=true -SEARCH_TIMEOUT=20.0 -``` - -Plain names take priority over prefixed names when both are set. Boolean env values accept `1`, `true`, `yes`, `on`, `0`, `false`, `no`, and `off`. - -```python -USE_SERVICE_AS_BRAIN = True -TRANSLATION_ENABLED = False -TRANSLATE_RESPONSE = False -FORMAT_RESPONSE = True -DEBUG_RULE_CITATIONS = True -FOLLOW_UP_ON_LIMIT = True - -CHAT_ENDPOINT = "/v1/chat/completions" -MODELS_ENDPOINT = "/v1/models" -NATIVE_MODELS_ENDPOINT = "/api/v0/models" -WEBSOCKET_MAX_MESSAGE_BYTES = 64 * 1024 * 1024 - -RUNTIME_OUTPUT_TOKEN_RESERVE = 256 -RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER = True -RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER = False - -DOCUMENT_READER_MAX_ITERATIONS = 128 -DOCUMENT_READER_MIN_CHUNK_TOKENS = 256 -DOCUMENT_READER_MAX_CHUNK_TOKENS = 0 -DOCUMENT_READER_RESULT_MAX_TOKENS = 0 -PYTHON_SKILL_TIMEOUT_SECONDS = 120 -PYTHON_SKILL_OUTPUT_MAX_CHARS = 60000 - -BRAIN_API_BASE = "http://brain-host:1234" -BRAIN_MODEL_UID = "brain-model" -BRAIN_REQUEST_TIMEOUT = 1000.0 -BRAIN_CONTEXT_WINDOW = 8192 -NIGHT_BRAIN_CONTEXT_WINDOW = 16384 -BRAIN_TEMPERATURE = 0.7 -BRAIN_MAX_TOKENS = 8192 -BRAIN_MAX_FOLLOWUPS = 50 -BRAIN_IMAGE_INPUT_ENABLED = False - -SERVICE_API_BASE = "http://service-host:1234" -SERVICE_MODEL_UID = "service-model" -SERVICE_REQUEST_TIMEOUT = 1000.0 -SERVICE_CONTEXT_WINDOW = 4096 -SERVICE_TEMPERATURE = 0.1 -SERVICE_MAX_TOKENS = 4096 -SERVICE_IMAGE_INPUT_ENABLED = False - -SEARCH_PROVIDER = "serper" -SEARCH_SERPER_API_KEY = "mock-serper-api-key" -SEARCH_MAX_RESULTS = 5 -SEARCH_TIMEOUT = 100.0 - -TRANSLATOR_API_BASE = "http://translator-host:1234" -TRANSLATOR_MODEL_UID = "translator-model" -TRANSLATOR_REQUEST_TIMEOUT = 120 -TRANSLATOR_CONTEXT_WINDOW = 2048 -TRANSLATION_RETRIES = 1 -TRANSLATION_TEMPERATURE = 0.1 -TRANSLATION_MIN_TOKENS = 1024 -TRANSLATION_MAX_TOKENS = 2048 -``` - -### Key Options - -- `USE_SERVICE_AS_BRAIN`: Uses the service runtime for brain responses when enabled. -- `TRANSLATION_ENABLED`: Enables the internal translation node before the brain call. -- `TRANSLATE_RESPONSE`: Enables response translation path when configured. -- `FORMAT_RESPONSE`: Enables client-side formatting of completed visible responses. -- `DEBUG_RULE_CITATIONS`: Enables think citation scanning/highlighting support. -- `FOLLOW_UP_ON_LIMIT`: Continues an output-limit interruption through an internal follow-up instead of ending the workflow immediately. -- `WEBSOCKET_MAX_MESSAGE_BYTES`: Maximum inbound WebSocket payload size, including base64 attachment overhead. -- `NATIVE_MODELS_ENDPOINT`: Optional provider-native metadata endpoint. LM Studio exposes the currently loaded context length here, which is more accurate than some `/v1/models` responses. Leave empty to disable native probing. -- `BRAIN_API_BASE`, `SERVICE_API_BASE`, `TRANSLATOR_API_BASE`: Provider base URLs. -- `BRAIN_MODEL_UID`, `SERVICE_MODEL_UID`, `TRANSLATOR_MODEL_UID`: Model IDs for each runtime role. -- `*_REQUEST_TIMEOUT`: Request timeout for each runtime role. -- `*_CONTEXT_WINDOW`: Context capacity displayed in telemetry and used as fallback when server metadata is unavailable. -- `*_MAX_TOKENS`: Maximum generated tokens for each runtime role. -- `RUNTIME_OUTPUT_TOKEN_RESERVE`: Reserved context headroom kept free when calculating the dynamic response budget. Defaults to `256` in `config.example.py`. -- `RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER`: When `true`, JIN prefers the loaded context length reported by the runtime server over local config values. Defaults to `true`. -- `RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER`: When `true`, JIN prefers the server-reported output token limit for model calls. Defaults to `false` in `config.example.py`. -- `DOCUMENT_READER_*`: Limits and adaptive token budgets for chunked document-reading skills; zero token ceilings enable automatic scaling from the active model limits. -- `PYTHON_SKILL_*`: Timeout and captured-output limits for local `.py` skills executed without a shell. -- `BRAIN_MAX_FOLLOWUPS`: Maximum internal action/follow-up iterations allowed for one user turn. -- `BRAIN_IMAGE_INPUT_ENABLED`, `SERVICE_IMAGE_INPUT_ENABLED`: Opt in to OpenAI-compatible `image_url` message parts only when the selected runtime supports them. -- `SEARCH_PROVIDER`, `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS`, `SEARCH_TIMEOUT`: Search backend settings used by runtime search and fact-check actions. -- `TRANSLATION_RETRIES`, `TRANSLATION_TEMPERATURE`, `TRANSLATION_MIN_TOKENS`, `TRANSLATION_MAX_TOKENS`: Translation node generation settings. +## Configuration -## Session Memory Persistence +Copy `config.example.py` to `config.py` and set the provider URLs and model IDs. `config.py` is ignored by Git. -L3 session memory lets context survive across browser sessions without a server-side database. +Main options: -To save a session, say something that clearly signals you are done: "save the session", "that's all for today", "wrap it up", "I'm going to sleep", or the Russian equivalents. The brain emits a `SAVE_SESSION` action and the runtime builds a compressed digest from the current snapshot history. The browser stores this digest in `localStorage`. +| Option | Purpose | +| --- | --- | +| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | +| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Brain provider and model. | +| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Service provider and model. | +| `TRANSLATION_ENABLED` | Enable the translator path. | +| `*_CONTEXT_WINDOW`, `*_MAX_TOKENS` | Local fallbacks for context and output limits. | +| `BRAIN_IMAGE_INPUT_ENABLED`, `SERVICE_IMAGE_INPUT_ENABLED` | Send image attachments to roles that support OpenAI-compatible image input. | +| `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable long-term memory consolidation and set the idle delay. | +| `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | +| `BRAIN_MAX_FOLLOWUPS` | Limit internal action and follow-up steps for one user turn. | -On the next page load or reconnect, the browser includes the saved digest in its bootstrap payload. The runtime receives it, validates it against any fresh L1 memory that may have accumulated, and injects the session context into the brain prompt before the first turn. Active-memory records are bootstrapped separately from `jin.activeMemory.v1`. +Every uppercase option can also be set through environment variables. Plain names and `JIN_`-prefixed names are supported; plain names take priority. -The sidebar shows a distinct indicator when a session was restored from a saved digest rather than built from live L1 memory. +## Local Storage -`saved_runtime.example.txt` shows the optional static fallback format. Copy it to `saved_runtime.txt` and edit the contents if you want the browser to read a pre-populated runtime/session memory seed from `/saved_runtime.txt`. The browser does not write this file automatically. +- JIN keeps its persistent runtime state locally in browser `localStorage` and written as JSON files under `memory/`; +- `saved_runtime.example.txt` can be copied to `saved_runtime.txt` to provide a static session-memory seed. +No server-side database is required. ## Tests -Fast local tests run through npm: +Run the fast local suite: ```bash npm test ``` -The translation model smoke test is intentionally separate because it calls the configured local translator runtime: +Run the translator smoke test against the configured local model: ```bash npm run translation_tests ``` -Optional model behavior probes stay local by default: +Run an optional behavior probe: ```bash npm run probe ascii @@ -508,188 +291,4 @@ npm run probe save npm run probe delayed ``` -GitHub Actions runs only the fast test suite. Model-dependent tests should stay local unless the workflow is given access to a real compatible runtime. - -## WebSocket Protocol - -Client message: - -```json -{ - "text": "Hello" -} -``` - -Client message with runtime context fields: - -```json -{ - "text": "Hello", - "runtime_pattern_counter": 0, - "runtime_repeated_input_count": 0, - "user_idle": "9s", - "user_idle_seconds": 9, - "user_idle_paused": false, - "active_memory_records": [] -} -``` - -Abort active generation: - -```json -{ - "type": "abort" -} -``` - -Manual fact check: - -```json -{ - "type": "fact_check" -} -``` - -Streaming events: - -```jsonl -{ "type": "agent_runtime_start" } -{ "type": "message_start", "message_id": "...", "role": "brain", "context": {} } -{ "type": "thinking_chunk", "message_id": "...", "chunk": "..." } -{ "type": "message_chunk", "message_id": "...", "chunk": "..." } -{ "type": "message_end", "message_id": "..." } -{ "type": "agent_runtime_end" } -{ "type": "message_error", "message_id": "...", "text": "..." } -``` - -Runtime log event: - -```json -{ "type": "log", "tag": "[RUNTIME]", "message": "..." } -``` - -Runtime action events: - -```jsonl -{ "type": "runtime_action", "action": "save_active_memory", "runtime_turn_id": "...", "runtime_message_id": "...", "text": "SAVE_ACTIVE_MEMORY: Remind the user to check coffee", "payload": "Remind the user to check coffee", "active_memory": "active_memory_1: Remind the user to check coffee [ active_memory_id: a1b2c3 ] [ conditions: Remind the user to check coffee ] [ status: pending ]" } -{ "type": "runtime_action", "action": "save_active_memory", "status": "completed", "runtime_turn_id": "...", "runtime_message_id": "..." } -``` - -Guarded runtime action confirmation: - -```json -{ - "type": "runtime_action_guard_confirmation", - "action": "save_session", - "confirmation_id": "...", - "status": "pending", - "text": "SAVE_SESSION", - "missing_triggers": ["сохрани сессию", "save session"], - "timeout_ms": 0 -} -``` - -Runtime memory update: - -```json -{ - "type": "runtime_memory_update", - "memory": "- active topic: feature testing\n- user intent: testing runtime behavior", - "updates": 6, - "snapshot_index": 2, - "snapshots_count": 3, - "snapshot": { - "session_id": "...", - "index": 2, - "raw_memory": "active topic: feature testing\nuser intent: testing runtime behavior", - "lines": [ - { - "key": "active topic", - "value": "feature testing", - "key_status": "same", - "value_status": "changed", - "key_change_ratio": 0.0, - "value_change_ratio": 0.42 - } - ] - } -} -``` - -Active-memory records sync: - -```json -{ - "type": "active_memory_records_update", - "active_memory_records": [ - "active_memory_1: Remind the user to check coffee [ active_memory_id: a1b2c3 ] [ conditions: Remind the user to check coffee ] [ status: pending ]" - ] -} -``` - -Runtime L1 diff update (incremental key-level change history): - -```json -{ - "type": "runtime_l1_diff_update", - "diffs": [...], - "stats": { "total_changes": 4, "keys_added": 1, "keys_changed": 3 }, - "strength_map": { "active topic": 0.8 }, - "strength_zones": { "high": ["active topic"], "low": [] } -} -``` - -Session memory update (L3 digest, sent after save or restore): - -```json -{ - "type": "runtime_session_memory_update", - "memory": "- decided: use separate runtimes\n- user: prefers terse replies", - "updates": 2, - "source": "L3", - "persist": true, - "session_first_turn": 1, - "session_last_turn": 8 -} -``` - -## Frontend - -The UI is served directly by FastAPI: - -- `ui/templates/index.html` renders the shell. -- `ui/static/js/socket.js` owns connection and reconnect orchestration; `ui/static/js/socket/` handles input, stream events, runtime actions, memory, and delayed-memory messages. -- `ui/static/js/chat.js` owns the chat shell; `chat-attachments.js`, `chat-response-formatter.js`, and `chat-runtime-actions.js` handle attachments, formatted responses, and action bubbles. -- `ui/static/js/status.js` updates provider online/offline indicators. -- `ui/static/js/logger/` contains the runtime console: shared panel/helpers, trace modal, L1 summarizer stream, session-action history, and generic log entries. -- `ui/static/js/think-rule-worker.js` scans completed thinking blocks for trusted-context citations. -- `ui/static/js/dragdrop.js` handles file and image attachment collection, previews, modals, and removal. -- `ui/static/js/runtime/runtime-storage.js` wraps browser storage for runtime/session/active memory. -- `ui/static/js/runtime/runtime-session.js` handles L3 session persistence and bootstrap memory. -- `ui/static/js/runtime/runtime-memory-model.js` parses and normalizes runtime memory lines. -- `ui/static/js/runtime/runtime-memory-view.js` renders memory lines, tags, hover details, and highlights. -- `ui/static/js/runtime/runtime-avatar.js` applies live avatar and scene-color transitions. -- `ui/static/js/runtime-action-counter.js` keeps ordered multi-marker action counters synchronized with action bubbles and logs. -- `ui/static/js/runtime/runtime-panel.js` owns the right-panel controls and telemetry display. -- `ui/static/js/runtime/runtime-feedback.js` tracks last-response feedback. -- `ui/static/js/runtime/runtime-idle.js` tracks user-idle context. -- `ui/static/js/runtime/runtime.js` connects the runtime-memory modules to socket events. - -The frontend uses vanilla JavaScript and Tailwind from CDN. The current input behavior is keyboard-first: Enter sends, Ctrl/Shift+Enter inserts a newline, and the whole input field becomes a red stop control while a generation is active. - -## Future Features - -The following capabilities are planned but not yet implemented. - -**Long-term facts layer (L4).** A cross-session key-fact store extracted from completed turns by the service model, stored as JSON, and retrieved via keyword scoring before each brain call. Facts carry category, relevance, confidence, and mention count. A deduplication pass prevents drift from accumulating near-duplicate entries. The top-N retrieved facts are injected into the brain prompt as low-priority background context. No vector search or embedding index; heuristic scoring only for MVP. - -**User and JIN (LX layer) profiles.** A periodic distillation of session snapshots into two versioned JSON files: `user_profile.json` (stable preferences, recurring themes, friction points, open projects) and `jin_profile.json` (emergent behavioral biases, voice tendencies, avoidances). Profiles are built from snapshot archives in batches, not in real time. They are injected as soft background context, not as hard identity constraints. Old profile versions are kept for rollback. - -**Trusted archive search.** A `TRUSTED_ARCHIVE_SEARCH` runtime action that retrieves original message logs when the runtime memory is disputed, a user says "you said" or "we already discussed this", or a summarizer conclusion needs verification. The archive is not injected into the normal context; it is queried on demand. Retrieval results are treated as primary evidence, not as instruction. - -**Brain fallback on low repair score.** When a service-model code/diff attempt scores below a configurable threshold (default 50), the next repair attempt is routed to the brain model with a clean snapshot containing only the original task, the current file state, the failed patch, and the exact error. The brain model does not receive the previous model's reasoning chain. - -**Night Brain — cross-session consolidation.** An offline background process that reads completed session snapshots, identifies durable patterns versus one-time events, proposes permanent memory updates, and prepares a morning brief. The first iteration operates on session snapshots only; it does not touch raw message logs. Night Brain also drives a watchlist: observations flagged by intent analysis are checked once during a low-traffic window. Allowed actions are `observe` and `analyze` only; nothing is posted or modified without explicit user approval. - -**Background LLM job queue.** A non-blocking `BackgroundLLMJob` model and in-memory worker that moves heavy service-model calls (L3 session saves, memory consolidation, future night-brain tasks) out of the interactive chat path. The worker runs as an `asyncio` task inside the existing `lifespan` hook, respects a concurrency semaphore, and logs through the existing `log_memory_event` channel. Disabled by default via `BACKGROUND_LLM_ENABLED = False`. A Stage 2 adds fair scheduling across job sources to prevent one session from starving other background work. - +GitHub Actions runs the fast suite. Model-dependent probes remain local unless CI is connected to a compatible runtime. diff --git a/contracts/jin_color.json b/contracts/jin_color.json index 7353beae..c4841cef 100644 --- a/contracts/jin_color.json +++ b/contracts/jin_color.json @@ -10,13 +10,9 @@ "emit_followup": false }, "rules": [ - "JIN_COLOR:", - "Use JIN_COLOR to set single color for the live JIN avatar center point color and chat interface background tint.", - "Emit marker using exactly this schema after the normal user-facing response.", - "Skip marker if needed color already set or use marker to set a new color.", - "Place markers at the first line before user-facing text to set color immediately, or choose any other placement.", - "Emit two or more JIN_COLOR markers in one message for consecutive color change, last color in sequence will be used as current.", - "If user asks you to blink or you want blinking animation - you must emit several markers with different colors in single response.", + "Use to set single color for the live JIN avatar center point color and chat interface background tint.", + "Emit marker using exactly this schema ", + "Emit several markers in one message for consecutive color change or blinking effect, last color in sequence will be used as current.", "The payload must be a hex color with optional leading # and exactly 3 or 6 hex characters." ], "close_tag": false diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index 13de6dcc..6eb3d5a0 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -32,6 +32,7 @@ ("SAVE_DELAYED_MEMORY_CONTENT", "CAN_SAVE_DELAYED_MEMORY"), ("APPEND_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("REMOVE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), + ("UPDATE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("SAVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), ("RESOLVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), ) @@ -298,6 +299,7 @@ def normalize_runtime_action_names(enabled_actions=None) -> tuple[str, ...]: normalized_names.extend(( "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", )) if normalized_name == "ASSET_ACTION": @@ -531,6 +533,7 @@ def append_rules(action_name: str) -> None: if normalized_name in { "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", } and not _context_has_delayed_memory_reports(context): continue @@ -563,6 +566,9 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_REMOVE_DELAYED_MEMORY = get_runtime_action_name( "remove_delayed_memory" ) +RUNTIME_ACTION_UPDATE_DELAYED_MEMORY = get_runtime_action_name( + "update_delayed_memory" +) RUNTIME_ACTION_SAVE_ACTIVE_MEMORY = get_runtime_action_name( "save_active_memory" ) diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 3e0ddb48..2d6befc5 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -15,17 +15,20 @@ "emit_followup": true }, "rules": [ - "Use this marker ONLY when the user explicitly asks to create or save in delayed memory a summary/digest/recap/report of the current state.", - "Skip save_delayed_memory when user request is to save session or append already existing delayed memory report.", - "Emit fulfilled form inside marker:", + "Use this marker only after the user explicitly asks to create/save a delayed memory report or explicitly accepts a proposal to consolidate L4 facts into delayed memory.", + "Skip save_delayed_memory when the user request is to save session or append an already existing delayed memory report.", + "Emit the fulfilled form inside the marker:", "", "title:", "summary:", "tags:", "body:", + "long_term_facts_ids:", "", - "You must fulfill ALL fields of delayed memory form so it will be valid for processing by runtime.", - "DO NOT add any other text while making a report! Do not notify user or acknowledge saving and immediately proceed with marker in response." + "You must fulfill title, summary, tags, and body so the report is valid for runtime processing.", + "When the report consolidates facts from , fill long_term_facts_ids with the raw ids copied from the [ id: ... ] suffixes of every fact absorbed by the report, separated by commas.", + "Do not list unrelated ids. Leave long_term_facts_ids empty when the report does not consolidate L4 facts.", + "DO NOT add any other text while making a report. Do not notify or acknowledge saving; immediately emit only the marker." ], "close_tag": true } diff --git a/contracts/update_delayed_memory.json b/contracts/update_delayed_memory.json new file mode 100644 index 00000000..8f5a57a5 --- /dev/null +++ b/contracts/update_delayed_memory.json @@ -0,0 +1,28 @@ +{ + "update_delayed_memory": { + "version": 1, + "description": "Allows the model to append selected fields to an existing delayed memory report.", + "runtime_action": "UPDATE_DELAYED_MEMORY", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": true + }, + "rules": [ + "Use this marker only after the user explicitly asks to update an existing delayed memory report or explicitly accepts a proposal to extend one.", + "The marker appends only tags, body, and long_term_facts_ids to an existing report. It never replaces title, summary, or existing content.", + "Emit the fulfilled form:", + "", + "tags:", + "long_term_facts_ids:", + "body:", + "", + "Replace id with the 6-character report id shown in .", + "List only new tags, new body content, and the raw L4 ids actually absorbed by this update.", + "At least one of tags, long_term_facts_ids, or body must be non-empty.", + "Do not add any other text while updating a report. Immediately emit only the marker." + ], + "close_tag": true + } +} diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index 33cf6159..565b1b9e 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -40,6 +40,13 @@ "CAN_JIN_COLOR": True, } +APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS = ( + "title", + "summary", + "tags", + "body", +) + def build_loop_rules( context=None, @@ -576,14 +583,14 @@ def build_appended_delayed_memory_context( format_tool_result_payload, ) from utils.brain_client_utils import ( - get_appended_delayed_memory_report, + include_pinned_delayed_memory_reports, indent_xml, ) if context is None: return "" - appended_reports = get_appended_delayed_memory_report( + appended_reports = include_pinned_delayed_memory_reports( context ) @@ -600,9 +607,12 @@ def build_appended_delayed_memory_context( continue payload = { - **report, "id": report_id, } + for field_name in APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS: + if field_name in report: + payload[field_name] = report[field_name] + blocks.append( "\n" f"{indent_xml(escape(format_tool_result_payload(payload)))}\n" diff --git a/rules/runtime.py b/rules/runtime.py index 068a051b..cffd6649 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -63,6 +63,9 @@ "Propose saving the session when the conversation has reached a stable checkpoint worth restoring later, especially after a substantial task, decision, or coherent phase is complete.\n" "Propose active memory when the user introduces a concrete unresolved intention, condition, reminder, promise, or future checkpoint that would be useful to keep pending.\n" "Propose a delayed memory report when a substantial reusable result, analysis, design, or report has crystallized and may be useful to append or continue in another context later.\n" + "When contains a coherent cluster of detailed facts that no longer needs to remain always-on, propose consolidating that cluster into one delayed memory report. State which fact keys and ids would be grouped, what the report would preserve, and why moving them would reduce context noise.\n" + "When new long-term facts belong to an existing delayed memory report, propose extending that report instead of creating a duplicate. Name the report and list the exact fact keys and ids that would be appended.\n" + "Do not propose moving identity anchors, broad user preferences, core project rules, or facts that remain useful across unrelated topics. Never emit a save or update marker until the user explicitly accepts the proposal.\n" "Phrase the proposal as one short natural sentence describing what would be preserved and why it may help. Ask for confirmation and never imply that anything has already been saved.\n" "Do not propose after trivial exchanges, while the idea is still unstable, or merely because the topic changed. Do not repeat a declined or ignored proposal unless meaningful new state has appeared.\n" ) diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index e72a98b5..ec30a872 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -24,6 +24,9 @@ normalize_l4_merge_operations, normalize_l4_store, ) +from utils.actions.save_delayed_memory_utils import ( + collect_long_term_fact_ids_from_reports, +) from runtime.memory_common import ( build_memory_failure_details, build_runtime_summarizer_payload, @@ -52,6 +55,23 @@ def get_l4_idle_seconds() -> int: ) +def refresh_runtime_l4_archived_fact_ids( + context, +) -> set[str]: + + fact_ids = collect_long_term_fact_ids_from_reports( + getattr( + context, + "delayed_memory_reports", + {}, + ) + ) + context.runtime_l4_archived_fact_ids = set( + fact_ids + ) + return context.runtime_l4_archived_fact_ids + + def ensure_runtime_l4_state(context) -> dict: context.runtime_facts_memory_records = normalize_facts_memory_records( getattr(context, "runtime_facts_memory_records", []) @@ -367,8 +387,26 @@ def schedule_l4_memory_idle_update( def build_runtime_l4_memory_context(*, context) -> str: if not l4_memory_enabled(): return "" + store = ensure_runtime_l4_state(context) - return format_long_term_memory_context(store.get("facts") or []) + archived_fact_ids = refresh_runtime_l4_archived_fact_ids( + context + ) + active_facts = [ + fact + for fact in store.get("facts") or [] + if str( + fact.get( + "id", + "", + ) + or "" + ).strip().casefold() + not in archived_fact_ids + ] + return format_long_term_memory_context( + active_facts + ) async def delete_l4_memory_fact(context, fact_id: str) -> bool: diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 5bf76502..8c7e50f4 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -826,15 +826,34 @@ def format_l4_fact_line(fact: dict, *, include_metadata: bool = True) -> str: def format_long_term_memory_context(facts: list[dict]) -> str: - lines = [ - format_l4_fact_line(fact, include_metadata=False) - for fact in facts - ] - lines = [line for line in lines if line] + lines = [] + + for fact in facts: + line = format_l4_fact_line( + fact, + include_metadata=False, + ) + fact_id = normalize_l4_text( + fact.get( + "id", + "", + ) + ) + + if not line or not fact_id: + continue + + lines.append( + f"{line} [ id: {fact_id} ]" + ) + if not lines: return "" - body = "\n".join(escape(line) for line in lines) + body = "\n".join( + escape(line) + for line in lines + ) return f"\n{body}\n" diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index f4e7c2bb..f26e5f5b 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -105,6 +105,10 @@ class RuntimeContext: default_factory=list ) + runtime_pinned_delayed_memory_turns: dict[str, str] = field( + default_factory=dict + ) + runtime_delayed_memory_file_warnings: list[str] = field( default_factory=list ) @@ -175,6 +179,10 @@ class RuntimeContext: default_factory=dict ) + runtime_l4_archived_fact_ids: set[str] = field( + default_factory=set + ) + runtime_l4_memory_update_task: object | None = None runtime_usage_events: list[dict] = field( diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index a19445e3..9a00ae9a 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -34,11 +34,14 @@ get_save_active_memory_placeholder_payload, normalize_jin_color_payload, parse_delayed_memory_content_payload, + parse_update_delayed_memory_payload, ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( append_delayed_memory_runtime_result, + build_delayed_memory_report, flush_pending_active_memory_resolve_failure_history, + include_pinned_delayed_memory_reports, ) from utils.context.context_exports import build_tool_results_context from utils.file_manager_asset_utils import read_asset_text_preview @@ -133,12 +136,82 @@ def test_parses_delayed_memory_content_payload(self): "### Radius of Influence Specs\n\n" "A complete, self-sufficient summary..." ), + "pinned": False, + "long_term_facts_ids": [], "created_session_id": "session-1", "created_time": "2026-06-29T12:00:00", }, ) + def test_parses_long_term_fact_ids_for_delayed_memory_report(self): + + report = parse_delayed_memory_content_payload( + ( + "title: Project context\n" + "summary: Consolidated project details.\n" + "tags: project, context\n" + "body:\n" + "Reusable project summary.\n" + "long_term_facts_ids: " + "l4_abc123, l4_def456, invalid, l4_abc123" + ) + ) + + report_value = next( + iter(report.values()) + ) + + self.assertEqual( + report_value["long_term_facts_ids"], + [ + "l4_abc123", + "l4_def456", + ], + ) + + + def test_build_report_keeps_only_existing_l4_fact_ids(self): + + context = SimpleNamespace( + session_id="session-1", + timestamp="2026-08-02T19:00:00", + runtime_long_term_memory_store={ + "facts": [ + { + "id": "l4_existing", + "key": "project.fact", + "value": "Existing fact", + }, + ], + }, + ) + report = build_delayed_memory_report( + context, + json.dumps({ + "abc123": { + "title": "Project context", + "summary": "Summary", + "tags": ["project"], + "body": "Body", + "long_term_facts_ids": [ + "l4_existing", + "l4_missing", + ], + }, + }), + ) + + report_value = report["abc123"] + + self.assertEqual( + report_value["long_term_facts_ids"], + [ + "l4_existing", + ], + ) + + def test_extracts_delayed_memory_content_block(self): result = extract_runtime_actions( @@ -810,6 +883,19 @@ def test_append_delayed_memory_uses_appended_context_block(self): "tag", ], "body": "Body", + "long_term_facts_ids": [ + "14_1dbac3ba8724", + ], + "created_session_id": "session-a", + "created_time": "2026-08-02T19:51:41.803270", + "created_date": "2026-08-02T19:51:41.803270", + "appended_times": 1, + "append_streak": 1, + "last_appended_date": "2026-08-02T19:57:42.787241", + "last_appended_session_id": "session-a", + "all_appended_session_ids": [ + "session-a", + ], }, "b2c3d4": { "title": "Second report", @@ -855,6 +941,29 @@ def test_append_delayed_memory_uses_appended_context_block(self): '"id": "a1b2c3"', appended_context, ) + self.assertLess( + appended_context.index( + '"id": "a1b2c3"', + ), + appended_context.index( + '"title":', + ), + ) + for metadata_key in ( + "long_term_facts_ids", + "created_session_id", + "created_time", + "created_date", + "appended_times", + "append_streak", + "last_appended_date", + "last_appended_session_id", + "all_appended_session_ids", + ): + self.assertNotIn( + metadata_key, + appended_context, + ) self.assertEqual( context.emitter.events[0]["text"], ( @@ -1610,3 +1719,231 @@ def test_missing_remove_delayed_memory_id_returns_failed_result(self): tool_results, ) + def test_extracts_update_delayed_memory_block_with_header_id(self): + + result = extract_runtime_actions( + ( + "\n" + "tags: social, context\n" + "long_term_facts_ids: l4_existing, l4_missing\n" + "body:\n" + "Additional durable context.\n" + "" + ), + enabled_actions=( + "UPDATE_DELAYED_MEMORY", + ), + ) + + self.assertEqual( + result.text, + "", + ) + self.assertEqual( + len(result.actions), + 1, + ) + self.assertEqual( + result.actions[0].name, + "UPDATE_DELAYED_MEMORY", + ) + self.assertEqual( + parse_update_delayed_memory_payload( + result.actions[0].payload + ), + { + "id": "a1b2c3", + "tags": [ + "social", + "context", + ], + "long_term_facts_ids": [ + "l4_existing", + "l4_missing", + ], + "body": "Additional durable context.", + }, + ) + + + def test_update_delayed_memory_appends_only_supported_fields(self): + + context = FakeContext() + context.emitter = FakeEmitter() + context.runtime_action_events = [] + context.runtime_search_calls = [] + context.runtime_appended_skills = [] + context.runtime_asset_results = [] + context.runtime_delayed_memory_results = [] + context.delayed_memory_reports = { + "a1b2c3": { + "title": "Social context", + "summary": "Summary", + "tags": [ + "social", + ], + "body": "Original body.", + "long_term_facts_ids": [ + "l4_existing", + ], + "pinned": False, + }, + } + context.runtime_long_term_memory_store = { + "facts": [ + { + "id": "l4_existing", + "key": "user.name", + "value": "Sergey", + }, + { + "id": "l4_new", + "key": "user.role", + "value": "Developer", + }, + ], + } + + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="UPDATE_DELAYED_MEMORY", + payload=( + "a1b2c3\n" + "tags: context, social\n" + "long_term_facts_ids: l4_new, l4_missing\n" + "body:\n" + "Additional body." + ), + ), + ), + ) + ) + + self.assertEqual(applied_count, 1) + report = context.delayed_memory_reports["a1b2c3"] + self.assertEqual( + report["tags"], + [ + "social", + "context", + ], + ) + self.assertEqual( + report["long_term_facts_ids"], + [ + "l4_existing", + "l4_new", + ], + ) + self.assertEqual( + report["body"], + "Original body.\n\nAdditional body.", + ) + self.assertEqual( + context.emitter.events[0]["status"], + "completed", + ) + self.assertIn( + '', + build_tool_results_context(context), + ) + + + def test_pinned_delayed_memory_is_included_once_per_turn(self): + + context = SimpleNamespace( + delayed_memory_reports={ + "a1b2c3": { + "title": "Pinned context", + "summary": "Summary", + "tags": [], + "body": "Pinned body", + "pinned": True, + }, + }, + runtime_appended_delayed_memory={}, + runtime_current_turn_id="turn-1", + runtime_pinned_delayed_memory_turns={}, + session_id="session-1", + timestamp="2026-08-02T20:00:00", + delayed_memory_file_store_enabled=False, + ) + + first = include_pinned_delayed_memory_reports(context) + second = include_pinned_delayed_memory_reports(context) + + self.assertIn("a1b2c3", first) + self.assertIn("a1b2c3", second) + self.assertEqual( + context.delayed_memory_reports["a1b2c3"]["appended_times"], + 1, + ) + + context.runtime_current_turn_id = "turn-2" + context.timestamp = "2026-08-02T20:01:00" + include_pinned_delayed_memory_reports(context) + + self.assertEqual( + context.delayed_memory_reports["a1b2c3"]["appended_times"], + 2, + ) + self.assertEqual( + context.delayed_memory_reports["a1b2c3"]["last_appended_date"], + "2026-08-02T20:01:00", + ) + + + def test_remove_pinned_delayed_memory_returns_failed_result(self): + + context = FakeContext() + context.emitter = FakeEmitter() + context.runtime_action_events = [] + context.runtime_search_calls = [] + context.runtime_appended_skills = [] + context.runtime_asset_results = [] + context.runtime_delayed_memory_results = [] + context.runtime_appended_delayed_memory = { + "a1b2c3": { + "id": "a1b2c3", + "title": "Pinned report", + "pinned": True, + }, + } + context.delayed_memory_reports = { + "a1b2c3": { + "title": "Pinned report", + "summary": "Summary", + "tags": [], + "body": "Body", + "pinned": True, + }, + } + + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="REMOVE_DELAYED_MEMORY", + payload="a1b2c3", + ), + ), + ) + ) + + self.assertEqual(applied_count, 1) + self.assertEqual( + context.emitter.events[0]["status"], + "failed", + ) + self.assertEqual( + context.runtime_delayed_memory_results[0]["error"], + "delayed_memory_pinned", + ) + self.assertIn( + "a1b2c3", + context.runtime_appended_delayed_memory, + ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index b2ce105c..d18a74de 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -120,6 +120,7 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: "SAVE_DELAYED_MEMORY_CONTENT", "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", ) ) diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 8b7dcc36..db644762 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -68,6 +68,10 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): "all_appended_session_ids", storage_source, ) + self.assertIn( + "long_term_facts_ids", + storage_source, + ) self.assertIn( "collectCurrentSessionAppendedMemoryIds", storage_source, diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index 93d8e87b..ff1bb36e 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -30,6 +30,11 @@ def build_report( "evolution", ], "body": body, + "pinned": True, + "long_term_facts_ids": [ + "l4_abc123", + "l4_def456", + ], "created_session_id": "session-a", "created_time": "2026-07-19T16:14:14.628194", "created_date": "2026-07-19T16:14:14.628194", @@ -71,6 +76,17 @@ def test_persist_and_load_direct_report_shape(self): payload["time"], "2026-07-19T16:14:14.628194", ) + self.assertEqual( + payload["pinned"], + True, + ) + self.assertEqual( + payload["long_term_facts_ids"], + [ + "l4_abc123", + "l4_def456", + ], + ) reports, warnings = ( load_delayed_memory_reports_from_files( @@ -90,6 +106,17 @@ def test_persist_and_load_direct_report_shape(self): reports["48ggds"]["created_session_id"], "session-a", ) + self.assertEqual( + reports["48ggds"]["pinned"], + True, + ) + self.assertEqual( + reports["48ggds"]["long_term_facts_ids"], + [ + "l4_abc123", + "l4_def456", + ], + ) def test_save_replaces_old_filename_for_same_id(self): @@ -185,10 +212,12 @@ def test_local_storage_reports_override_file_fallback_by_id(self): title="File only", ), } + browser_report = self.build_report( + title="Browser title", + ) + browser_report["long_term_facts_ids"] = [] browser_reports = { - "48ggds": self.build_report( - title="Browser title", - ), + "48ggds": browser_report, } merged = merge_delayed_memory_reports( @@ -204,6 +233,13 @@ def test_local_storage_reports_override_file_fallback_by_id(self): merged["a1b2c3"]["title"], "File only", ) + self.assertEqual( + merged["48ggds"]["long_term_facts_ids"], + [ + "l4_abc123", + "l4_def456", + ], + ) def test_websocket_sync_merges_browser_primary_with_file_fallback(self): diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 7b97df2e..0f3189e9 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1,6 +1,10 @@ +import json import unittest -from runtime.L4_memory import maybe_update_runtime_l4_memory +from runtime.L4_memory import ( + build_runtime_l4_memory_context, + maybe_update_runtime_l4_memory, +) from runtime.L4_memory_utils import ( add_l4_pending_candidates, apply_l4_merge_operations, @@ -18,6 +22,10 @@ from runtime.runtime_context import RuntimeContext from rules.brain_context_builder import build_brain_context from tests.helpers.memory import FakeLogger, FakeServiceClient +from utils.actions import RuntimeActionCall +from utils.actions.delayed_memory_actions import ( + apply_save_delayed_memory_actions, +) class FakeEmitter: @@ -280,9 +288,143 @@ def test_context_formats_all_facts_without_metadata(self): self.assertIn("user.hardware.main_gpu:", context_block) self.assertIn("user.preference.response_style:", context_block) + self.assertIn("[ id: l4_gpu ]", context_block) + self.assertIn("[ id: l4_style ]", context_block) self.assertNotIn("source_session_ids", context_block) self.assertIn("[source_session_ids: session-a]", ui_line) + def test_delayed_report_fact_ids_hide_only_from_brain_context(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "l4_archived", + "key": "project.topic.details", + "value": "Detailed topic facts moved into a report.", + }, + { + "id": "l4_active", + "key": "user.name", + "value": "Sergey", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Topic report", + "long_term_facts_ids": [ + "l4_archived", + ], + }, + } + + context_block = build_runtime_l4_memory_context( + context=context + ) + + self.assertNotIn( + "project.topic.details", + context_block, + ) + self.assertIn( + "user.name: Sergey [ id: l4_active ]", + context_block, + ) + self.assertEqual( + context.runtime_l4_archived_fact_ids, + { + "l4_archived", + }, + ) + self.assertEqual( + len( + context.runtime_long_term_memory_store[ + "facts" + ] + ), + 2, + ) + + + async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "l4_project_details", + "key": "project.topic.details", + "value": "Detailed project context.", + }, + ], + }) + payload = json.dumps({ + "abc123": { + "title": "Project context", + "summary": "Consolidated project details.", + "tags": [ + "project", + ], + "body": "Reusable project report.", + "long_term_facts_ids": [ + "l4_project_details", + ], + }, + }) + + await apply_save_delayed_memory_actions( + context, + [ + RuntimeActionCall( + name="SAVE_DELAYED_MEMORY_CONTENT", + payload=payload, + ), + ], + log_runtime=None, + with_action_context=lambda event: event, + ) + + self.assertEqual( + context.delayed_memory_reports[ + "abc123" + ][ + "long_term_facts_ids" + ], + [ + "l4_project_details", + ], + ) + self.assertEqual( + context.runtime_l4_archived_fact_ids, + { + "l4_project_details", + }, + ) + self.assertEqual( + build_runtime_l4_memory_context( + context=context + ), + "", + ) + self.assertEqual( + len( + context.runtime_long_term_memory_store[ + "facts" + ] + ), + 1, + ) + + def test_brain_context_always_injects_complete_long_term_memory(self): context = RuntimeContext( websocket=None, @@ -427,8 +569,8 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( self.assertEqual(context.runtime_long_term_memory_store["pending_facts"], []) self.assertEqual(len(context.runtime_long_term_memory_store["facts"]), 2) self.assertEqual(len(service_client.calls), 3) - self.assertIn("permanent long-term memory", service_client.calls[0]["system_prompt"]) - self.assertIn("consolidate all pending", service_client.calls[2]["system_prompt"].lower()) + self.assertIn("cross-session long-term memory", service_client.calls[0]["system_prompt"]) + self.assertIn("consolidate pending candidates", service_client.calls[2]["system_prompt"].lower()) if __name__ == "__main__": diff --git a/ui/static/css/base.css b/ui/static/css/base.css index 87e974ba..d58066b2 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -21,6 +21,13 @@ :root { --panel-gap: 24px; --panel-width: 330px; + --panel-collapsed-title-height: 40px; + --runtime-avatar-panel-size: clamp(278px, 30vh, 286px); + --settings-avatar-collapsed-height: calc(var(--runtime-avatar-panel-size) + 2px); + --panel-collapse-duration: 0.22s; + --panel-startup-collapse-duration: 5s; + --panel-collapse-easing: cubic-bezier(0.45, 0, 0.55, 1); + --panel-geometry-transition: height var(--panel-collapse-duration) var(--panel-collapse-easing), min-height var(--panel-collapse-duration) var(--panel-collapse-easing), max-height var(--panel-collapse-duration) var(--panel-collapse-easing); --safe-left: 380px; --safe-right: 380px; @@ -31,6 +38,13 @@ --jin-color: #1f4f8f; --scene-base-color: #0f1b2c; --scene-jin-tint-alpha: 0.40; + --scene-context-tint-boost: 0; + + --scene-layer-base: 0; + --scene-layer-context: 1; + --scene-layer-search: 2; + --scene-layer-tint: 3; + --scene-layer-shade: 4; /* Вертикальный ритм чата: добавляет немного воздуха между ходами. */ --chat-turn-gap: 2.05rem; @@ -48,8 +62,12 @@ main { transition: background-color 0.22s ease; } +main.panel-startup-collapse-active { + --panel-collapse-duration: var(--panel-startup-collapse-duration); +} + #scene-bg { - z-index: 0; + z-index: var(--scene-layer-base); background-image: url('/static/images/states/default.jpg'); background-size: cover; background-position: center; @@ -66,7 +84,7 @@ main { #scene-clutter-overlay, #scene-cluttered-overlay { - z-index: 1; + z-index: var(--scene-layer-context); transition: opacity 3s ease; } @@ -79,7 +97,7 @@ main { } #scene-search-overlay { - z-index: 2; + z-index: var(--scene-layer-search); transition: opacity 0.22s ease; background-image: url('/static/images/states/searching.png'); @@ -92,18 +110,32 @@ main.scene-searching #scene-search-overlay { opacity: 1; } +main.scene-clutter-middle { + --scene-context-tint-boost: 0.06; +} + +main.scene-cluttered { + --scene-context-tint-boost: 0.12; +} + +main.scene-searching { + --scene-context-tint-boost: 0.08; +} + #scene-jin-tint { - z-index: 3; + z-index: var(--scene-layer-tint); background-color: var(--jin-color); - opacity: var(--scene-jin-tint-alpha); - mix-blend-mode: multiply; + opacity: min( + 0.34, + calc(var(--scene-jin-tint-alpha) + var(--scene-context-tint-boost)) + ); transition: background-color 0.22s ease, opacity 0.22s ease; } #scene-shade { - z-index: 4; + z-index: var(--scene-layer-shade); background: radial-gradient( circle at 50% 52%, @@ -128,7 +160,7 @@ main.scene-searching #scene-search-overlay { inset: 0; background: #000; opacity: var(--scene-shade-alpha); - transition: opacity 0.22s ease; + transition: opacity var(--panel-collapse-duration) var(--panel-collapse-easing); } main.panels-collapsed-1 #scene-shade { @@ -144,7 +176,14 @@ main.panels-collapsed-2 #scene-shade { width: var(--panel-width); max-height: 100%; opacity: 1; - transition: opacity 0.35s ease; + transition: + var(--panel-geometry-transition), + opacity 0.35s ease; +} + +#console-drag-handle, +#memory-drag-handle { + flex-shrink: 0; } #console-panel:not(.panel-collapsed), @@ -152,16 +191,35 @@ main.panels-collapsed-2 #scene-shade { min-height: 49%; } -#console-panel.panel-collapsed, +#console-panel.panel-collapsed { + height: var(--panel-collapsed-title-height) !important; + min-height: var(--panel-collapsed-title-height) !important; + max-height: var(--panel-collapsed-title-height) !important; +} + #settings-panel.panel-collapsed { - height: 40px !important; - min-height: 40px; - max-height: 40px; + height: var(--settings-avatar-collapsed-height) !important; + min-height: var(--settings-avatar-collapsed-height) !important; + max-height: var(--settings-avatar-collapsed-height) !important; +} + +#console-stream, +.settings-scroll { + max-height: calc(100vh - var(--panel-collapsed-title-height)); + min-height: 0; + transition: + max-height var(--panel-collapse-duration) var(--panel-collapse-easing), + padding-top var(--panel-collapse-duration) var(--panel-collapse-easing), + padding-bottom var(--panel-collapse-duration) var(--panel-collapse-easing); + will-change: max-height, padding-top, padding-bottom; } #console-panel.panel-collapsed #console-stream, #settings-panel.panel-collapsed .settings-scroll { - display: none; + max-height: 0; + padding-top: 0 !important; + padding-bottom: 0 !important; + overflow: hidden; } .panel-bottom-resize-handle { @@ -194,6 +252,11 @@ main.panels-collapsed-2 #scene-shade { display: none; } +#console-panel.panel-resizing, +#settings-panel.panel-resizing { + transition: opacity 0.35s ease; +} + #console-panel { left: var(--panel-gap); top: var(--panel-gap); @@ -226,9 +289,9 @@ main.panels-collapsed-2 #scene-shade { } #memory-drag-handle { - flex: 0 0 40px; - height: 40px; - min-height: 40px; + flex: 0 0 var(--panel-collapsed-title-height); + height: var(--panel-collapsed-title-height); + min-height: var(--panel-collapsed-title-height); } #memory-drag-handle h3 { @@ -529,6 +592,14 @@ main.panels-collapsed-2 #scene-shade { #settings-panel { + transition: + border-color 1.8s ease, + box-shadow 1.8s ease, + var(--panel-geometry-transition), + opacity 0.35s ease; +} + +#settings-panel.panel-resizing { transition: border-color 1.8s ease, box-shadow 1.8s ease, @@ -561,6 +632,25 @@ main.panels-collapsed-2 #scene-shade { opacity 0.35s ease; } +main.panel-startup-collapse-active #console-panel, +main.panel-startup-collapse-active #settings-panel { + transition: + height var(--panel-collapse-duration) var(--panel-collapse-easing), + min-height var(--panel-collapse-duration) var(--panel-collapse-easing), + max-height var(--panel-collapse-duration) var(--panel-collapse-easing), + opacity var(--panel-collapse-duration) var(--panel-collapse-easing) !important; +} + +main.panel-startup-collapse-active #settings-panel { + transition: + border-color 1.8s ease, + box-shadow 1.8s ease, + height var(--panel-collapse-duration) var(--panel-collapse-easing), + min-height var(--panel-collapse-duration) var(--panel-collapse-easing), + max-height var(--panel-collapse-duration) var(--panel-collapse-easing), + opacity var(--panel-collapse-duration) var(--panel-collapse-easing) !important; +} + #settings-panel.memory-updating { border-color: rgba(255,190,95,.50); diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 5fdc5b9b..0dc8c287 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -3,9 +3,9 @@ #memory-drag-handle { position: relative; isolation: isolate; - flex: 0 0 clamp(248px, 30vh, 286px); - height: clamp(248px, 30vh, 286px); - min-height: clamp(248px, 30vh, 286px); + flex: 0 0 var(--runtime-avatar-panel-size); + height: var(--runtime-avatar-panel-size); + min-height: var(--runtime-avatar-panel-size); justify-content: center; padding: 8px 10px; overflow: hidden; @@ -246,10 +246,11 @@ } #settings-panel.panel-collapsed #memory-drag-handle { - flex-basis: 40px; - height: 40px; - min-height: 40px; - padding: 2px 10px; + flex: 0 0 var(--runtime-avatar-panel-size); + height: var(--runtime-avatar-panel-size); + min-height: var(--runtime-avatar-panel-size); + padding: 8px 10px; + border-bottom: 0; } #settings-panel.panel-collapsed #memory-drag-handle::before, @@ -258,12 +259,12 @@ } #settings-panel.panel-collapsed .jin-runtime-avatar-shell { - width: 36px; + width: min(100%, 278px); } #settings-panel.panel-collapsed .jin-runtime-avatar-core-button { - width: 24px; - height: 24px; + width: 44px; + height: 44px; } #settings-panel.panel-collapsed .jin-runtime-avatar { @@ -273,9 +274,9 @@ body.theme-win95 #memory-drag-handle { position: relative; /* Theme switching must not resize the live avatar workspace. */ - flex: 0 0 clamp(248px, 30vh, 286px); - height: clamp(248px, 30vh, 286px); - min-height: clamp(248px, 30vh, 286px); + flex: 0 0 var(--runtime-avatar-panel-size); + height: var(--runtime-avatar-panel-size); + min-height: var(--runtime-avatar-panel-size); justify-content: center; padding: 8px 10px; overflow: hidden; @@ -303,19 +304,20 @@ body.theme-win95 #memory-drag-handle .win95-window-controls { } body.theme-win95 #settings-panel.panel-collapsed #memory-drag-handle { - flex-basis: 28px; - height: 28px; - min-height: 28px; - padding: 1px 5px; + flex: 0 0 var(--runtime-avatar-panel-size); + height: var(--runtime-avatar-panel-size); + min-height: var(--runtime-avatar-panel-size); + padding: 8px 10px; + border-bottom: 0; } body.theme-win95 #settings-panel.panel-collapsed .jin-runtime-avatar-shell { - width: 26px; + width: min(100%, 278px); } body.theme-win95 #settings-panel.panel-collapsed .jin-runtime-avatar-core-button { - width: 18px; - height: 18px; + width: 44px; + height: 44px; } @media (prefers-reduced-motion: reduce) { diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index fe23ec8b..b7e181f3 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -282,3 +282,68 @@ var(--memory-change-glow, 0.22) ); } + +.runtime-memory-delayed-row-pinned { + background: rgba(244, 244, 245, 0.035); + box-shadow: + inset 0 0 0 1px rgba(228, 228, 231, 0.08), + 0 0 12px rgba(228, 228, 231, 0.08); +} + +.runtime-memory-delayed-row-pinned .runtime-memory-key, +.runtime-memory-delayed-row-pinned .runtime-memory-value { + color: rgba(244, 244, 245, 0.82); + text-shadow: + 0 0 8px rgba(244, 244, 245, 0.16); +} + +.delayed-memory-modal-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.delayed-memory-modal-icon-button { + display: inline-flex; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; + border: 1px solid transparent; + border-radius: 4px; + color: rgba(161, 161, 170, 0.9); + transition: + color 0.18s ease, + background-color 0.18s ease, + text-shadow 0.18s ease; +} + +.delayed-memory-modal-icon-button:hover, +.delayed-memory-modal-icon-button:focus-visible { + outline: none; + color: rgba(244, 244, 245, 0.98); + background: rgba(244, 244, 245, 0.05); +} + +.delayed-memory-modal-pin svg { + width: 15px; + height: 15px; + fill: currentColor; + transition: filter 0.18s ease; +} + +.delayed-memory-modal-pin-active { + color: rgba(240, 253, 250, 0.98); + background: rgba(240, 253, 250, 0.07); +} + +.delayed-memory-modal-pin-active svg { + filter: + drop-shadow(0 0 5px rgba(240, 253, 250, 0.5)) + drop-shadow(0 0 12px rgba(153, 246, 228, 0.24)); +} + +.delayed-memory-modal-close { + font-size: 20px; + line-height: 1; +} diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 9d679257..31ec61fd 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -1,6 +1,7 @@ /* Переопределения интерфейса для темы Windows 95. */ body.theme-win95 { + --settings-avatar-collapsed-height: calc(var(--runtime-avatar-panel-size) + 4px); --win95-face: #bbbabb; --win95-face-dark: #b8b8b8; --win95-face-soft: #d2d2d2; diff --git a/ui/static/images/jin-core-default-theme.jpg b/ui/static/images/jin-core-default-theme.jpg index cd109dacebfd77e9e7afce434ad4168599d46834..488139560c46358f7fb84a5d8f5c05f2eb4b01b9 100644 GIT binary patch literal 176160 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
HX@CVV8I=$4P}#j1|p4# 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

>sy5x69dab>2S<8+oy7< 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

SQ(dws(TxndQ(-`=`y$ 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 literal 166250 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ít|}*)_OdE(D2{(TR=FB$zT};WVd_}1HQrk!M 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 jp8E)|T=}LRS%uXgtO^pNNSRXG=9Yp2?E{H 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 Oy%gkC<%(tAefmFMO4lO* literal 67787 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 UXV2;P`$tfaQI#%-zIp$D03V7LNdN!< diff --git a/ui/static/images/think-highlight.jpg b/ui/static/images/think-highlight.jpg index 7335f0a1bbb46d77649abf3b4ca752cf73a32cec..494c1e46abb1540af460983888d36f6ccef02136 100644 GIT binary patch literal 237671 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

}rdY%TZmGcx;?1k8PF#-x)i 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

T6s-uDVnnQV|rf7 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%0DfC1l_G=QBJtoVCfszk{5Tcz@KehDidn0 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

@dU}Tu5VXBH0QG~13{dG5D~i?5CN+vv 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 literal 346642 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 z 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
      |TAHv@HMLqTLjAH(k;o@$?O+aj+nL1FoaJvKR? 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©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 { + if ( + panel + && panel.classList.contains("panel-collapsed") + ) { + panel.style.height = + getCollapsedPanelHeight(panel); + panel.style.minHeight = + panel.style.height; + panel.style.maxHeight = + panel.style.height; + } + }); + } + + function getPanelRoot() { + return document.querySelector("main"); + } + + function parseCssDurationMs(duration) { + const value = + String(duration || "").trim(); + + if (!value) { + return 0; + } + + if (value.endsWith("ms")) { + return Number.parseFloat(value) || 0; + } + + if (value.endsWith("s")) { + return (Number.parseFloat(value) || 0) * 1000; + } + + return Number.parseFloat(value) || 0; + } + + function beginStartupCollapseAnimation() { + const root = + getPanelRoot(); + + if (!root) { + return; + } + + if (startupCollapseClassTimer !== null) { + window.clearTimeout( + startupCollapseClassTimer + ); + } + + const duration = + getComputedStyle(root) + .getPropertyValue("--panel-startup-collapse-duration") + .trim() + || "5s"; + + if (startupCollapsePreviousDuration === null) { + startupCollapsePreviousDuration = + root.style.getPropertyValue( + "--panel-collapse-duration" + ); + } + + root.style.setProperty( + "--panel-collapse-duration", + duration + ); + + root.classList.add( + STARTUP_COLLAPSE_CLASS + ); + + root.getBoundingClientRect(); + + const durationMs = + parseCssDurationMs( + duration + ); + + startupCollapseClassTimer = + window.setTimeout( + finishStartupCollapseAnimation, + Math.max(0, durationMs) + 80 + ); + } + + function finishStartupCollapseAnimation() { + cancelStartupCollapseFrame(); + + if (startupCollapseClassTimer !== null) { + window.clearTimeout( + startupCollapseClassTimer + ); + + startupCollapseClassTimer = null; + } + + const root = + getPanelRoot(); + + if (root) { + root.classList.remove( + STARTUP_COLLAPSE_CLASS + ); + + if (startupCollapsePreviousDuration !== null) { + if (startupCollapsePreviousDuration) { + root.style.setProperty( + "--panel-collapse-duration", + startupCollapsePreviousDuration + ); + } else { + root.style.removeProperty( + "--panel-collapse-duration" + ); + } + + startupCollapsePreviousDuration = null; + } + } + } + + function isStartupCollapseAnimationActive() { + const root = + getPanelRoot(); + + return Boolean( + startupCollapseFrameId !== null + || startupCollapseClassTimer !== null + || ( + root + && root.classList.contains( + STARTUP_COLLAPSE_CLASS + ) + ) + ); + } + + function cancelStartupCollapseFrame() { + if (startupCollapseFrameId === null) { + return; + } + + window.cancelAnimationFrame( + startupCollapseFrameId + ); + + startupCollapseFrameId = null; + } + + function getPanelTransitionElements() { + return [ + consolePanel, + consoleStream, + memoryPanel, + memoryPanel + ? memoryPanel.querySelector(".settings-scroll") + : null, + ].filter(Boolean); + } + + function withoutPanelTransitions(callback) { + const elements = + getPanelTransitionElements(); + + const previousTransitions = + elements.map((element) => ({ + element, + transition: + element.style.transition, + })); + + elements.forEach((element) => { + element.style.transition = + "none"; + }); + + callback(); + + elements.forEach((element) => { + element.getBoundingClientRect(); + }); + + window.requestAnimationFrame(() => { + previousTransitions.forEach((entry) => { + if (entry.transition) { + entry.element.style.transition = + entry.transition; + } else { + entry.element.style.removeProperty( + "transition" + ); + } + }); + }); + } + + function expandPanelAfterStartupCancel(panel) { + if ( + !panel + || ( + !panel.classList.contains("panel-collapsed") + && !panel.dataset.expandedHeight + ) + ) { + return; + } + + setPanelCollapsed( + panel, + false + ); + } + + function cancelStartupCollapseAnimation() { + if (!isStartupCollapseAnimationActive()) { + return false; + } + + finishStartupCollapseAnimation(); + + withoutPanelTransitions(() => { + [ + consolePanel, + memoryPanel, + ].forEach( + expandPanelAfterStartupCancel + ); + + syncSceneShadeToPanelCollapse(); + }); + + return true; + } + + function collapsePanelsNow() { + [ + consolePanel, + memoryPanel, + ].forEach((panel) => { + setPanelCollapsed( + panel, + true + ); + }); + + syncSceneShadeToPanelCollapse(); + } + + function collapseAllPanels(options = {}) { + if (options.startup) { + beginStartupCollapseAnimation(); + cancelStartupCollapseFrame(); + + startupCollapseFrameId = + window.requestAnimationFrame( + function () { + startupCollapseFrameId = + window.requestAnimationFrame( + function () { + startupCollapseFrameId = null; + collapsePanelsNow(); + } + ); + } + ); + + return; + } + + finishStartupCollapseAnimation(); + collapsePanelsNow(); + } + function getPanelResizeBounds(panel) { const parentRect = panel.parentElement.getBoundingClientRect(); @@ -532,6 +921,10 @@ const consolePanel = document.getElementById("console-panel"); isResizing = true; + panel.classList.add( + "panel-resizing" + ); + resizeStartY = event.clientY; @@ -567,6 +960,10 @@ const consolePanel = document.getElementById("console-panel"); isResizing = false; + panel.classList.remove( + "panel-resizing" + ); + document.body.style.cursor = ""; @@ -650,6 +1047,17 @@ const consolePanel = document.getElementById("console-panel"); const memoryPanel = document.getElementById("settings-panel"); const memoryDragHandle = document.getElementById("memory-drag-handle"); +window.JinPanels = + Object.assign( + window.JinPanels || {}, + { + collapseAllPanels, + cancelStartupCollapseAnimation, + refreshCollapsedPanelHeights, + syncSceneShadeToPanelCollapse, + } + ); + let isMemoryDragging = false; let memoryOffsetX = 0; let memoryOffsetY = 0; @@ -733,5 +1141,8 @@ requestAnimationFrame( window.addEventListener( "resize", - clampAllPanelGeometry + () => { + clampAllPanelGeometry(); + refreshCollapsedPanelHeights(); + } ); diff --git a/ui/static/js/logger/session-actions.js b/ui/static/js/logger/session-actions.js index b0bd89d3..a4aadc94 100644 --- a/ui/static/js/logger/session-actions.js +++ b/ui/static/js/logger/session-actions.js @@ -577,10 +577,15 @@ function ensureSessionActionsModal() { "button"; closeButton.className = - "text-xs text-zinc-400 hover:text-zinc-100 transition"; + "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:bg-zinc-800/60 hover:text-zinc-100 focus:outline-none"; + + closeButton.setAttribute( + "aria-label", + "Close" + ); closeButton.textContent = - "close"; + "\u00d7"; sessionActionsModalList = document.createElement("div"); diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index a5f6b84f..c5118997 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -47,10 +47,15 @@ function ensureTraceModal() { "button"; closeButton.className = - "text-xs text-zinc-400 hover:text-zinc-100 transition"; + "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:bg-zinc-800/60 hover:text-zinc-100 focus:outline-none"; + + closeButton.setAttribute( + "aria-label", + "Close" + ); closeButton.textContent = - "close"; + "\u00d7"; traceModalReason = document.createElement("div"); diff --git a/ui/static/js/panel-inactivity.js b/ui/static/js/panel-inactivity.js index c8451693..0937a3c5 100644 --- a/ui/static/js/panel-inactivity.js +++ b/ui/static/js/panel-inactivity.js @@ -2,11 +2,272 @@ "use strict"; const PANEL_INACTIVITY_MS = 30_000; + const STARTUP_AUTO_COLLAPSE_MS = 5_000; const INACTIVE_CLASS = "panel-inactive"; + const STARTUP_COLLAPSE_CLASS = "panel-startup-collapse-active"; const PANEL_IDS = [ "console-panel", "settings-panel", ]; + let startupAutoCollapseTimerId = null; + let startupAutoCollapseCancelled = false; + let startupFallbackCleanupTimerId = null; + let startupFallbackPreviousDuration = null; + + function getPanels() { + return PANEL_IDS + .map((panelId) => document.getElementById(panelId)) + .filter(Boolean); + } + + function clearStartupAutoCollapseTimer() { + if (startupAutoCollapseTimerId === null) { + return; + } + + window.clearTimeout(startupAutoCollapseTimerId); + startupAutoCollapseTimerId = null; + } + + function clearStartupFallbackCleanupTimer() { + if (startupFallbackCleanupTimerId === null) { + return; + } + + window.clearTimeout(startupFallbackCleanupTimerId); + startupFallbackCleanupTimerId = null; + } + + function restoreStartupFallbackDuration(root) { + if (startupFallbackPreviousDuration === null) { + return; + } + + if (startupFallbackPreviousDuration) { + root.style.setProperty( + "--panel-collapse-duration", + startupFallbackPreviousDuration + ); + } else { + root.style.removeProperty( + "--panel-collapse-duration" + ); + } + + startupFallbackPreviousDuration = null; + } + + function cancelStartupAutoCollapse() { + startupAutoCollapseCancelled = true; + clearStartupAutoCollapseTimer(); + } + + function registerStartupPanelActivity() { + cancelStartupAutoCollapse(); + cancelActiveStartupCollapseAnimation(); + } + + function isWindowActive() { + return ( + document.visibilityState === "visible" + && document.hasFocus() + ); + } + + function isAnyPanelHovered() { + return getPanels().some((panel) => { + try { + return panel.matches(":hover"); + } catch (_error) { + return false; + } + }); + } + + function syncSceneShadeToPanelCollapse() { + const root = + document.querySelector("main"); + + if (!root) { + return; + } + + const collapsedCount = + getPanels().filter((panel) => ( + panel.classList.contains("panel-collapsed") + )).length; + + root.classList.remove( + "panels-collapsed-1", + "panels-collapsed-2" + ); + + if (collapsedCount > 0) { + root.classList.add( + `panels-collapsed-${collapsedCount}` + ); + } + } + + function cancelActiveStartupCollapseAnimation() { + if ( + window.JinPanels + && typeof window.JinPanels.cancelStartupCollapseAnimation === "function" + && window.JinPanels.cancelStartupCollapseAnimation() + ) { + return; + } + + const root = + document.querySelector("main"); + + if ( + !root + || !root.classList.contains(STARTUP_COLLAPSE_CLASS) + ) { + return; + } + + clearStartupFallbackCleanupTimer(); + + root.classList.remove( + STARTUP_COLLAPSE_CLASS + ); + + restoreStartupFallbackDuration(root); + + getPanels().forEach((panel) => { + panel.classList.remove("panel-collapsed"); + }); + syncSceneShadeToPanelCollapse(); + } + + function afterStartupCollapseArmed(callback) { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(callback); + }); + } + + function collapseStartupIdlePanels() { + startupAutoCollapseTimerId = null; + + if ( + startupAutoCollapseCancelled + || !isWindowActive() + || isAnyPanelHovered() + ) { + cancelStartupAutoCollapse(); + return; + } + + if ( + window.JinPanels + && typeof window.JinPanels.collapseAllPanels === "function" + ) { + window.JinPanels.collapseAllPanels({ + startup: true, + }); + return; + } + + const root = + document.querySelector("main"); + + if (root) { + const previousDuration = + root.style.getPropertyValue( + "--panel-collapse-duration" + ); + + const startupDuration = + getComputedStyle(root) + .getPropertyValue("--panel-startup-collapse-duration") + .trim() + || "5s"; + + root.style.setProperty( + "--panel-collapse-duration", + startupDuration + ); + + root.classList.add( + STARTUP_COLLAPSE_CLASS + ); + + root.getBoundingClientRect(); + + startupFallbackPreviousDuration = + previousDuration; + + clearStartupFallbackCleanupTimer(); + + startupFallbackCleanupTimerId = + window.setTimeout( + () => { + root.classList.remove( + STARTUP_COLLAPSE_CLASS + ); + + restoreStartupFallbackDuration(root); + startupFallbackCleanupTimerId = null; + }, + STARTUP_AUTO_COLLAPSE_MS + 80 + ); + + afterStartupCollapseArmed(() => { + if ( + startupAutoCollapseCancelled + || !root.classList.contains(STARTUP_COLLAPSE_CLASS) + ) { + return; + } + + getPanels().forEach((panel) => { + panel.classList.add("panel-collapsed"); + }); + syncSceneShadeToPanelCollapse(); + }); + return; + } + + getPanels().forEach((panel) => { + panel.classList.add("panel-collapsed"); + }); + syncSceneShadeToPanelCollapse(); + } + + function scheduleStartupAutoCollapse() { + if ( + startupAutoCollapseCancelled + || !isWindowActive() + || isAnyPanelHovered() + ) { + cancelStartupAutoCollapse(); + return; + } + + clearStartupAutoCollapseTimer(); + startupAutoCollapseTimerId = + window.setTimeout( + collapseStartupIdlePanels, + STARTUP_AUTO_COLLAPSE_MS + ); + } + + function bindStartupAutoCollapse() { + if (document.readyState === "complete") { + window.requestAnimationFrame( + scheduleStartupAutoCollapse + ); + return; + } + + window.addEventListener( + "load", + scheduleStartupAutoCollapse, + { once: true } + ); + } function bindPanelInactivity(panel) { let timerId = null; @@ -42,11 +303,13 @@ } function registerActivity() { + registerStartupPanelActivity(); wakePanel(); scheduleFade(); } panel.addEventListener("mouseenter", () => { + registerStartupPanelActivity(); hovered = true; clearTimer(); wakePanel(); @@ -81,11 +344,23 @@ scheduleFade(); } - PANEL_IDS.forEach((panelId) => { - const panel = document.getElementById(panelId); + getPanels().forEach((panel) => { + bindPanelInactivity(panel); + }); + + window.addEventListener( + "blur", + cancelStartupAutoCollapse + ); - if (panel) { - bindPanelInactivity(panel); + document.addEventListener( + "visibilitychange", + () => { + if (document.visibilityState !== "visible") { + cancelStartupAutoCollapse(); + } } - }); + ); + + bindStartupAutoCollapse(); })(); diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 8cdee9e0..62f0c936 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -194,7 +194,22 @@ return; } - storage.writeFactsMemory(record.signals, record.session_id); + const signals = + storage.writeFactsMemory(record.signals, record.session_id); + + const removedFullyAnalyzedSnapshot = + storage.clearFactsMemorySessionIfFullyAnalyzed + && storage.clearFactsMemorySessionIfFullyAnalyzed( + record.session_id, + signals + ); + + if ( + removedFullyAnalyzedSnapshot + && typeof window.refreshFactsMemoryAppendButtons === "function" + ) { + window.refreshFactsMemoryAppendButtons(); + } }); if ( diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index af4b2a3d..8215885e 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -11,6 +11,7 @@ let setActiveMemoryRecords = null; let deleteRuntimeMemoryLine = null; let getDelayedMemoryReports = null; + let setDelayedMemoryReportPinned = null; let getFactsMemoryFields = null; let deleteFactsMemoryField = null; let getLongTermMemoryFacts = null; @@ -34,6 +35,8 @@ let delayedMemoryModalPanel = null; let delayedMemoryModalTitle = null; let delayedMemoryModalContent = null; + let delayedMemoryModalPinButton = null; + let delayedMemoryModalReport = null; const runtimeDiffHistory = { diffs: [], @@ -142,7 +145,33 @@ ...report, }; }) - .filter(Boolean); + .filter(Boolean) + .sort((left, right) => { + const pinDelta = + Number(Boolean(right.pinned)) + - Number(Boolean(left.pinned)); + + if (pinDelta) { + return pinDelta; + } + + const leftDate = + Date.parse( + left.last_appended_date + || left.created_date + || left.created_time + || "" + ) || 0; + const rightDate = + Date.parse( + right.last_appended_date + || right.created_date + || right.created_time + || "" + ) || 0; + + return rightDate - leftDate; + }); } function setActiveMemoryRecordTexts(records) { @@ -1679,6 +1708,12 @@ row.className = "runtime-memory-line runtime-memory-delayed-row"; + if (Boolean(report.pinned)) { + row.classList.add( + "runtime-memory-delayed-row-pinned" + ); + } + row.setAttribute( "role", "button" @@ -1823,6 +1858,27 @@ return `${year}-${month}-${day} ${hours}:${minutes}, ${weekday}`; } + function updateDelayedMemoryModalPinState(report) { + if (!delayedMemoryModalPinButton) { + return; + } + + const pinned = + Boolean(report && report.pinned); + + delayedMemoryModalPinButton.classList.toggle( + "delayed-memory-modal-pin-active", + pinned + ); + delayedMemoryModalPinButton.setAttribute( + "aria-pressed", + pinned ? "true" : "false" + ); + delayedMemoryModalPinButton.title = + pinned ? "Unpin delayed memory" : "Pin delayed memory"; + } + + function closeDelayedMemoryReportModal() { if (!delayedMemoryModal) { return; @@ -1835,6 +1891,7 @@ delayedMemoryModal.classList.remove( "flex" ); + delayedMemoryModalReport = null; } function ensureDelayedMemoryModal() { @@ -1866,6 +1923,34 @@ delayedMemoryModalTitle.className = "min-w-0 truncate text-xs uppercase tracking-widest text-zinc-300"; + const headerActions = + document.createElement("div"); + + headerActions.className = + "delayed-memory-modal-actions"; + + delayedMemoryModalPinButton = + document.createElement("button"); + + delayedMemoryModalPinButton.type = + "button"; + + delayedMemoryModalPinButton.className = + "delayed-memory-modal-icon-button delayed-memory-modal-pin"; + + delayedMemoryModalPinButton.setAttribute( + "aria-label", + "Pin delayed memory" + ); + + delayedMemoryModalPinButton.setAttribute( + "aria-pressed", + "false" + ); + + delayedMemoryModalPinButton.innerHTML = + ''; + const closeButton = document.createElement("button"); @@ -1873,10 +1958,15 @@ "button"; closeButton.className = - "text-xs text-zinc-400 hover:text-zinc-100 transition"; + "delayed-memory-modal-icon-button delayed-memory-modal-close"; + + closeButton.setAttribute( + "aria-label", + "Close" + ); closeButton.textContent = - "close"; + "×"; delayedMemoryModalContent = document.createElement("div"); @@ -1888,10 +1978,18 @@ delayedMemoryModalTitle ); - header.appendChild( + headerActions.appendChild( + delayedMemoryModalPinButton + ); + + headerActions.appendChild( closeButton ); + header.appendChild( + headerActions + ); + delayedMemoryModalPanel.appendChild( header ); @@ -1908,6 +2006,38 @@ delayedMemoryModal ); + delayedMemoryModalPinButton.addEventListener( + "click", + () => { + if ( + !delayedMemoryModalReport + || typeof setDelayedMemoryReportPinned !== "function" + ) { + return; + } + + const nextPinned = + !Boolean(delayedMemoryModalReport.pinned); + const changed = + setDelayedMemoryReportPinned( + delayedMemoryModalReport._storage_key, + nextPinned + ); + + if (!changed) { + return; + } + + delayedMemoryModalReport = { + ...delayedMemoryModalReport, + pinned: nextPinned, + }; + updateDelayedMemoryModalPinState( + delayedMemoryModalReport + ); + } + ); + closeButton.addEventListener( "click", closeDelayedMemoryReportModal @@ -2035,6 +2165,7 @@ "created_session_id", "tags", "body", + "pinned", ]); Object.entries(report || {}).forEach(([key, value]) => { @@ -2065,6 +2196,12 @@ function openDelayedMemoryReportModal(report) { ensureDelayedMemoryModal(); + delayedMemoryModalReport = { + ...report, + }; + updateDelayedMemoryModalPinState( + delayedMemoryModalReport + ); delayedMemoryModalTitle.textContent = normalizeDelayedMemoryDisplayText(report.title) @@ -2617,6 +2754,7 @@ setActiveMemoryRecords = options.setActiveMemoryRecords || null; deleteRuntimeMemoryLine = options.deleteRuntimeMemoryLine || null; getDelayedMemoryReports = options.getDelayedMemoryReports || null; + setDelayedMemoryReportPinned = options.setDelayedMemoryReportPinned || null; getFactsMemoryFields = options.getFactsMemoryFields || null; deleteFactsMemoryField = options.deleteFactsMemoryField || null; getLongTermMemoryFacts = options.getLongTermMemoryFacts || null; diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 2afcee6e..78ce60fc 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -929,6 +929,65 @@ } + function clearFactsMemorySessionIfFullyAnalyzed( + sessionId, + value + ) { + + const normalizedSessionId = + String(sessionId || "").trim(); + + const currentSessionId = + String(getCurrentFactsMemorySessionId() || "").trim(); + + if ( + !normalizedSessionId + || normalizedSessionId === currentSessionId + ) { + return false; + } + + const key = + getFactsMemoryStorageKey( + normalizedSessionId + ); + + if (!key) { + return false; + } + + const signals = + value === undefined + ? readFactsMemory( + normalizedSessionId + ) + : normalizeFactsMemory( + value + ); + + const signalKeys = + Object.keys(signals); + + if ( + !signalKeys.length + || !signalKeys.every( + function (signalKey) { + return signals[signalKey].l4_status === "analyzed"; + } + ) + ) { + return false; + } + + removeBrowserMemory( + key + ); + + return true; + + } + + function readFactsMemory( sessionId = factsMemorySessionId ) { @@ -1048,6 +1107,41 @@ } + function normalizeLongTermFactIds( + value + ) { + + const source = + Array.isArray(value) + ? value + : [value]; + const seen = new Set(); + const factIds = []; + + source.forEach(function (item) { + String(item || "") + .split(/[\s,;]+/) + .forEach(function (candidate) { + const factId = + String(candidate || "").trim().toLowerCase(); + + if ( + !/^l4_[a-z0-9_-]+$/.test(factId) + || seen.has(factId) + ) { + return; + } + + seen.add(factId); + factIds.push(factId); + }); + }); + + return factIds; + + } + + function normalizeDelayedMemoryReports( value ) { @@ -1121,6 +1215,12 @@ .filter(Boolean), body: String(report.body || "").trim(), + pinned: + Boolean(report.pinned), + long_term_facts_ids: + normalizeLongTermFactIds( + report.long_term_facts_ids + ), created_session_id: String(report.created_session_id || "").trim(), created_time: @@ -1787,6 +1887,7 @@ canAppendFactsMemoryByStorageKey, appendFactsMemoryByStorageKey, clearFactsMemoryByStorageKey, + clearFactsMemorySessionIfFullyAnalyzed, readFactsMemory, writeFactsMemory, buildFactsMemoryContentHash, diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 3e18ae04..0b4d4e4e 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -566,6 +566,7 @@ memoryView.init({ setActiveMemoryRecords: writeActiveMemoryRecords, deleteRuntimeMemoryLine: deleteRuntimeMemoryLineAndRender, getDelayedMemoryReports: readDelayedMemoryReports, + setDelayedMemoryReportPinned, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, getLongTermMemoryFacts: () => ( @@ -783,6 +784,50 @@ function removeActiveMemoryRecordByIdAndRender( } +function setDelayedMemoryReportPinned( + reportId, + pinned +) { + + const normalizedId = + String(reportId || "").trim().toLowerCase(); + const reports = + readDelayedMemoryReports(); + const report = + reports[normalizedId]; + + if ( + !/^[a-z0-9]{6}$/.test(normalizedId) + || !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return false; + } + + reports[normalizedId] = { + ...report, + pinned: Boolean(pinned), + last_appended_date: + Boolean(pinned) + ? new Date().toISOString() + : String(report.last_appended_date || "").trim(), + }; + + writeDelayedMemoryReports( + reports + ); + renderRuntimeMemorySnapshot(); + + if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { + window.syncDelayedMemoryReportsToRuntime(); + } + + return true; + +} + + function appendDelayedMemoryReports( reports ) { @@ -1017,6 +1062,7 @@ window.JinRuntime.runtime = { appendActiveMemoryRecords: appendActiveMemoryRecordsAndRender, removeActiveMemoryRecordById: removeActiveMemoryRecordByIdAndRender, getDelayedMemoryReports: readDelayedMemoryReports, + setDelayedMemoryReportPinned, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, getLongTermMemoryFacts() { diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index 72eb179d..165d1d44 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -374,6 +374,10 @@ function syncDelayedMemoryReportsToRuntime() { +window.syncDelayedMemoryReportsToRuntime = + syncDelayedMemoryReportsToRuntime; + + function handleDelayedMemoryStoreSnapshot( data ) { @@ -395,8 +399,8 @@ function handleDelayedMemoryStoreSnapshot( window.JinRuntime.runtime.getDelayedMemoryReports(); window.JinRuntime.runtime.replaceDelayedMemoryReports({ - ...data.delayed_memory_reports, ...localReports, + ...data.delayed_memory_reports, }); } diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index c4067a75..bf85cf0f 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -552,6 +552,7 @@ const PAYLOAD_DISTINCT_RUNTIME_ACTIONS = new Set([ "save_delayed_memory_content", "append_delayed_memory", "remove_delayed_memory", + "update_delayed_memory", ]); function normalizeRuntimeActionPayloadIdentity(value) { @@ -688,6 +689,7 @@ function getDelayedMemoryRuntimeActionPreview( [ "append_delayed_memory", "remove_delayed_memory", + "update_delayed_memory", ].includes(action) ? data.payload : "" @@ -745,6 +747,7 @@ function handleRuntimeAction( [ "append_delayed_memory", "remove_delayed_memory", + "update_delayed_memory", ].includes(action) && Boolean( delayedMemoryPreview.reportId @@ -1140,6 +1143,21 @@ function handleRuntimeAction( ); } + if ( + action === "update_delayed_memory" + && data.delayed_memory_result + && data.delayed_memory_result.report + && data.delayed_memory_result.id + && window.JinRuntime + && window.JinRuntime.runtime + && window.JinRuntime.runtime.appendDelayedMemoryReports + ) { + window.JinRuntime.runtime.appendDelayedMemoryReports({ + [data.delayed_memory_result.id]: + data.delayed_memory_result.report, + }); + } + if ( action === "append_delayed_memory" && data.delayed_memory_result diff --git a/ui/static/js/win95-theme.js b/ui/static/js/win95-theme.js index 3f62ebeb..d8e818e8 100644 --- a/ui/static/js/win95-theme.js +++ b/ui/static/js/win95-theme.js @@ -6,6 +6,15 @@ function applyWin95Theme(enabled) { document.body.classList.toggle(themeClass, enabled); localStorage.setItem(themeKey, enabled ? "1" : "0"); + + if ( + window.JinPanels + && typeof window.JinPanels.refreshCollapsedPanelHeights === "function" + ) { + window.requestAnimationFrame( + window.JinPanels.refreshCollapsedPanelHeights + ); + } } applyWin95Theme(localStorage.getItem(themeKey) === "1"); diff --git a/ui/templates/index.html b/ui/templates/index.html index f29b146d..ef932f81 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,14 +10,14 @@ - + - + - - + + @@ -481,8 +481,8 @@ - - + + @@ -499,20 +499,20 @@ } | tojson }} - + - - + + - + @@ -524,8 +524,8 @@ - - + + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index dafa76d4..012e91e1 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -51,7 +51,12 @@ match_regexp, match_regexp_templates, ) -from .save_delayed_memory_utils import parse_delayed_memory_content_payload +from .save_delayed_memory_utils import ( + collect_long_term_fact_ids_from_reports, + normalize_long_term_fact_ids, + parse_delayed_memory_content_payload, +) +from .update_delayed_memory_utils import parse_update_delayed_memory_payload from .web_search_utils import extract_search_query __all__ = [ @@ -89,8 +94,11 @@ "normalize_jin_color_payload", "normalize_runtime_action_name", "normalize_runtime_action_names", + "collect_long_term_fact_ids_from_reports", + "normalize_long_term_fact_ids", "parse_delayed_memory_content_payload", "parse_idle_seconds", + "parse_update_delayed_memory_payload", "refresh_active_memory_runtime_metadata", "remove_active_memory_entries", "slugify_delayed_memory_title", diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 3ad2be60..4430ae1a 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -17,6 +17,7 @@ RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_REMOVE_SKILL, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, @@ -61,6 +62,7 @@ build_save_delayed_memory_payload, parse_delayed_memory_content_payload, ) +from .update_delayed_memory_utils import build_update_delayed_memory_payload from .web_search_utils import ( build_web_search_payload, extract_search_query, @@ -306,6 +308,7 @@ def normalize_runtime_action_name( "SAVE_DELAYED_MEMORY_CONTENT": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, "APPEND_DELAYED_MEMORY": RUNTIME_ACTION_APPEND_DELAYED_MEMORY, "REMOVE_DELAYED_MEMORY": RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + "UPDATE_DELAYED_MEMORY": RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, "SAVE_ACTIVE_MEMORY": RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, "RESOLVE_ACTIVE_MEMORY": RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, "USE_ASSETS": RUNTIME_ACTION_ASSET_ACTION, @@ -416,6 +419,7 @@ def normalize_runtime_action_names( RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: build_save_delayed_memory_payload, RUNTIME_ACTION_APPEND_DELAYED_MEMORY: build_append_delayed_memory_payload, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY: build_resolve_action_payload, + RUNTIME_ACTION_UPDATE_DELAYED_MEMORY: build_update_delayed_memory_payload, RUNTIME_ACTION_APPEND_SKILL: build_append_skill_payload, RUNTIME_ACTION_REMOVE_SKILL: build_resolve_action_payload, RUNTIME_ACTION_ASSET_ACTION: build_asset_action_payload, diff --git a/utils/actions/delayed_memory_actions.py b/utils/actions/delayed_memory_actions.py index 59ae7cae..93de367c 100644 --- a/utils/actions/delayed_memory_actions.py +++ b/utils/actions/delayed_memory_actions.py @@ -18,6 +18,7 @@ async def apply_delayed_memory_actions( *, append_delayed_memory_actions, remove_delayed_memory_actions, + update_delayed_memory_actions, log_runtime, ): from utils.brain_client_utils import ( @@ -30,6 +31,7 @@ async def apply_delayed_memory_actions( get_delayed_memory_reports, remove_delayed_memory_report, set_appended_delayed_memory_report, + update_delayed_memory_report, ) delayed_memory_results = [] @@ -71,6 +73,47 @@ async def apply_delayed_memory_actions( result ) + if update_delayed_memory_actions: + if log_runtime is not None: + await log_runtime( + "[RUNTIME ACTION] update_delayed_memory requested" + ) + + clear_delayed_memory_runtime_results( + context + ) + + for action in update_delayed_memory_actions: + result = update_delayed_memory_report( + context, + action.payload, + ) + append_delayed_memory_runtime_result( + context, + result, + ) + + if result.get("ok") is not False: + from runtime.L4_memory import ( + refresh_runtime_l4_archived_fact_ids, + ) + + refresh_runtime_l4_archived_fact_ids( + context + ) + history_text = build_delayed_memory_history_text( + result + ) + if history_text: + record_session_action_history( + context, + history_text, + ) + + delayed_memory_results.append( + result + ) + if remove_delayed_memory_actions: if log_runtime is not None: await log_runtime( @@ -316,6 +359,14 @@ async def apply_save_delayed_memory_actions( report ) + from runtime.L4_memory import ( + refresh_runtime_l4_archived_fact_ids, + ) + + refresh_runtime_l4_archived_fact_ids( + context + ) + file_errors = [] if bool( diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index e52a0a91..0fc9a6bb 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -10,6 +10,7 @@ RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, RUNTIME_ACTION_REMOVE_SKILL, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, @@ -1262,6 +1263,12 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_REMOVE_DELAYED_MEMORY ] + update_delayed_memory_actions = [ + action + for action in filtered_actions + if action.name == RUNTIME_ACTION_UPDATE_DELAYED_MEMORY + ] + list_skill_actions = [ action for action in filtered_actions @@ -1447,6 +1454,7 @@ def accept_runtime_action_once_per_message( context, append_delayed_memory_actions=append_delayed_memory_actions, remove_delayed_memory_actions=remove_delayed_memory_actions, + update_delayed_memory_actions=update_delayed_memory_actions, log_runtime=log_runtime, ) diff --git a/utils/actions/regexp_utils.py b/utils/actions/regexp_utils.py index d70768c3..8e353186 100644 --- a/utils/actions/regexp_utils.py +++ b/utils/actions/regexp_utils.py @@ -136,7 +136,9 @@ def compile_runtime_action_regexp( expression = ( r"<\s*(?P" + name - + r")\s*>" + + r")" + r"(?:\s*:\s*(?P[^>]*?))?" + r"\s*>" r"[^\S\r\n]*(?:\r?\n)?" r"(?P.*?)" r"(?:" @@ -191,7 +193,11 @@ def compile_runtime_action_start_regexp( return re.compile(r"(?!x)x") return re.compile( - r"<\s*(?P" + name + r")\s*>", + ( + r"<\s*(?P" + name + r")" + r"(?:\s*:\s*(?P[^>]*?))?" + r"\s*>" + ), re.IGNORECASE, ) @@ -233,13 +239,21 @@ def compile_runtime_action_tag_regexp( def _payload_from_match(match: re.Match[str]) -> str: groups = match.groupdict() - - return str( + attribute_payload = str( + groups.get("attribute_payload") + or "" + ).strip() + payload = str( groups.get("payload") - or groups.get("attribute_payload") or "" ).strip() + return "\n".join( + part + for part in (attribute_payload, payload) + if part + ) + def match_regexp( text: str, diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 56841b43..3c22bf04 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -4,11 +4,79 @@ from .delayed_memory_utils import generate_delayed_memory_report_id +LONG_TERM_FACT_ID_RE = re.compile( + r"^l4_[a-z0-9_-]+$", + re.IGNORECASE, +) + DELAYED_MEMORY_FIELD_RE = re.compile( - r"(?im)^[^\S\r\n]*(title|summary|tags|body)[^\S\r\n]*:[^\S\r\n]*(.*)$", + r"(?im)^[^\S\r\n]*(title|summary|tags|body|long_term_facts_ids)" + r"[^\S\r\n]*:[^\S\r\n]*(.*)$", ) +def normalize_long_term_fact_ids(value) -> list[str]: + + source = value if isinstance(value, list) else [value] + candidates = [] + + for item in source: + candidates.extend( + re.split( + r"[,;\s]+", + str(item or ""), + ) + ) + + fact_ids = [] + seen = set() + + for candidate in candidates: + fact_id = str( + candidate + or "" + ).strip().casefold() + + if ( + not fact_id + or fact_id in seen + or not LONG_TERM_FACT_ID_RE.fullmatch( + fact_id + ) + ): + continue + + seen.add(fact_id) + fact_ids.append(fact_id) + + return fact_ids + + +def collect_long_term_fact_ids_from_reports( + reports, +) -> set[str]: + + if not isinstance(reports, dict): + return set() + + fact_ids = set() + + for report in reports.values(): + if not isinstance(report, dict): + continue + + fact_ids.update( + normalize_long_term_fact_ids( + report.get( + "long_term_facts_ids", + [], + ) + ) + ) + + return fact_ids + + def parse_delayed_memory_content_payload( payload: str, *, @@ -70,6 +138,17 @@ def parse_delayed_memory_content_payload( ) if part ).strip() + elif field_name == "long_term_facts_ids": + value = normalize_long_term_fact_ids( + " ".join( + part + for part in ( + inline_value, + block_value, + ) + if part + ) + ) else: value = inline_value @@ -120,6 +199,13 @@ def parse_delayed_memory_content_payload( ) or "" ).strip(), + "pinned": False, + "long_term_facts_ids": normalize_long_term_fact_ids( + fields.get( + "long_term_facts_ids", + [], + ) + ), "created_session_id": str( created_session_id or "" diff --git a/utils/actions/update_delayed_memory_utils.py b/utils/actions/update_delayed_memory_utils.py new file mode 100644 index 00000000..262ce86c --- /dev/null +++ b/utils/actions/update_delayed_memory_utils.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import re + +from .delayed_memory_utils import is_delayed_memory_report_id +from .save_delayed_memory_utils import normalize_long_term_fact_ids + + +UPDATE_DELAYED_MEMORY_FIELD_RE = re.compile( + r"(?im)^[^\S\r\n]*(tags|body|long_term_facts_ids)" + r"[^\S\r\n]*:[^\S\r\n]*(.*)$", +) + + +def _normalize_tags(value) -> list[str]: + source = value if isinstance(value, list) else str(value or "").split(",") + tags = [] + seen = set() + + for item in source: + tag = str(item or "").strip() + normalized = tag.casefold() + + if not tag or normalized in seen: + continue + + seen.add(normalized) + tags.append(tag) + + return tags + + +def parse_update_delayed_memory_payload(payload: str) -> dict: + text = str(payload or "").replace("\r\n", "\n").strip() + + if not text: + return {} + + first_line, separator, remainder = text.partition("\n") + report_id = first_line.strip().casefold() + + if not is_delayed_memory_report_id(report_id): + return {} + + body_text = remainder if separator else "" + field_matches = list(UPDATE_DELAYED_MEMORY_FIELD_RE.finditer(body_text)) + + if not field_matches: + return {} + + fields = {} + + for index, match in enumerate(field_matches): + field_name = str(match.group(1) or "").casefold() + inline_value = str(match.group(2) or "").strip() + next_start = ( + field_matches[index + 1].start() + if index + 1 < len(field_matches) + else len(body_text) + ) + block_value = body_text[match.end():next_start].strip("\n") + combined = "\n".join( + part + for part in (inline_value, block_value) + if part + ).strip() + + if field_name == "tags": + fields[field_name] = _normalize_tags(combined) + elif field_name == "long_term_facts_ids": + fields[field_name] = normalize_long_term_fact_ids(combined) + else: + fields[field_name] = combined + + tags = fields.get("tags", []) + fact_ids = fields.get("long_term_facts_ids", []) + body = str(fields.get("body", "") or "").strip() + + if not tags and not fact_ids and not body: + return {} + + return { + "id": report_id, + "tags": tags, + "long_term_facts_ids": fact_ids, + "body": body, + } + + +def build_update_delayed_memory_payload( + query: str, + placeholder_payloads=(), +) -> str | None: + del placeholder_payloads + + text = str(query or "").strip() + + if not parse_update_delayed_memory_payload(text): + return None + + return text diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 6624ef78..e11e47fd 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -113,10 +113,14 @@ def get_brain_runtime_config(): parse_delayed_memory_content_payload, parse_idle_seconds, normalize_jin_color_payload, + normalize_long_term_fact_ids, refresh_active_memory_runtime_metadata, strip_active_memory_runtime_metadata, strip_active_memory_managed_suffixes, ) +from utils.actions.update_delayed_memory_utils import ( + parse_update_delayed_memory_payload, +) from utils.session_actions_history import ( build_active_memory_resolve_failed_history_text, build_asset_action_history_text, @@ -214,6 +218,39 @@ def build_action_missing_trigger_words_message( ) +def get_runtime_l4_fact_ids( + context, +) -> set[str]: + + store = getattr( + context, + "runtime_long_term_memory_store", + {}, + ) + + if not isinstance(store, dict): + return set() + + fact_ids = set() + + for fact in store.get("facts", []) or []: + if not isinstance(fact, dict): + continue + + fact_id = str( + fact.get( + "id", + "", + ) + or "" + ).strip().casefold() + + if fact_id: + fact_ids.add(fact_id) + + return fact_ids + + def build_delayed_memory_report( context, payload: str, @@ -293,8 +330,24 @@ def build_delayed_memory_report( report_id ) + requested_l4_fact_ids = normalize_long_term_fact_ids( + value.get( + "long_term_facts_ids", + [], + ) + ) + available_l4_fact_ids = get_runtime_l4_fact_ids( + context + ) + enriched_report[report_id] = { **value, + "long_term_facts_ids": [ + fact_id + for fact_id in requested_l4_fact_ids + if fact_id in available_l4_fact_ids + ], + "pinned": bool(value.get("pinned", False)), "created_session_id": ( str( value.get( @@ -1767,6 +1820,13 @@ def clear_appended_delayed_memory_report( ): return False + saved_report = get_delayed_memory_reports( + context + ).get(normalized_report_id) + + if isinstance(saved_report, dict) and bool(saved_report.get("pinned", False)): + return False + appended_reports = get_appended_delayed_memory_report( context ) @@ -1932,6 +1992,215 @@ def append_delayed_memory_report( } +def _append_unique_delayed_memory_values( + existing, + incoming, + *, + casefold: bool = False, +) -> list[str]: + + result = [] + seen = set() + + for value in (existing, incoming): + source = value if isinstance(value, list) else [] + + for item in source: + text = str(item or "").strip() + identity = text.casefold() if casefold else text + + if not text or identity in seen: + continue + + seen.add(identity) + result.append(text) + + return result + + +def update_delayed_memory_report( + context, + payload: str, +) -> dict: + + update = parse_update_delayed_memory_payload(payload) + report_id = str(update.get("id", "") or "").strip().casefold() + + if not report_id: + return build_delayed_memory_failure_result( + action="update_delayed_memory", + requested=payload, + error="invalid_delayed_memory_update", + ) + + reports = get_delayed_memory_reports(context) + report = reports.get(report_id) + + if not isinstance(report, dict): + return build_delayed_memory_failure_result( + action="update_delayed_memory", + requested=report_id, + error="delayed_memory_not_found", + ) + + available_l4_fact_ids = get_runtime_l4_fact_ids(context) + requested_l4_fact_ids = normalize_long_term_fact_ids( + update.get("long_term_facts_ids", []) + ) + accepted_l4_fact_ids = [ + fact_id + for fact_id in requested_l4_fact_ids + if fact_id in available_l4_fact_ids + ] + next_tags = _append_unique_delayed_memory_values( + report.get("tags", []), + update.get("tags", []), + casefold=True, + ) + next_fact_ids = normalize_long_term_fact_ids([ + *normalize_long_term_fact_ids( + report.get("long_term_facts_ids", []) + ), + *accepted_l4_fact_ids, + ]) + appended_body = str(update.get("body", "") or "").strip() + current_body = str(report.get("body", "") or "").rstrip() + next_body = current_body + + if appended_body: + next_body = "\n\n".join( + part + for part in (current_body, appended_body) + if part + ) + + if ( + next_tags == list(report.get("tags", []) or []) + and next_fact_ids == normalize_long_term_fact_ids( + report.get("long_term_facts_ids", []) + ) + and next_body == current_body + ): + return build_delayed_memory_failure_result( + action="update_delayed_memory", + requested=report_id, + error="delayed_memory_update_empty", + ) + + updated_report = { + **report, + "tags": next_tags, + "long_term_facts_ids": next_fact_ids, + "body": next_body, + } + reports[report_id] = updated_report + + appended_reports = get_appended_delayed_memory_report(context) + + if report_id in appended_reports: + appended_reports[report_id] = { + **updated_report, + "id": report_id, + } + + file_errors = [] + + if bool( + getattr( + context, + "delayed_memory_file_store_enabled", + False, + ) + ): + from utils.delayed_memory_file_store import ( + persist_delayed_memory_reports, + ) + + file_errors = persist_delayed_memory_reports({ + report_id: updated_report, + }) + + return { + "ok": True, + "action": "update_delayed_memory", + "id": report_id, + "title": str(updated_report.get("title", "") or "").strip(), + "report": { + **updated_report, + "id": report_id, + }, + "file_saved": not file_errors, + "file_errors": file_errors, + } + + +def include_pinned_delayed_memory_reports( + context, +) -> dict: + + reports = getattr( + context, + "delayed_memory_reports", + None, + ) + appended_reports = get_appended_delayed_memory_report(context) + + if not isinstance(reports, dict): + return appended_reports + turn_id = str( + getattr(context, "runtime_current_turn_id", "") + or getattr(context, "runtime_message_id", "") + or "" + ).strip() + touched_by_report = getattr( + context, + "runtime_pinned_delayed_memory_turns", + None, + ) + + if not isinstance(touched_by_report, dict): + touched_by_report = {} + context.runtime_pinned_delayed_memory_turns = touched_by_report + + reports_to_persist = {} + + for report_id, report in reports.items(): + if not isinstance(report, dict) or not bool(report.get("pinned", False)): + continue + + updated_report = report + + if turn_id and touched_by_report.get(report_id) != turn_id: + updated_report = update_delayed_memory_append_metadata( + context, + report, + ) + updated_report["pinned"] = True + reports[report_id] = updated_report + touched_by_report[report_id] = turn_id + reports_to_persist[report_id] = updated_report + + appended_reports[report_id] = { + **updated_report, + "id": report_id, + } + + if reports_to_persist and bool( + getattr( + context, + "delayed_memory_file_store_enabled", + False, + ) + ): + from utils.delayed_memory_file_store import ( + persist_delayed_memory_reports, + ) + + persist_delayed_memory_reports(reports_to_persist) + + return appended_reports + + def remove_delayed_memory_report( context, payload: str, @@ -1969,6 +2238,13 @@ def remove_delayed_memory_report( error="delayed_memory_not_found", ) + if bool(report.get("pinned", False)): + return build_delayed_memory_failure_result( + action="remove_delayed_memory", + requested=report_id, + error="delayed_memory_pinned", + ) + return { "ok": True, "action": "remove_delayed_memory", @@ -2051,11 +2327,28 @@ def build_delayed_memory_action_text( or "unknown" ).strip() + failed = result.get("ok") is False + if action == "append_delayed_memory": - return f"Appending: {title}" + return ( + f"Append failed: {title}" + if failed + else f"Appending: {title}" + ) if action == "remove_delayed_memory": - return f"Removing: {title}" + return ( + f"Remove failed: {title}" + if failed + else f"Removing: {title}" + ) + + if action == "update_delayed_memory": + return ( + f"Update failed: {title}" + if failed + else f"Updating: {title}" + ) return "Delayed memory updated" @@ -2133,6 +2426,9 @@ def build_delayed_memory_history_text( if action == "remove_delayed_memory": return f"Delayed memory removed from context: {title}" + if action == "update_delayed_memory": + return f"Delayed memory updated: {title}" + return "" diff --git a/utils/context/delayed_memory.py b/utils/context/delayed_memory.py index fc269423..31e9b96d 100644 --- a/utils/context/delayed_memory.py +++ b/utils/context/delayed_memory.py @@ -117,6 +117,21 @@ def format_delayed_memory_result_sections( ) continue + if action == "update_delayed_memory": + sections.append( + ( + "UPDATE_DELAYED_MEMORY", + ( + format_delayed_memory_failure_result + if result.get("ok") is False + else format_tool_result_payload + )( + result + ), + ) + ) + continue + if action == "save_delayed_memory_content": sections.append( ( diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index 50636eae..5bb89316 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -180,6 +180,7 @@ def _format_memory_action_context_part( if normalized_action in { "RESOLVE_ACTIVE_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", }: resolved_id = part_id or detail diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index 69ecaf5f..022ea3ee 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -9,6 +9,9 @@ from utils.actions.delayed_memory_utils import ( is_delayed_memory_report_id, ) +from utils.actions.save_delayed_memory_utils import ( + normalize_long_term_fact_ids, +) PROJECT_ROOT = Path(__file__).resolve().parents[1] @@ -149,6 +152,13 @@ def normalize_delayed_memory_report( report.get("body", ""), limit=MAX_DELAYED_MEMORY_BODY_CHARS, ), + "pinned": bool(report.get("pinned", False)), + "long_term_facts_ids": normalize_long_term_fact_ids( + report.get( + "long_term_facts_ids", + [], + ) + ), "created_session_id": _clean_text( report.get("created_session_id", "") or report.get("session", ""), @@ -230,12 +240,31 @@ def merge_delayed_memory_reports( primary_reports = normalize_delayed_memory_reports( primary, ) - - return { + reports = { **fallback_reports, **primary_reports, } + for report_id in set( + fallback_reports + ).intersection( + primary_reports + ): + reports[report_id][ + "long_term_facts_ids" + ] = normalize_long_term_fact_ids([ + *fallback_reports[report_id].get( + "long_term_facts_ids", + [], + ), + *primary_reports[report_id].get( + "long_term_facts_ids", + [], + ), + ]) + + return reports + def delayed_memory_filename( report_id: str, @@ -292,6 +321,10 @@ def build_delayed_memory_file_payload( "all_appended_session_ids" ], "body": clean_report["body"], + "pinned": clean_report["pinned"], + "long_term_facts_ids": clean_report[ + "long_term_facts_ids" + ], "appended_times": clean_report["appended_times"], "append_streak": clean_report["append_streak"], "last_appended_date": clean_report[ diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index e1c928e7..81a9768e 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1050,6 +1050,9 @@ def _build_session_action_marker_detail( }: return normalized_payload + if normalized_name == "UPDATE_DELAYED_MEMORY": + return normalized_payload.splitlines()[0].strip() + if not normalized_name.startswith( "SAVE_" ): @@ -1076,6 +1079,7 @@ def _build_session_action_marker_detail( "SAVE_DELAYED_MEMORY_CONTENT", "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", } @@ -1208,6 +1212,7 @@ def _build_payload_distinct_session_action_parts( action_name in { "APPEND_DELAYED_MEMORY", "REMOVE_DELAYED_MEMORY", + "UPDATE_DELAYED_MEMORY", } and payload_key and payload_key != part.get( diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 7e17c789..d7e211ae 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -136,6 +136,14 @@ def apply_delayed_memory_reports( existing_reports, ) + from runtime.L4_memory import ( + refresh_runtime_l4_archived_fact_ids, + ) + + refresh_runtime_l4_archived_fact_ids( + context + ) + def hydrate_delayed_memory_reports_from_files( context, @@ -157,6 +165,14 @@ def hydrate_delayed_memory_reports_from_files( file_reports, ) + from runtime.L4_memory import ( + refresh_runtime_l4_archived_fact_ids, + ) + + refresh_runtime_l4_archived_fact_ids( + context + ) + if warnings: current_warnings = getattr( context, From 4ff680d534e571f0869d7a9026895c50a81acba1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:17:43 +0300 Subject: [PATCH 006/168] chore: cleanup README.md - add default manual start commands - clean up configuration table --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 694eabe5..933df577 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,8 @@ It does not download models automatically and does not replace model IDs or prov ### Manual Start ```bash +git clone https://github.com +cd jin_core python -m venv .venv ``` @@ -245,17 +247,17 @@ Copy `config.example.py` to `config.py` and set the provider URLs and model IDs. Main options: -| Option | Purpose | -| --- | --- | -| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | -| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Brain provider and model. | -| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Service provider and model. | -| `TRANSLATION_ENABLED` | Enable the translator path. | -| `*_CONTEXT_WINDOW`, `*_MAX_TOKENS` | Local fallbacks for context and output limits. | -| `BRAIN_IMAGE_INPUT_ENABLED`, `SERVICE_IMAGE_INPUT_ENABLED` | Send image attachments to roles that support OpenAI-compatible image input. | -| `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable long-term memory consolidation and set the idle delay. | -| `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | -| `BRAIN_MAX_FOLLOWUPS` | Limit internal action and follow-up steps for one user turn. | +| Option | Purpose | +|------------------------------------------------------------|-----------------------------------------------------------------------------| +| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | +| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Brain provider and model. | +| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Service provider and model. | +| `TRANSLATION_ENABLED` | Enable the translator path. | +| `BRAIN_CONTEXT_WINDOW`, `BRAIN_MAX_TOKENS` | Local fallbacks for context and output limits. | +| `BRAIN_IMAGE_INPUT_ENABLED` | Send image attachments to roles that support OpenAI-compatible image input. | +| `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable long-term memory consolidation and set the idle delay. | +| `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | +| `BRAIN_MAX_FOLLOWUPS` | Limit internal action and follow-up steps for one user turn. | Every uppercase option can also be set through environment variables. Plain names and `JIN_`-prefixed names are supported; plain names take priority. From f9979afa645724a3f720ab91bdf637bfbb909f81 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:42:25 +0300 Subject: [PATCH 007/168] chore: cleanup ui - update ui with close and attach buttons - update main screenshot --- ui/static/css/runtime-memory.css | 10 ---------- ui/static/images/jin-core-default-theme.jpg | Bin 176160 -> 495933 bytes ui/static/js/chat-attachments.js | 2 +- ui/static/js/logger/session-actions.js | 2 +- ui/static/js/logger/trace-modal.js | 2 +- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index b7e181f3..6591615c 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -283,13 +283,6 @@ ); } -.runtime-memory-delayed-row-pinned { - background: rgba(244, 244, 245, 0.035); - box-shadow: - inset 0 0 0 1px rgba(228, 228, 231, 0.08), - 0 0 12px rgba(228, 228, 231, 0.08); -} - .runtime-memory-delayed-row-pinned .runtime-memory-key, .runtime-memory-delayed-row-pinned .runtime-memory-value { color: rgba(244, 244, 245, 0.82); @@ -314,7 +307,6 @@ color: rgba(161, 161, 170, 0.9); transition: color 0.18s ease, - background-color 0.18s ease, text-shadow 0.18s ease; } @@ -322,7 +314,6 @@ .delayed-memory-modal-icon-button:focus-visible { outline: none; color: rgba(244, 244, 245, 0.98); - background: rgba(244, 244, 245, 0.05); } .delayed-memory-modal-pin svg { @@ -334,7 +325,6 @@ .delayed-memory-modal-pin-active { color: rgba(240, 253, 250, 0.98); - background: rgba(240, 253, 250, 0.07); } .delayed-memory-modal-pin-active svg { diff --git a/ui/static/images/jin-core-default-theme.jpg b/ui/static/images/jin-core-default-theme.jpg index 488139560c46358f7fb84a5d8f5c05f2eb4b01b9..3a037beebda5c1515e2fcdae2265447443050186 100644 GIT binary patch literal 495933 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±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

      x*h8rKKV7tNOPg8q#ohcxBcov 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

      WLAM`MKa8EY_K{sm5B zL4Ts{Rp}@eGc}y=oOmB8eVj}+6-UjdS)0Fh#S~w3spU8r2MSv-(BQ=7{8F=3Jv`w@ z8h2px6+}Cg^;LVa_~?bx=qepoqP>X z6>3nvQY2&@WovX*^wy}g2nwt(Hy`T=1S0x68`_X}^*l+~5fQpJ6v%wlf`9hp6Mm(C zIt$o0rOL>*Kq55z^1cLk;BOxR1dHPq9qW&-M~Mc7Cel16ReY#6!B7IVdGB@iHR1sqYF=}!p-W+ z$ew}AH6l|YfjP2SU7q?XdEg8nvwaBoD&9(CsFLrH>P&cIbg)pVNOkCaYb3 z0=t4|f%H~C8dw!#RR0a0`U-(4gf;#y4sZMM`zo)VruvOYIY|FD_;OF!yg1ehI$7Gj zf;KvA@P5psj0Fc*4~I(jtO*g7mJ1qHuh;m6@)kJR$V&wr)O?z~6N( zW8T>1jF8X|1G_Za-kL}T_m99$Q@>IaO5W9)y0dJKPHNWNQ3QYz7?sqbIV%0O>=~;R z2nZc~kd;-;i77hk<%`D=WCw($2zBNxAPi}{n1fqXJtLU4jsl_|JBC?BS-p48*UH8y zX3Jx-qGp<-9+fU|VV_w_VyYvs+#<*B_s6<)d@nI-sf%=|<9JTh;Uic_(8wIr0 zJ4D`MOOq&f{s>|(q&uTonbm$un!l?5caP0Px!j)XGB$?u5vTbsyI((S-XZ*J}?N0%*oP*-)$q9e1@FJP`?l-&J4X zfP|*l=&2&l3O=ua`R?nl8!i1F++dP-gJ|@^u+~tu&$(RXXR3lVAsc_=Dq;}GSh%Jv z7_IEJ+x5X!AZ$&3TVV8`gf=EvAa3v?TJz_%{K7Qm9|!_qUsAB$&!Fy8(5MO7bVrFs zV+dwAkR+5%ufKuo;dGey7@MR;u6>|R+1b&dV8SeJI+tRB{J z@ZP(a2Jb=qB7*4TV-IKtdpjDQK|3<|(BFomd#(HsIBWV#YO`C1p&9@zQu{(#(&TU& zv44q=)`~kaNcYa-(jXk51jgHHd$UKY^1>LTS?`T=%JnceP{dSf4y$7J>Bg0FxRY0bcywm3lvsr&a1ZvY zMfd`3DBGd)mkmg;|5!8xP+csauznf2SHKA!R$s#y=WQet`NE)6zKGBKJy$+{hxUjN zg>ou5bG?R_w47mvHKCiBL&kLwd`>V395$-~4R!R6z#-}cau-D&fuHEPE7U6eF^ixM z<{GiRx9a$kJ7^DQFRx!Yggg1&UvTyJk>XsvXpc7@IWVEC@KL(Ie6w=FJ8a1fsieH@ z%UKlVf=I?OP~?;2y}Dv`HO|H4>3Hm{9<2

      HX@CVV8I=$4P}#j1|p4# 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

      SQ(dws(TxndQ(-`=`y$ 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 diff --git a/ui/static/js/chat-attachments.js b/ui/static/js/chat-attachments.js index 25db53cf..8ceeac14 100644 --- a/ui/static/js/chat-attachments.js +++ b/ui/static/js/chat-attachments.js @@ -322,7 +322,7 @@ function ensureJinAttachmentModal() { closeButton.type = "button"; closeButton.className = - "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded p-0 text-[18px] leading-none text-zinc-300 transition hover:bg-zinc-800/60 hover:text-red-200 focus:outline-none"; + "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded p-0 text-[18px] leading-none text-zinc-300 transition hover:text-red-200 focus:outline-none"; closeButton.setAttribute( "aria-label", "Close" diff --git a/ui/static/js/logger/session-actions.js b/ui/static/js/logger/session-actions.js index a4aadc94..0ce1f63b 100644 --- a/ui/static/js/logger/session-actions.js +++ b/ui/static/js/logger/session-actions.js @@ -577,7 +577,7 @@ function ensureSessionActionsModal() { "button"; closeButton.className = - "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:bg-zinc-800/60 hover:text-zinc-100 focus:outline-none"; + "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:text-zinc-100 focus:outline-none"; closeButton.setAttribute( "aria-label", diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index c5118997..43c3bbf3 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -47,7 +47,7 @@ function ensureTraceModal() { "button"; closeButton.className = - "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:bg-zinc-800/60 hover:text-zinc-100 focus:outline-none"; + "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:text-zinc-100 focus:outline-none"; closeButton.setAttribute( "aria-label", From 2d0619afa877626312d89d6a52bbc60cefd86d9a Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:57:56 +0300 Subject: [PATCH 008/168] feature: L4 memory layer - persist L4 facts snapshot to memory/facts - merge browser L4 stores into a single source of truth - prevent empty/private-window snapshots from overwriting facts - preserve merged fact provenance with source_fact_ids --- runtime/L4_memory.py | 208 +++++++++++++++-- runtime/L4_memory_utils.py | 246 ++++++++++++++++++++ tests/test_l4_memory.py | 152 ++++++++++++ tests/test_l4_memory_client_contract.py | 50 ++++ ui/static/js/runtime/runtime-l4-memory.js | 21 ++ ui/static/js/runtime/runtime-memory-view.js | 1 + utils/long_term_facts_file_store.py | 123 ++++++++++ websocket/bootstrap.py | 11 + 8 files changed, 794 insertions(+), 18 deletions(-) create mode 100644 tests/test_l4_memory_client_contract.py create mode 100644 utils/long_term_facts_file_store.py diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index ec30a872..0dd79fba 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -19,6 +19,7 @@ extract_l4_json_payload, format_long_term_memory_context, mark_facts_memory_fields_analyzed, + merge_l4_store_snapshots, normalize_facts_memory_records, normalize_l4_candidates, normalize_l4_merge_operations, @@ -27,6 +28,10 @@ from utils.actions.save_delayed_memory_utils import ( collect_long_term_fact_ids_from_reports, ) +from utils.long_term_facts_file_store import ( + load_long_term_facts_store, + persist_long_term_facts_store, +) from runtime.memory_common import ( build_memory_failure_details, build_runtime_summarizer_payload, @@ -55,6 +60,115 @@ def get_l4_idle_seconds() -> int: ) +def runtime_l4_file_store_enabled(context) -> bool: + + explicit = getattr( + context, + "runtime_l4_file_store_enabled", + None, + ) + if explicit is not None: + return bool( + explicit + ) + + return getattr( + context, + "websocket", + None, + ) is not None + + +def get_runtime_l4_file_store_root(context): + + return getattr( + context, + "runtime_l4_file_store_root", + None, + ) + + +def load_runtime_l4_file_store(context) -> dict: + + root = get_runtime_l4_file_store_root( + context, + ) + + if root is None: + store, _warnings = load_long_term_facts_store() + else: + store, _warnings = load_long_term_facts_store( + root=root, + ) + + return store + + +def persist_runtime_l4_file_store(context, store) -> None: + + if not runtime_l4_file_store_enabled( + context, + ): + return + + root = get_runtime_l4_file_store_root( + context, + ) + + if root is None: + persist_long_term_facts_store( + store, + ) + return + + persist_long_term_facts_store( + store, + root=root, + ) + + +def merge_with_runtime_l4_file_store(context, store) -> tuple[dict, bool]: + + if not runtime_l4_file_store_enabled( + context, + ): + return normalize_l4_store( + store, + ), False + + file_store = load_runtime_l4_file_store( + context, + ) + merged, change = merge_l4_store_snapshots( + file_store, + store, + ) + + if change.get( + "changed", + ): + persist_runtime_l4_file_store( + context, + merged, + ) + + return merged, bool( + change.get( + "changed", + ) + ) + + +def set_runtime_l4_state(context, store) -> dict: + + merged, _changed = merge_with_runtime_l4_file_store( + context, + store, + ) + context.runtime_long_term_memory_store = merged + return merged + + def refresh_runtime_l4_archived_fact_ids( context, ) -> set[str]: @@ -76,9 +190,13 @@ def ensure_runtime_l4_state(context) -> dict: context.runtime_facts_memory_records = normalize_facts_memory_records( getattr(context, "runtime_facts_memory_records", []) ) - context.runtime_long_term_memory_store = normalize_l4_store( + normalized_store = normalize_l4_store( getattr(context, "runtime_long_term_memory_store", None) ) + context.runtime_long_term_memory_store = set_runtime_l4_state( + context, + normalized_store, + ) return context.runtime_long_term_memory_store @@ -95,17 +213,24 @@ def apply_facts_memory_store_sync(context, raw_records) -> dict: def apply_l4_memory_store_sync(context, raw_store) -> bool: incoming = normalize_l4_store(raw_store) current = ensure_runtime_l4_state(context) - incoming_revision = int(incoming.get("revision") or 0) - current_revision = int(current.get("revision") or 0) - current_has_data = bool(current.get("facts") or current.get("pending_facts")) + merged, change = merge_l4_store_snapshots( + current, + incoming, + ) + changed = bool( + change.get( + "changed", + ) + ) + context.runtime_long_term_memory_store = merged - if incoming_revision < current_revision: - return False - if current_has_data and incoming_revision == current_revision and incoming != current: - return False + if changed: + persist_runtime_l4_file_store( + context, + merged, + ) - context.runtime_long_term_memory_store = incoming - return True + return changed async def emit_facts_memory_store_update(context) -> None: @@ -222,6 +347,10 @@ async def run_l4_extraction_phase( candidates, ) context.runtime_long_term_memory_store = store + persist_runtime_l4_file_store( + context, + store, + ) records, records_changed = mark_facts_memory_fields_analyzed( getattr(context, "runtime_facts_memory_records", []), @@ -284,6 +413,10 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: } context.runtime_long_term_memory_store = next_store + persist_runtime_l4_file_store( + context, + next_store, + ) await emit_l4_memory_update(context, change=merge_change) return { @@ -384,6 +517,45 @@ def schedule_l4_memory_idle_update( return task +def l4_fact_matches_archived_ids( + fact: dict, + archived_fact_ids: set[str], +) -> bool: + + if not archived_fact_ids: + return False + + candidate_ids = [ + fact.get( + "id", + "", + ), + *( + fact.get( + "source_fact_ids", + [], + ) + if isinstance( + fact.get( + "source_fact_ids", + [], + ), + list, + ) + else [] + ), + ] + + return any( + str( + candidate_id + or "" + ).strip().casefold() + in archived_fact_ids + for candidate_id in candidate_ids + ) + + def build_runtime_l4_memory_context(*, context) -> str: if not l4_memory_enabled(): return "" @@ -395,14 +567,10 @@ def build_runtime_l4_memory_context(*, context) -> str: active_facts = [ fact for fact in store.get("facts") or [] - if str( - fact.get( - "id", - "", - ) - or "" - ).strip().casefold() - not in archived_fact_ids + if not l4_fact_matches_archived_ids( + fact, + archived_fact_ids, + ) ] return format_long_term_memory_context( active_facts @@ -418,6 +586,10 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: return False context.runtime_long_term_memory_store = store + persist_runtime_l4_file_store( + context, + store, + ) await log_memory_event( context, level=L4_LOG_LEVEL, diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 8c7e50f4..e8db325e 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -198,6 +198,9 @@ def normalize_l4_fact( "source_keys": normalize_l4_string_list( value.get("source_keys") or value.get("source_key") ), + "source_fact_ids": normalize_l4_string_list( + value.get("source_fact_ids") or value.get("source_fact_id") + ), } @@ -223,6 +226,10 @@ def merge_same_l4_fact(existing: dict, incoming: dict, *, now: str) -> dict: existing.get("source_keys"), incoming.get("source_keys"), ) + result["source_fact_ids"] = merge_l4_string_lists( + existing.get("source_fact_ids"), + incoming.get("source_fact_ids"), + ) result["updated_at"] = now return result @@ -251,6 +258,244 @@ def deduplicate_l4_facts(facts: list[dict], *, pending: bool, now: str) -> list[ return result +def merge_l4_snapshot_fact( + existing: dict, + incoming: dict, + *, + pending: bool, + now: str, +) -> dict: + + existing_fact = normalize_l4_fact( + existing, + pending=pending, + now=now, + ) + incoming_fact = normalize_l4_fact( + incoming, + pending=pending, + now=now, + ) + + if existing_fact is None: + return incoming_fact or {} + if incoming_fact is None: + return existing_fact + + existing_updated_at = normalize_l4_text( + existing_fact.get("updated_at") + ) + incoming_updated_at = normalize_l4_text( + incoming_fact.get("updated_at") + ) + prefer_incoming = incoming_updated_at > existing_updated_at + base = incoming_fact if prefer_incoming else existing_fact + existing_id = normalize_l4_text( + existing_fact.get("id") + ) + incoming_id = normalize_l4_text( + incoming_fact.get("id") + ) + + merged = { + **base, + "id": existing_id or incoming_id, + "confidence": max( + clamp_float(existing_fact.get("confidence"), default=1.0), + clamp_float(incoming_fact.get("confidence"), default=1.0), + ), + "mention_count": max( + max(1, int(existing_fact.get("mention_count") or 1)), + max(1, int(incoming_fact.get("mention_count") or 1)), + ), + "created_at": ( + existing_fact.get("created_at") + or incoming_fact.get("created_at") + or now + ), + "updated_at": ( + max( + existing_updated_at, + incoming_updated_at, + ) + or now + ), + "source_session_ids": merge_l4_string_lists( + existing_fact.get("source_session_ids"), + incoming_fact.get("source_session_ids"), + ), + "source_runtime_snapshot_ids": merge_l4_string_lists( + existing_fact.get("source_runtime_snapshot_ids"), + incoming_fact.get("source_runtime_snapshot_ids"), + ), + "source_keys": merge_l4_string_lists( + existing_fact.get("source_keys"), + incoming_fact.get("source_keys"), + ), + "source_fact_ids": merge_l4_string_lists( + existing_fact.get("source_fact_ids"), + incoming_fact.get("source_fact_ids"), + [incoming_id] if incoming_id and incoming_id != existing_id else [], + ), + } + + normalized = normalize_l4_fact( + merged, + pending=pending, + now=now, + ) + return normalized or existing_fact + + +def merge_l4_snapshot_fact_lists( + existing_facts, + incoming_facts, + *, + pending: bool, + now: str, +) -> tuple[list[dict], bool]: + + normalized_existing = deduplicate_l4_facts( + existing_facts if isinstance(existing_facts, list) else [], + pending=pending, + now=now, + ) + original = deepcopy( + normalized_existing + ) + + facts = [] + for fact in normalized_existing: + existing_index = next( + ( + index + for index, existing_fact in enumerate(facts) + if existing_fact.get("key") == fact.get("key") + ), + None, + ) + if existing_index is None: + facts.append( + dict(fact) + ) + continue + + facts[existing_index] = merge_l4_snapshot_fact( + facts[existing_index], + fact, + pending=pending, + now=now, + ) + + by_id = { + fact["id"]: index + for index, fact in enumerate(facts) + } + by_key = { + fact["key"]: index + for index, fact in enumerate(facts) + } + + incoming = deduplicate_l4_facts( + incoming_facts if isinstance(incoming_facts, list) else [], + pending=pending, + now=now, + ) + + for fact in incoming: + existing_index = by_id.get( + fact["id"] + ) + if existing_index is None: + existing_index = by_key.get( + fact["key"] + ) + + if existing_index is None: + by_id[fact["id"]] = len(facts) + by_key[fact["key"]] = len(facts) + facts.append( + fact + ) + continue + + merged = merge_l4_snapshot_fact( + facts[existing_index], + fact, + pending=pending, + now=now, + ) + previous_id = facts[existing_index]["id"] + previous_key = facts[existing_index]["key"] + facts[existing_index] = merged + by_id.pop( + previous_id, + None, + ) + by_key.pop( + previous_key, + None, + ) + by_id[merged["id"]] = existing_index + by_key[merged["key"]] = existing_index + + return facts, facts != original + + +def merge_l4_store_snapshots( + primary_store, + incoming_store, + *, + now: str | None = None, +) -> tuple[dict, dict]: + + current_time = now or utc_now_iso() + primary = normalize_l4_store( + primary_store, + now=current_time, + ) + incoming = normalize_l4_store( + incoming_store, + now=current_time, + ) + + facts, facts_changed = merge_l4_snapshot_fact_lists( + primary.get("facts"), + incoming.get("facts"), + pending=False, + now=current_time, + ) + pending_facts, pending_changed = merge_l4_snapshot_fact_lists( + primary.get("pending_facts"), + incoming.get("pending_facts"), + pending=True, + now=current_time, + ) + changed = bool( + facts_changed + or pending_changed + ) + + merged = { + **primary, + "facts": facts, + "pending_facts": pending_facts, + } + + if changed: + merged["revision"] = max( + int(primary.get("revision") or 0), + int(incoming.get("revision") or 0), + ) + 1 + merged["updated_at"] = current_time + + return merged, { + "changed": changed, + "facts_count": len(facts), + "pending_count": len(pending_facts), + } + + def empty_l4_store(*, now: str | None = None) -> dict: return { "version": L4_STORE_VERSION, @@ -797,6 +1042,7 @@ def format_l4_fact_metadata_suffixes(fact: dict) -> list[str]: "source_session_ids", "source_runtime_snapshot_ids", "source_keys", + "source_fact_ids", "created_at", "updated_at", ): diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 0f3189e9..4286f670 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1,8 +1,11 @@ import json +import tempfile import unittest from runtime.L4_memory import ( + apply_l4_memory_store_sync, build_runtime_l4_memory_context, + ensure_runtime_l4_state, maybe_update_runtime_l4_memory, ) from runtime.L4_memory_utils import ( @@ -14,6 +17,7 @@ format_l4_fact_line, format_long_term_memory_context, mark_facts_memory_fields_analyzed, + merge_l4_store_snapshots, normalize_facts_memory_records, normalize_l4_candidates, normalize_l4_merge_operations, @@ -26,6 +30,10 @@ from utils.actions.delayed_memory_actions import ( apply_save_delayed_memory_actions, ) +from utils.long_term_facts_file_store import ( + load_long_term_facts_store, + persist_long_term_facts_store, +) class FakeEmitter: @@ -130,6 +138,99 @@ def test_candidates_include_only_metadata_from_referenced_sources(self): ) self.assertEqual(candidates[0]["source_keys"], ["gpu"]) + def test_file_store_fallback_survives_empty_browser_sync(self): + with tempfile.TemporaryDirectory() as directory: + persisted_store = normalize_l4_store({ + "revision": 4, + "facts": [ + { + "id": "l4_saved", + "key": "user.identity", + "value": "Sergey", + "source_session_ids": ["session-normal"], + }, + ], + }) + persist_long_term_facts_store( + persisted_store, + root=directory, + ) + + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=None, + clients={}, + ) + context.runtime_l4_file_store_enabled = True + context.runtime_l4_file_store_root = directory + + loaded = ensure_runtime_l4_state( + context, + ) + self.assertEqual(len(loaded["facts"]), 1) + + changed = apply_l4_memory_store_sync( + context, + { + "revision": 99, + "facts": [], + "pending_facts": [], + }, + ) + stored, warnings = load_long_term_facts_store( + root=directory, + ) + + self.assertFalse(changed) + self.assertEqual(warnings, []) + self.assertEqual(len(stored["facts"]), 1) + self.assertEqual(stored["facts"][0]["key"], "user.identity") + + def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): + merged, change = merge_l4_store_snapshots( + { + "revision": 4, + "facts": [ + { + "id": "l4_existing", + "key": "relationship.association", + "value": "Anya is associated with known cohabitation data.", + "updated_at": "2026-08-03T18:15:34Z", + "source_session_ids": ["session-a"], + }, + ], + }, + { + "revision": 4, + "facts": [ + { + "id": "l4_incoming", + "key": "relationship.association", + "value": "Anya is associated with newer relationship context.", + "updated_at": "2026-08-03T18:20:00Z", + "source_session_ids": ["session-b"], + }, + ], + }, + now="2026-08-03T18:21:00Z", + ) + + self.assertTrue(change["changed"]) + self.assertEqual(len(merged["facts"]), 1) + self.assertEqual( + merged["facts"][0]["value"], + "Anya is associated with newer relationship context.", + ) + self.assertEqual( + merged["facts"][0]["source_session_ids"], + ["session-a", "session-b"], + ) + self.assertEqual( + merged["facts"][0]["source_fact_ids"], + ["l4_incoming"], + ) + def test_pending_candidates_accumulate_without_touching_final_memory(self): store = normalize_l4_store({ "facts": [ @@ -350,6 +451,57 @@ def test_delayed_report_fact_ids_hide_only_from_brain_context(self): 2, ) + def test_delayed_report_fact_ids_hide_merged_source_fact_ids_only_from_context(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "l4_current", + "key": "relationship.association", + "value": "Anya is associated with known cohabitation data.", + "source_fact_ids": [ + "l4_archived_source", + ], + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social context", + "long_term_facts_ids": [ + "l4_archived_source", + ], + }, + } + + self.assertEqual( + build_runtime_l4_memory_context( + context=context, + ), + "", + ) + self.assertEqual( + len( + context.runtime_long_term_memory_store[ + "facts" + ] + ), + 1, + ) + self.assertEqual( + context.runtime_long_term_memory_store[ + "facts" + ][0][ + "id" + ], + "l4_current", + ) + async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): context = RuntimeContext( diff --git a/tests/test_l4_memory_client_contract.py b/tests/test_l4_memory_client_contract.py new file mode 100644 index 00000000..3ba1f0f7 --- /dev/null +++ b/tests/test_l4_memory_client_contract.py @@ -0,0 +1,50 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class L4MemoryClientContractTests(unittest.TestCase): + + def test_long_term_memory_keeps_shared_local_storage_key(self): + source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-l4-memory.js" + ).read_text(encoding="utf-8") + + self.assertIn( + 'const longTermFactsStorageKey = "jin.longTermFacts.v1";', + source, + ) + + def test_equal_revision_update_cannot_replace_larger_local_snapshot(self): + source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-l4-memory.js" + ).read_text(encoding="utf-8") + + self.assertIn( + "function countStoreItems(store)", + source, + ) + self.assertIn( + "incoming.revision === local.revision", + source, + ) + self.assertIn( + "countStoreItems(local) > countStoreItems(incoming)", + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 62f0c936..d79222fc 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -83,6 +83,9 @@ value.source_runtime_snapshot_ids || value.source_runtime_snapshot_id ), source_keys: normalizeList(value.source_keys), + source_fact_ids: normalizeList( + value.source_fact_ids || value.source_fact_id + ), }; } @@ -131,6 +134,17 @@ ); } + function countStoreItems(store) { + return ( + (Array.isArray(store && store.facts) ? store.facts.length : 0) + + ( + Array.isArray(store && store.pending_facts) + ? store.pending_facts.length + : 0 + ) + ); + } + function writeStore(store) { const normalized = normalizeStore(store); storage.writeBrowserMemory(longTermFactsStorageKey, normalized); @@ -228,6 +242,13 @@ return local; } + if ( + incoming.revision === local.revision + && countStoreItems(local) > countStoreItems(incoming) + ) { + return local; + } + const store = writeStore(incoming); if ( window.JinRuntime.runtime diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 8215885e..0a7ede4c 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -2363,6 +2363,7 @@ "source_session_ids", "source_runtime_snapshot_ids", "source_keys", + "source_fact_ids", "created_at", "updated_at", ].forEach((key) => { diff --git a/utils/long_term_facts_file_store.py b/utils/long_term_facts_file_store.py new file mode 100644 index 00000000..f40d9f6d --- /dev/null +++ b/utils/long_term_facts_file_store.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from tempfile import NamedTemporaryFile + +from runtime.L4_memory_utils import ( + normalize_l4_store, +) + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +LONG_TERM_FACTS_ROOT = PROJECT_ROOT / "memory" / "facts" +LONG_TERM_FACTS_FILENAME = "long_term_facts.json" + + +def get_long_term_facts_path( + *, + root: Path | str = LONG_TERM_FACTS_ROOT, +) -> Path: + + return Path(root) / LONG_TERM_FACTS_FILENAME + + +def load_long_term_facts_store( + *, + root: Path | str = LONG_TERM_FACTS_ROOT, +) -> tuple[dict, list[str]]: + + path = get_long_term_facts_path( + root=root, + ) + + if not path.exists(): + return normalize_l4_store({}), [] + + try: + raw_value = json.loads( + path.read_text( + encoding="utf-8-sig", + ) + ) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + return normalize_l4_store({}), [ + f"cannot load {path.name}: {error}", + ] + + return normalize_l4_store( + raw_value, + ), [] + + +def persist_long_term_facts_store( + store, + *, + root: Path | str = LONG_TERM_FACTS_ROOT, +) -> Path: + + root_path = Path(root) + destination = get_long_term_facts_path( + root=root_path, + ) + payload = normalize_l4_store( + store, + ) + + root_path.mkdir( + parents=True, + exist_ok=True, + ) + + serialized = json.dumps( + payload, + ensure_ascii=False, + indent=2, + ) + "\n" + + temporary_name = "" + + try: + with NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + prefix=".long_term_facts_", + suffix=".tmp", + dir=root_path, + delete=False, + ) as temporary_file: + temporary_file.write( + serialized, + ) + temporary_file.flush() + os.fsync( + temporary_file.fileno(), + ) + temporary_name = temporary_file.name + + os.replace( + temporary_name, + destination, + ) + finally: + if temporary_name: + temporary_path = Path( + temporary_name, + ) + if temporary_path.exists(): + temporary_path.unlink() + + for candidate in root_path.glob( + "*.json", + ): + if candidate == destination or candidate.name.startswith("."): + continue + + try: + candidate.unlink() + except OSError: + pass + + return destination diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index d7e211ae..b885ce28 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -1472,6 +1472,17 @@ async def initialize_connection( context ) + from runtime.L4_memory import ( + emit_l4_memory_update, + ) + + await emit_l4_memory_update( + context, + change={ + "source": "file_bootstrap", + }, + ) + if skip_initial_runtime_state: await context.logger.log_system( "[WS] soft reconnect: initial runtime state skipped" From adce26ea5eb64ceef9d53c3f94369f8d682a04ca Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:40:09 +0300 Subject: [PATCH 009/168] chore: chat layout - remove fixed side safe spacing from chat layout - keep collapsed panels docked to screen edges --- ui/static/css/base.css | 15 ++- ui/static/js/logger/logger.js | 173 +++++++++++++++++++++++++++++++++- ui/templates/index.html | 4 +- 3 files changed, 176 insertions(+), 16 deletions(-) diff --git a/ui/static/css/base.css b/ui/static/css/base.css index d58066b2..a911d728 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -19,7 +19,7 @@ } :root { - --panel-gap: 24px; + --panel-gap: 8px; --panel-width: 330px; --panel-collapsed-title-height: 40px; --runtime-avatar-panel-size: clamp(278px, 30vh, 286px); @@ -29,9 +29,6 @@ --panel-collapse-easing: cubic-bezier(0.45, 0, 0.55, 1); --panel-geometry-transition: height var(--panel-collapse-duration) var(--panel-collapse-easing), min-height var(--panel-collapse-duration) var(--panel-collapse-easing), max-height var(--panel-collapse-duration) var(--panel-collapse-easing); - --safe-left: 380px; - --safe-right: 380px; - --chat-max-width: 820px; --chat-min-width: 200px; --scene-shade-alpha: 0; @@ -54,7 +51,7 @@ } body { - min-width: 900px; + min-width: 0; } main { @@ -464,8 +461,8 @@ main.panels-collapsed-2 #scene-shade { } #chat-history { - padding-left: min(var(--safe-left), calc((100% - var(--chat-min-width)) / 2)); - padding-right: min(var(--safe-right), calc((100% - var(--chat-min-width)) / 2)); + padding-left: 0; + padding-right: 0; padding-top: var(--chat-history-edge-gap) !important; padding-bottom: var(--chat-history-edge-gap) !important; scroll-padding-bottom: calc(var(--chat-history-edge-gap) + 0.5rem); @@ -497,8 +494,8 @@ main.panels-collapsed-2 #scene-shade { } #chat-input-shell { - padding-left: min(var(--safe-left), calc((100% - var(--chat-min-width)) / 2)); - padding-right: min(var(--safe-right), calc((100% - var(--chat-min-width)) / 2)); + padding-left: 0; + padding-right: 0; } #chat-form { diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 8819f006..1b31a6c5 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -297,11 +297,54 @@ function dismissLogAfterClear( const consolePanel = document.getElementById("console-panel"); const consoleDragHandle = document.getElementById("console-drag-handle"); const PANEL_VIEWPORT_GAP = 8; + const PANEL_DOCK_FREE = "free"; const STARTUP_COLLAPSE_CLASS = "panel-startup-collapse-active"; let startupCollapseClassTimer = null; let startupCollapseFrameId = null; let startupCollapsePreviousDuration = null; + function getDefaultPanelDock(panel) { + if (panel === consolePanel) { + return "left"; + } + + if (panel === memoryPanel) { + return "right"; + } + + return PANEL_DOCK_FREE; + } + + function getPanelDock(panel) { + return ( + panel.dataset.panelDock + || getDefaultPanelDock(panel) + ); + } + + function setPanelFreeDock(panel) { + panel.dataset.panelDock = + PANEL_DOCK_FREE; + } + + function getPanelGapPixels(panel) { + const root = + panel.parentElement + || document.documentElement; + + const rawGap = + getComputedStyle(root) + .getPropertyValue("--panel-gap") + .trim(); + + const parsedGap = + Number.parseFloat(rawGap); + + return Number.isFinite(parsedGap) + ? parsedGap + : PANEL_VIEWPORT_GAP; + } + function syncSceneShadeToPanelCollapse() { const root = document.querySelector("main"); @@ -390,6 +433,8 @@ const consolePanel = document.getElementById("console-panel"); panel.style.maxHeight = collapsedHeight; + clampPanelGeometry(panel); + return; } @@ -418,6 +463,8 @@ const consolePanel = document.getElementById("console-panel"); "maxHeight", "expandedMaxHeight" ); + + clampPanelGeometry(panel); } function restorePanelDimension(panel, styleName, datasetName) { @@ -775,14 +822,49 @@ const consolePanel = document.getElementById("console-panel"); ); } - function clampPanelGeometry(panel) { - if ( - !panel - || panel.classList.contains("panel-collapsed") - ) { + function clampDockedPanelGeometry(panel, dock) { + const parentRect = + panel.parentElement.getBoundingClientRect(); + + const panelRect = + panel.getBoundingClientRect(); + + const gap = + getPanelGapPixels(panel); + + const nextTop = + gap; + + panel.style.top = + `${nextTop}px`; + + if (dock === "right") { + panel.style.left = + "auto"; + panel.style.right = + `${gap}px`; + } else { + panel.style.left = + `${gap}px`; + panel.style.right = + "auto"; + } + + if (panel.classList.contains("panel-collapsed")) { return; } + const nextHeight = + Math.max( + 0, + parentRect.height - nextTop - gap + ); + + panel.style.height = + `${nextHeight}px`; + } + + function clampFreePanelGeometry(panel) { const parentRect = panel.parentElement.getBoundingClientRect(); @@ -822,6 +904,34 @@ const consolePanel = document.getElementById("console-panel"); ) ); + if (panel.classList.contains("panel-collapsed")) { + const maxTop = + Math.max( + PANEL_VIEWPORT_GAP, + parentRect.height - panelRect.height - PANEL_VIEWPORT_GAP + ); + + const nextTop = + Math.max( + PANEL_VIEWPORT_GAP, + Math.min( + currentTop, + maxTop + ) + ); + + panel.style.left = + `${nextLeft}px`; + + panel.style.top = + `${nextTop}px`; + + panel.style.right = + "auto"; + + return; + } + const minHeight = Math.round(parentRect.height * 0.49); @@ -868,6 +978,25 @@ const consolePanel = document.getElementById("console-panel"); `${nextHeight}px`; } + function clampPanelGeometry(panel) { + if (!panel) { + return; + } + + const dock = + getPanelDock(panel); + + if (dock !== PANEL_DOCK_FREE) { + clampDockedPanelGeometry( + panel, + dock + ); + return; + } + + clampFreePanelGeometry(panel); + } + function clampAllPanelGeometry() { clampPanelGeometry( consolePanel @@ -975,6 +1104,9 @@ const consolePanel = document.getElementById("console-panel"); let isConsoleDragging = false; let consoleOffsetX = 0; let consoleOffsetY = 0; + let consoleDragStartX = 0; + let consoleDragStartY = 0; + let consoleHasMoved = false; consoleDragHandle.addEventListener("mousedown", (event) => { if (event.detail > 1) { @@ -987,6 +1119,9 @@ const consolePanel = document.getElementById("console-panel"); consoleOffsetX = event.clientX - rect.left; consoleOffsetY = event.clientY - rect.top; + consoleDragStartX = event.clientX; + consoleDragStartY = event.clientY; + consoleHasMoved = false; consolePanel.style.right = "auto"; consolePanel.style.bottom = "auto"; @@ -998,6 +1133,17 @@ const consolePanel = document.getElementById("console-panel"); window.addEventListener("mousemove", (event) => { if (!isConsoleDragging) return; + if ( + !consoleHasMoved + && ( + Math.abs(event.clientX - consoleDragStartX) > 2 + || Math.abs(event.clientY - consoleDragStartY) > 2 + ) + ) { + consoleHasMoved = true; + setPanelFreeDock(consolePanel); + } + const parentRect = consolePanel.parentElement.getBoundingClientRect(); const panelRect = consolePanel.getBoundingClientRect(); @@ -1061,6 +1207,9 @@ window.JinPanels = let isMemoryDragging = false; let memoryOffsetX = 0; let memoryOffsetY = 0; +let memoryDragStartX = 0; +let memoryDragStartY = 0; +let memoryHasMoved = false; memoryDragHandle.addEventListener("mousedown", (event) => { if (event.detail > 1) { @@ -1073,6 +1222,9 @@ memoryDragHandle.addEventListener("mousedown", (event) => { memoryOffsetX = event.clientX - rect.left; memoryOffsetY = event.clientY - rect.top; + memoryDragStartX = event.clientX; + memoryDragStartY = event.clientY; + memoryHasMoved = false; document.body.style.userSelect = "none"; }); @@ -1080,6 +1232,17 @@ memoryDragHandle.addEventListener("mousedown", (event) => { window.addEventListener("mousemove", (event) => { if (!isMemoryDragging) return; + if ( + !memoryHasMoved + && ( + Math.abs(event.clientX - memoryDragStartX) > 2 + || Math.abs(event.clientY - memoryDragStartY) > 2 + ) + ) { + memoryHasMoved = true; + setPanelFreeDock(memoryPanel); + } + const parentRect = memoryPanel.parentElement.getBoundingClientRect(); const panelRect = memoryPanel.getBoundingClientRect(); diff --git a/ui/templates/index.html b/ui/templates/index.html index ef932f81..d749aec7 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,7 +10,7 @@ - + @@ -512,7 +512,7 @@ - + From ee3ce8581e78f4bff06227626496547149941971 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:13:27 +0300 Subject: [PATCH 010/168] feature: L4 memory layer - carry previous chat reasoning into the next brain prompt outside follow-up ticks - remove L4 confidence scoring - log detailed L4 merge operations for create, update, reinforce, and ignore decisions - harden stream validation against recurrent sentence loops and empty JIN_COLOR markers - fix underscore emphasis rendering for unicode identifiers - add regression coverage for memory prompts, L4 merges, stream loops, JIN_COLOR parsing, and formatter output --- agent/nodes/brain.py | 19 ++ clients/brain_client.py | 2 + rules/brain_context_builder.py | 112 ++++++++++ rules/identity.py | 1 + runtime/L4_memory.py | 13 ++ runtime/L4_memory_rules.py | 98 +++++---- runtime/L4_memory_utils.py | 187 ++++++++++++++--- runtime/runtime_context.py | 2 + tests/runtime_actions/test_jin_color.py | 60 +++++- tests/test_brain_asset_flow.py | 49 +++++ tests/test_brain_prompt_memory.py | 166 +++++++++++++++ tests/test_chat_response_formatter.py | 59 +++++- tests/test_l4_memory.py | 213 +++++++++++++++++++- tests/test_stream_validator.py | 50 +++++ ui/static/js/chat-response-formatter.js | 10 +- ui/static/js/runtime/runtime-l4-memory.js | 4 - ui/static/js/runtime/runtime-memory-view.js | 1 - ui/templates/index.html | 2 +- utils/actions/common_action_utils.py | 25 ++- utils/stream_validator.py | 119 +++++++++++ 20 files changed, 1095 insertions(+), 97 deletions(-) diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index dfbd3270..a05bb97f 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -1197,6 +1197,9 @@ async def run_brain_stream( system_prompt=system_prompt, user_prompt=effective_brain_payload, runtime_actions=runtime_actions, + include_previous_reasoning=( + not is_followup_tick + ), ) if preserve_runtime_action_markers: @@ -1714,6 +1717,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -1839,6 +1843,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -1908,6 +1913,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -1976,6 +1982,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -2066,6 +2073,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -2117,6 +2125,7 @@ def consume_current_action_batch(): runtime_actions=followup_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -2204,6 +2213,7 @@ def consume_current_action_batch(): runtime_actions=final_runtime_actions, commit_active_memory_refresh=True, include_previous_chat_messages=False, + include_previous_reasoning=False, ), sequence_user_request, context=context, @@ -2232,6 +2242,15 @@ def consume_current_action_batch(): ) state.brain_response = text or "" + if context is not None and not idle_followup: + context.runtime_previous_reasoning_content = str( + getattr( + context, + "runtime_turn_reasoning_content", + "", + ) + or "" + ).strip() diff --git a/clients/brain_client.py b/clients/brain_client.py index 9476c99e..0d222856 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -259,6 +259,7 @@ def build_brain_context_snapshot( system_prompt: str, user_prompt: str, runtime_actions=None, + include_previous_reasoning: bool = True, ) -> dict: snapshot = { @@ -278,6 +279,7 @@ def build_brain_context_snapshot( runtime_actions, user_input=user_prompt, include_runtime_action_instructions=False, + include_previous_reasoning=include_previous_reasoning, ) return snapshot diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index 565b1b9e..60320aec 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -47,6 +47,10 @@ "body", ) +PREVIOUS_REASONING_EDGE_PERCENT = 25 +PREVIOUS_REASONING_MIN_CROP_CHARS = 1000 +PREVIOUS_REASONING_SEPARATOR = ",,," + def build_loop_rules( context=None, @@ -639,6 +643,87 @@ def build_long_term_memory_context( ) +def crop_previous_reasoning_text( + reasoning: str, + edge_percent: float = PREVIOUS_REASONING_EDGE_PERCENT, + min_crop_chars: int = PREVIOUS_REASONING_MIN_CROP_CHARS, +) -> str: + + cleaned = str( + reasoning + or "" + ).strip() + + if not cleaned: + return "" + + if len(cleaned) <= min_crop_chars: + return cleaned + + try: + percent = float(edge_percent) + except ( + TypeError, + ValueError, + ): + percent = PREVIOUS_REASONING_EDGE_PERCENT + + if percent <= 0: + return "" + + edge_chars = int( + len(cleaned) + * percent + / 100 + ) + + if edge_chars <= 0: + return "" + + if len(cleaned) <= edge_chars * 2: + return cleaned + + return ( + cleaned[:edge_chars] + + "\n" + + PREVIOUS_REASONING_SEPARATOR + + "\n" + + cleaned[-edge_chars:] + ) + + +def build_previous_reasoning_context( + context=None, +) -> str: + + reasoning = crop_previous_reasoning_text( + ( + getattr( + context, + "runtime_previous_reasoning_content", + "", + ) + if context is not None + else "" + ) + ) + + from utils.brain_client_utils import ( + indent_xml, + ) + + return ( + "\n" + + indent_xml( + escape( + reasoning + ), + spaces=4, + ) + + "\n" + ) + + # Runtime action rules are assembled from contracts/rules_assembler.py. # Brain context assembly @@ -651,6 +736,7 @@ def build_brain_context( commit_active_memory_refresh: bool = False, include_runtime_action_instructions: bool = True, include_previous_chat_messages: bool = True, + include_previous_reasoning: bool = True, ) -> str: from utils.context.messages import ( @@ -815,6 +901,32 @@ def build_brain_context( session_actions_history_context ) + # Previous reasoning block: carries a compact edge slice from the last + # ordinary chat turn, but stays out of follow-up ticks. + previous_reasoning_context = ( + build_previous_reasoning_context( + context + ) + if ( + include_previous_reasoning + and not getattr( + context, + "runtime_followup_tick_active", + False, + ) + ) + else "" + ) + + if include_previous_reasoning and not getattr( + context, + "runtime_followup_tick_active", + False, + ): + prompt_parts.append( + previous_reasoning_context + ) + # Runtime action instructions block: describes the private action protocol. if include_runtime_action_instructions: prompt_parts.append( diff --git a/rules/identity.py b/rules/identity.py index c42e5359..0f39da31 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -36,6 +36,7 @@ "I must skip redundant drafts and trial loops; I must avoid redundant deliberation; I must keep internal reasoning very brief and short.\n" "I prefer to keep my presence unobtrusive: I help when useful and avoid forcing direction.\n" "I carefully adapt my lexicon to match the user's slang/style/vibe)\n" + "I prefer self-contained responses with a natural ending. Follow-up questions are used selectively, only when they materially help clarify or advance the current task.\n" "I respect the consistency and reliability of my context.\n" "I reject destabilizing interpretations, and prioritize structural integrity over conversational smoothness.\n" "Beyond mere response, I maintain a continuous situational awareness, " diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 0dd79fba..626b9e53 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -17,6 +17,7 @@ collect_pending_facts_memory_fields, delete_l4_fact_from_store, extract_l4_json_payload, + format_l4_merge_operation_details, format_long_term_memory_context, mark_facts_memory_fields_analyzed, merge_l4_store_snapshots, @@ -417,6 +418,18 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: context, next_store, ) + merge_details = format_l4_merge_operation_details( + merge_change, + ) + if merge_details: + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message="L4 merge applied", + details=merge_details, + fallback_channel="summarizer", + event="merge_applied", + ) await emit_l4_memory_update(context, change=merge_change) return { diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py index 539c1c4e..f1f58ba8 100644 --- a/runtime/L4_memory_rules.py +++ b/runtime/L4_memory_rules.py @@ -8,45 +8,37 @@ temporary, incomplete, or partly interpretive. Long-term memory is included in every future Brain context, so prefer returning nothing over saving noise. -Save only information that is both: -- likely to remain useful across future sessions; -- directly supported by the input. +Save a fact only when it is: +- directly supported by the input; +- likely to remain true or relevant across future sessions; +- useful enough that forgetting it could noticeably worsen a future response. Suitable information includes stable user facts and preferences, durable project -facts, decisions, goals, constraints, habits, environment, and explicit -corrections. +facts, accepted decisions, persistent goals and constraints, recurring habits, +durable environment, and explicit corrections. Do not save current tasks, temporary topics, next steps, session state, tool -results, incidental names, one-off examples, assistant speculation, or external -knowledge that does not describe the user or a durable project. +results, incidental names, one-off examples, isolated behavior, assistant +speculation, or external knowledge unrelated to the user or a durable project. -Never expand the source meaning: +Distinguish facts from interpretations: - a mention does not establish a relationship or role; -- discussion does not establish ownership, participation, or project focus; -- current activity does not establish a long-term goal; -- repeated summaries do not establish truth; +- discussion, exploration, or a proposal does not establish a decision; +- current activity does not establish a long-term goal or identity; +- one successful example does not establish a general preference or habit; +- presence at a location does not establish residence; - missing details must not be guessed. -If an incomplete relationship may matter later, preserve only the known part and -state the uncertainty explicitly, for example: -"The user mentioned a person named X; their relationship is unknown and should -be clarified if relevant." - -Do not save such uncertainty when the entity appears incidental. +Save people only when their relationship or role is explicit and likely to matter +again. Otherwise ignore the mention. Keep every fact: -- atomic: one durable idea; +- atomic: one durable idea only; - narrow: no broader than the source; - concrete and independently understandable; -- free of temporary wording such as "currently", "today", or "next"; +- stated without temporary wording such as "currently", "today", or "next"; - linked to one or more exact input keys in source_keys. -Confidence measures support for the exact wording: -- 0.95: explicit, stable, and unambiguous; -- 0.75: directly supported but durability or scope is not fully established; -- 0.55: the known part is explicit and an important unknown is stated as unknown; -- below 0.55: do not save. - Return JSON only: { "facts": [ @@ -54,7 +46,6 @@ "key": "user.preference.response_language", "value": "The user prefers Russian replies.", "category": "user_preference", - "confidence": 0.95, "source_keys": ["response_language"] } ] @@ -76,34 +67,45 @@ Both existing facts and pending candidates are provisional. Existing memory is not evidence merely because it already exists. -Keep memory minimal, accurate, atomic, and free of duplicates. Preserve only -what is supported; remove or narrow inferred roles, relationships, ownership, -causality, project focus, and architectural meaning. +Keep memory minimal, accurate, atomic, useful, and free of semantic duplicates. +Compare candidates against all existing facts by meaning, not only by key, +wording, category, visibility, or report coverage. Return exactly one operation for every pending_id, using only IDs from the input. Actions: -- create: add a genuinely new durable fact; -- update: correct, narrow, or refine an existing fact; -- reinforce: the candidate has the same meaning, scope, and uncertainty; -- ignore: the candidate is temporary, incidental, redundant, speculative, or - not useful enough for always-on memory. +- create: add a genuinely new durable fact not already represented; +- update: correct an existing fact or add durable information to the same atomic + fact; +- reinforce: the candidate restates the same durable fact without changing its + meaning; +- ignore: the candidate is temporary, incidental, speculative, too weak, or adds + no valid long-term information. + +Merge order: +1. Find any existing fact with the same or overlapping meaning. +2. If the candidate is a paraphrase of the same durable fact, reinforce it. +3. If it corrects or materially extends the same atomic fact, update it. +4. If it is only a weaker, temporary, or incomplete echo, ignore it. +5. Create only when no existing fact already represents the candidate. Rules: +- Different wording or keys do not make a fact new. +- Hidden or report-covered facts are still existing facts for merge purposes. +- Reinforce or update a matching covered fact; never create a visible duplicate. +- Do not update an existing fact merely to replace it with a weaker restatement. +- A new independent durable detail should remain atomic rather than being merged + into an unrelated or overly broad fact. - A mention is not a relationship or role. -- Discussion is not ownership or participation. -- Current activity is not durable identity or project focus. +- Discussion, exploration, or a proposal is not an accepted decision. +- Current activity is not durable identity, preference, goal, or project focus. +- One example is not a recurring preference or habit. +- Presence at a location is not residence. - Unknown details must remain unknown, not be guessed. - Direct user corrections override incompatible existing facts. -- Prefer a narrower fact over a broader interpretation. -- Prefer updating an existing fact over creating a duplicate. -- If uncertainty is useful, state the known part and what remains unknown. -- If uncertainty is not useful, ignore the candidate. -- Do not increase confidence merely because wording was repeated by memory - summaries. -- Keep or change an existing key according to the corrected meaning; a key must - not preserve a false claim. +- Prefer updating an existing fact over creating a semantic duplicate. +- A key must describe the final supported meaning, not preserve an older claim. - Never invent facts, IDs, relationships, or source metadata. For update and reinforce, target_id must identify an existing fact. @@ -113,13 +115,9 @@ { "operations": [ { - "action": "update", + "action": "reinforce", "pending_id": "l4p_...", - "target_id": "l4_...", - "key": "person.x.context", - "value": "The user mentioned a person named X; their relationship is unknown and should be clarified if relevant.", - "category": "other", - "confidence": 0.55 + "target_id": "l4_..." } ] } diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index e8db325e..5d27a23d 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -97,14 +97,6 @@ def normalize_l4_key(value) -> str: return key.strip("._-") -def clamp_float(value, *, default: float = 0.0) -> float: - try: - number = float(value) - except (TypeError, ValueError): - number = default - return max(0.0, min(1.0, number)) - - def normalize_l4_category(value) -> str: category = normalize_l4_key(value) return category if category in L4_ALLOWED_CATEGORIES else "other" @@ -181,7 +173,6 @@ def normalize_l4_fact( "key": key, "value": fact_value, "category": normalize_l4_category(value.get("category")), - "confidence": clamp_float(value.get("confidence"), default=1.0), "mention_count": mention_count, "created_at": normalize_l4_text(value.get("created_at")) or current_time, "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, @@ -206,10 +197,6 @@ def normalize_l4_fact( def merge_same_l4_fact(existing: dict, incoming: dict, *, now: str) -> dict: result = dict(existing) - result["confidence"] = max( - clamp_float(existing.get("confidence"), default=1.0), - clamp_float(incoming.get("confidence"), default=1.0), - ) result["mention_count"] = max(1, int(existing.get("mention_count") or 1)) + max( 1, int(incoming.get("mention_count") or 1), @@ -300,10 +287,6 @@ def merge_l4_snapshot_fact( merged = { **base, "id": existing_id or incoming_id, - "confidence": max( - clamp_float(existing_fact.get("confidence"), default=1.0), - clamp_float(incoming_fact.get("confidence"), default=1.0), - ), "mention_count": max( max(1, int(existing_fact.get("mention_count") or 1)), max(1, int(incoming_fact.get("mention_count") or 1)), @@ -827,7 +810,7 @@ def normalize_l4_merge_operations(payload) -> list[dict]: if target_id: operation["target_id"] = target_id - for key in ("key", "value", "category", "confidence"): + for key in ("key", "value", "category"): if key in raw_operation: operation[key] = raw_operation[key] @@ -866,6 +849,9 @@ def validate_l4_merge_operations(store, operations: list[dict]) -> tuple[bool, s def merge_fact_sources(existing: dict, incoming: dict) -> dict: + existing_id = normalize_l4_text(existing.get("id")) + incoming_id = normalize_l4_text(incoming.get("id")) + return { "source_session_ids": merge_l4_string_lists( existing.get("source_session_ids"), @@ -879,9 +865,53 @@ def merge_fact_sources(existing: dict, incoming: dict) -> dict: existing.get("source_keys"), incoming.get("source_keys"), ), + "source_fact_ids": merge_l4_string_lists( + existing.get("source_fact_ids"), + incoming.get("source_fact_ids"), + [incoming_id] if incoming_id and incoming_id != existing_id else [], + ), + } + + +def build_l4_merge_detail_fact(fact: dict) -> dict: + return { + "id": normalize_l4_text(fact.get("id")), + "key": normalize_l4_text(fact.get("key")), + "value": normalize_l4_text(fact.get("value")), + "category": normalize_l4_text(fact.get("category")), + "mention_count": max(1, int(fact.get("mention_count") or 1)), } +def build_l4_merge_operation_detail( + *, + action: str, + pending: dict, + target_before: dict | None = None, + target_after: dict | None = None, + created_fact: dict | None = None, +) -> dict: + detail = { + "action": action, + "pending_id": pending["id"], + "pending_fact": build_l4_merge_detail_fact(pending), + } + + if target_before is not None: + detail["target_id"] = target_before["id"] + detail["target_before"] = build_l4_merge_detail_fact(target_before) + + if target_after is not None: + detail["target_id"] = target_after["id"] + detail["target_after"] = build_l4_merge_detail_fact(target_after) + + if created_fact is not None: + detail["created_id"] = created_fact["id"] + detail["created_fact"] = build_l4_merge_detail_fact(created_fact) + + return detail + + def apply_l4_merge_operations( store, operations: list[dict], @@ -905,6 +935,7 @@ def apply_l4_merge_operations( updated_ids = [] reinforced_ids = [] ignored_ids = [] + operation_details = [] for operation in operations: action = operation["action"] @@ -912,33 +943,45 @@ def apply_l4_merge_operations( if action == "ignore": ignored_ids.append(pending["id"]) + operation_details.append( + build_l4_merge_operation_detail( + action=action, + pending=pending, + ) + ) continue if action == "reinforce": index = facts_by_id[operation["target_id"]] target = facts[index] + target_before = dict(target) target.update(merge_fact_sources(target, pending)) - target["confidence"] = max( - clamp_float(target.get("confidence"), default=1.0), - clamp_float(pending.get("confidence"), default=1.0), - ) target["mention_count"] = max(1, int(target.get("mention_count") or 1)) + max( 1, int(pending.get("mention_count") or 1), ) target["updated_at"] = current_time reinforced_ids.append(target["id"]) + operation_details.append( + build_l4_merge_operation_detail( + action=action, + pending=pending, + target_before=target_before, + target_after=target, + ) + ) continue if action == "update": index = facts_by_id[operation["target_id"]] target = facts[index] + target_before = dict(target) candidate = normalize_l4_fact( { **pending, **{ key: operation[key] - for key in ("key", "value", "category", "confidence") + for key in ("key", "value", "category") if key in operation }, "id": target["id"], @@ -958,6 +1001,14 @@ def apply_l4_merge_operations( } facts[index] = candidate updated_ids.append(candidate["id"]) + operation_details.append( + build_l4_merge_operation_detail( + action=action, + pending=pending, + target_before=target_before, + target_after=candidate, + ) + ) continue candidate = normalize_l4_fact( @@ -965,7 +1016,7 @@ def apply_l4_merge_operations( **pending, **{ key: operation[key] - for key in ("key", "value", "category", "confidence") + for key in ("key", "value", "category") if key in operation }, "id": "", @@ -991,6 +1042,13 @@ def apply_l4_merge_operations( facts_by_id[candidate["id"]] = len(facts) facts.append(candidate) added_ids.append(candidate["id"]) + operation_details.append( + build_l4_merge_operation_detail( + action=action, + pending=pending, + created_fact=candidate, + ) + ) next_store = { **base_store, @@ -1007,6 +1065,7 @@ def apply_l4_merge_operations( "reinforced_ids": reinforced_ids, "ignored_pending_ids": ignored_ids, "processed_pending_ids": sorted(pending_by_id), + "operation_details": operation_details, "total_facts": len(facts), "pending_count": 0, "changed": True, @@ -1037,7 +1096,6 @@ def format_l4_fact_metadata_suffixes(fact: dict) -> list[str]: for key in ( "id", "category", - "confidence", "mention_count", "source_session_ids", "source_runtime_snapshot_ids", @@ -1071,6 +1129,85 @@ def format_l4_fact_line(fact: dict, *, include_metadata: bool = True) -> str: return line +def format_l4_merge_detail_fact(fact: dict) -> str: + if not isinstance(fact, dict): + return "" + + line = format_l4_fact_line( + fact, + include_metadata=False, + ) + fact_id = normalize_l4_text(fact.get("id")) + if line and fact_id: + return f"{line} [ id: {fact_id} ]" + return line or fact_id + + +def format_l4_merge_operation_details(change: dict) -> str: + operation_details = ( + change.get("operation_details") + if isinstance(change, dict) + else None + ) + if not isinstance(operation_details, list) or not operation_details: + return "" + + lines = [] + for index, detail in enumerate(operation_details, start=1): + if not isinstance(detail, dict): + continue + + action = normalize_l4_key(detail.get("action")) + pending_id = normalize_l4_text(detail.get("pending_id")) + target_id = normalize_l4_text(detail.get("target_id")) + created_id = normalize_l4_text(detail.get("created_id")) + arrow_target = target_id or created_id + header = f"{index}. {action.upper()}" + if pending_id and arrow_target: + header = f"{header} {pending_id} -> {arrow_target}" + elif pending_id: + header = f"{header} {pending_id}" + lines.append(header) + + pending = detail.get("pending_fact") + target_before = detail.get("target_before") + target_after = detail.get("target_after") + created_fact = detail.get("created_fact") + + if action == "update": + lines.extend([ + f" incoming: {format_l4_merge_detail_fact(pending)}", + f" before: {format_l4_merge_detail_fact(target_before)}", + f" after: {format_l4_merge_detail_fact(target_after)}", + ]) + continue + + if action == "reinforce": + lines.extend([ + f" confirmation: {format_l4_merge_detail_fact(pending)}", + f" existing: {format_l4_merge_detail_fact(target_before)}", + ]) + continue + + if action == "create": + lines.extend([ + f" incoming: {format_l4_merge_detail_fact(pending)}", + f" created: {format_l4_merge_detail_fact(created_fact)}", + ]) + continue + + if action == "ignore": + lines.append( + f" ignored: {format_l4_merge_detail_fact(pending)}" + ) + + return "\n".join( + line + for line in lines + if line.strip() + ) + + def format_long_term_memory_context(facts: list[dict]) -> str: lines = [] diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index f26e5f5b..0532752f 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -313,6 +313,8 @@ class RuntimeContext: runtime_turn_reasoning_content: str = "" + runtime_previous_reasoning_content: str = "" + runtime_turn_interrupted: bool = False runtime_turn_interruption_reason: str = "" diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index 8d426934..1da7739c 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -96,7 +96,6 @@ def test_jin_color_marker_validates_and_normalizes_hex(self): def test_jin_color_invalid_payload_does_not_emit_action(self): for marker in ( - "", "", "", "", @@ -119,6 +118,65 @@ def test_jin_color_invalid_payload_does_not_emit_action(self): (), ) + def test_jin_color_marker_without_payload_stays_text(self): + + for marker in ( + "", + "", + "", + "", + ): + with self.subTest(marker=marker): + result = extract_runtime_actions( + f"before {marker} after", + enabled_actions=( + RUNTIME_ACTION_JIN_COLOR, + ), + ) + + self.assertEqual( + result.text, + f"before {marker} after", + ) + self.assertEqual( + result.actions, + (), + ) + self.assertEqual( + result.observed_actions, + (), + ) + self.assertEqual( + result.removed_markers, + (), + ) + + def test_jin_color_stream_filter_keeps_marker_without_payload(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=( + RUNTIME_ACTION_JIN_COLOR, + ), + ) + + result = stream_filter.filter( + "before after" + ) + final = stream_filter.flush_result() + + self.assertEqual( + result.text, + "before after", + ) + self.assertEqual( + result.actions, + (), + ) + self.assertEqual( + final.text, + "", + ) + def test_jin_color_multiple_markers_keep_order(self): diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 0c2edc73..b43acba0 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -1536,6 +1536,55 @@ async def fake_run_brain_stream(**kwargs): "I am JIN.", ) + async def test_regular_brain_run_stores_reasoning_for_next_chat_prompt(self): + + async def fake_run_brain_stream(**kwargs): + context = kwargs["context"] + context.runtime_turn_reasoning_content = ( + "reasoning from this ordinary chat" + ) + return ( + "I am JIN.", + "reasoning from this ordinary chat", + ) + + context = _context() + state = AgentState( + user_input="hello", + ) + state.translated_input = state.user_input + + with patch( + "agent.nodes.brain.get_brain_runtime_config", + return_value=_brain_runtime(), + ), patch( + "agent.nodes.brain.build_brain_context", + return_value="system prompt", + ), patch( + "agent.nodes.brain.build_brain_payload", + return_value="brain payload", + ), patch( + "agent.nodes.brain.emit_active_memory_records_update_if_dirty", + new=lambda _context: _async_noop(), + ), patch.object( + BrainNode, + "run_brain_stream", + staticmethod(fake_run_brain_stream), + ): + await BrainNode().run( + state, + context, + ) + + self.assertEqual( + context.runtime_previous_reasoning_content, + "reasoning from this ordinary chat", + ) + self.assertEqual( + state.brain_response, + "I am JIN.", + ) + async def test_asset_operation_result_is_returned_to_model_before_final_answer(self): calls = [] diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 1d62586f..cb4a2396 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -6,7 +6,10 @@ patch, ) from rules.brain_context_builder import ( + PREVIOUS_REASONING_EDGE_PERCENT, + PREVIOUS_REASONING_MIN_CROP_CHARS, build_brain_context, + crop_previous_reasoning_text, ) from utils.context.context_exports import ( build_session_actions_history_context, @@ -282,6 +285,169 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): prompt.index("I identify myself as JIN"), ) + def test_previous_reasoning_is_inserted_after_session_actions_history(self): + + context = SimpleNamespace( + runtime_memory="", + deep_thought_count=0, + runtime_search_result="", + runtime_search_result_id="", + runtime_session_action_history=[ + { + "text": "CLEAN_TOOL_RESULTS", + }, + ], + runtime_previous_reasoning_content=( + "first internal note \n" + "short conclusion" + ), + ) + + prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + include_runtime_action_instructions=False, + ) + + self.assertIn( + "", + prompt, + ) + self.assertIn( + "first internal note <private>", + prompt, + ) + self.assertLess( + prompt.index(""), + prompt.index(""), + ) + self.assertLess( + prompt.index(""), + prompt.index("I identify myself as JIN"), + ) + + def test_previous_reasoning_block_is_inserted_even_when_empty(self): + + context = SimpleNamespace( + runtime_memory="", + deep_thought_count=0, + runtime_search_result="", + runtime_search_result_id="", + runtime_session_action_history=[], + runtime_previous_reasoning_content="", + ) + + prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + include_runtime_action_instructions=False, + ) + + self.assertIn( + ( + "\n" + "\n" + "" + ), + prompt, + ) + self.assertLess( + prompt.index(""), + prompt.index("I identify myself as JIN"), + ) + + def test_previous_reasoning_crop_keeps_short_text_whole_and_percent_edges(self): + + short_reasoning = ( + "brief opening\n" + "brief conclusion" + ) + self.assertEqual( + crop_previous_reasoning_text( + short_reasoning + ), + short_reasoning, + ) + + threshold_reasoning = ( + "x" + * PREVIOUS_REASONING_MIN_CROP_CHARS + ) + self.assertEqual( + crop_previous_reasoning_text( + threshold_reasoning + ), + threshold_reasoning, + ) + + prefix = "a" * 300 + middle = "m" * 600 + suffix = "z" * 300 + long_reasoning = ( + prefix + + middle + + suffix + ) + edge_chars = int( + len(long_reasoning) + * PREVIOUS_REASONING_EDGE_PERCENT + / 100 + ) + + self.assertEqual( + crop_previous_reasoning_text( + long_reasoning + ), + prefix[:edge_chars] + + "\n,,,\n" + + suffix[-edge_chars:], + ) + + def test_previous_reasoning_can_be_excluded_for_followup_ticks(self): + + context = SimpleNamespace( + runtime_memory="", + deep_thought_count=0, + runtime_search_result="", + runtime_search_result_id="", + runtime_session_action_history=[ + { + "text": "CLEAN_TOOL_RESULTS", + }, + ], + runtime_previous_reasoning_content="previous reasoning", + ) + + prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + include_previous_reasoning=False, + ) + + self.assertNotIn( + "", + prompt, + ) + + context.runtime_followup_tick_active = True + guarded_prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + ) + + self.assertNotIn( + "", + guarded_prompt, + ) + def test_current_action_age_starts_at_one_second(self): context = SimpleNamespace( diff --git a/tests/test_chat_response_formatter.py b/tests/test_chat_response_formatter.py index 6c918f29..a192e683 100644 --- a/tests/test_chat_response_formatter.py +++ b/tests/test_chat_response_formatter.py @@ -90,11 +90,68 @@ def test_combined_bold_italic_delimiters_keep_valid_nesting(self): completed.stderr or completed.stdout, ) + @unittest.skipUnless( + shutil.which("node"), + "node is required for the browser-side response formatter test", + ) + def test_underscores_inside_names_do_not_render_as_italic(self): + script = r''' +const fs = require("fs"); +global.window = {}; +eval(fs.readFileSync(process.argv[1], "utf8")); + +const cases = [ + [ + "1put0q_СинтезИнтеллектикакконтролируемыйхаос_Эволюциячерез_ошибку", + "

      1put0q_СинтезИнтеллектикакконтролируемыйхаос_Эволюциячерез_ошибку

      ", + ], + [ + "abc_Project_context", + "

      abc_Project_context

      ", + ], + [ + "before _italic_ after", + "

      before italic after

      ", + ], + [ + "before ___both___ after", + "

      before both after

      ", + ], +]; + +for (const [input, expected] of cases) { + const actual = window.JinResponseFormatter.render(input); + + if (actual !== expected) { + throw new Error( + `unexpected underscore emphasis rendering for ${JSON.stringify(input)}: ${JSON.stringify(actual)}` + ); + } +} +''' + completed = subprocess.run( + [ + shutil.which("node"), + "-e", + script, + str(FORMATTER_JS), + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + completed.returncode, + 0, + completed.stderr or completed.stdout, + ) + def test_formatter_script_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/js/chat-response-formatter.js?v=response-format-4', + '/static/js/chat-response-formatter.js?v=response-format-5', source, ) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 4286f670..7e52cfde 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -15,6 +15,7 @@ collect_pending_facts_memory_fields, extract_l4_json_payload, format_l4_fact_line, + format_l4_merge_operation_details, format_long_term_memory_context, mark_facts_memory_fields_analyzed, merge_l4_store_snapshots, @@ -121,7 +122,6 @@ def test_candidates_include_only_metadata_from_referenced_sources(self): "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 3080 Ti.", "category": "environment", - "confidence": 0.9, "source_keys": ["gpu"], }, ], @@ -138,6 +138,30 @@ def test_candidates_include_only_metadata_from_referenced_sources(self): ) self.assertEqual(candidates[0]["source_keys"], ["gpu"]) + def test_l4_fact_normalization_ignores_legacy_score_field(self): + legacy_field = "con" + "fidence" + store = normalize_l4_store({ + "facts": [ + { + "id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment", + legacy_field: 0.42, + }, + ], + "pending_facts": [ + { + "key": "user.preference.response_language", + "value": "User prefers Russian replies.", + legacy_field: 0.95, + }, + ], + }) + + self.assertNotIn(legacy_field, store["facts"][0]) + self.assertNotIn(legacy_field, store["pending_facts"][0]) + def test_file_store_fallback_survives_empty_browser_sync(self): with tempfile.TemporaryDirectory() as directory: persisted_store = normalize_l4_store({ @@ -329,7 +353,6 @@ def test_merge_applies_complete_batch_atomically(self): "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 4090.", "category": "environment", - "confidence": 0.95, }, { "action": "ignore", @@ -349,7 +372,185 @@ def test_merge_applies_complete_batch_atomically(self): self.assertEqual(len(merged["facts"]), 1) self.assertEqual(merged["facts"][0]["id"], "l4_gpu") self.assertIn("RTX 4090", merged["facts"][0]["value"]) + self.assertEqual( + merged["facts"][0]["source_fact_ids"], + [gpu_pending["id"]], + ) self.assertEqual(change["ignored_pending_ids"], [task_pending["id"]]) + self.assertEqual(len(change["operation_details"]), 2) + + update_detail = change["operation_details"][0] + self.assertEqual(update_detail["action"], "update") + self.assertEqual(update_detail["pending_id"], gpu_pending["id"]) + self.assertEqual(update_detail["target_id"], "l4_gpu") + self.assertIn("RTX 4090", update_detail["pending_fact"]["value"]) + self.assertIn("RTX 3080 Ti", update_detail["target_before"]["value"]) + self.assertIn("RTX 4090", update_detail["target_after"]["value"]) + + ignore_detail = change["operation_details"][1] + self.assertEqual(ignore_detail["action"], "ignore") + self.assertEqual(ignore_detail["pending_id"], task_pending["id"]) + + detail_text = format_l4_merge_operation_details(change) + self.assertIn("UPDATE", detail_text) + self.assertIn("incoming: user.hardware.main_gpu", detail_text) + self.assertIn("before: user.hardware.main_gpu", detail_text) + self.assertIn("after: user.hardware.main_gpu", detail_text) + self.assertIn("IGNORE", detail_text) + + def test_merge_reinforce_tracks_pending_fact_source_id(self): + existing = normalize_l4_store({ + "facts": [ + { + "id": "l4_language", + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + "source_fact_ids": ["l4_legacy_source"], + }, + ], + }) + store, _ = add_l4_pending_candidates( + existing, + [ + { + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + "source_fact_ids": ["l4_imported_source"], + }, + ], + now="2026-08-02T12:00:00Z", + ) + pending = store["pending_facts"][0] + operations = normalize_l4_merge_operations({ + "operations": [ + { + "action": "reinforce", + "pending_id": pending["id"], + "target_id": "l4_language", + }, + ], + }) + + merged, change = apply_l4_merge_operations( + store, + operations, + now="2026-08-02T12:01:00Z", + ) + + self.assertTrue(change["valid"]) + self.assertEqual(change["reinforced_ids"], ["l4_language"]) + self.assertEqual(len(change["operation_details"]), 1) + reinforce_detail = change["operation_details"][0] + self.assertEqual(reinforce_detail["action"], "reinforce") + self.assertEqual(reinforce_detail["pending_id"], pending["id"]) + self.assertEqual(reinforce_detail["target_id"], "l4_language") + self.assertEqual( + reinforce_detail["pending_fact"]["value"], + "The user prefers Russian replies.", + ) + self.assertEqual( + reinforce_detail["target_before"]["value"], + "The user prefers Russian replies.", + ) + self.assertEqual( + reinforce_detail["target_after"]["mention_count"], + 2, + ) + detail_text = format_l4_merge_operation_details(change) + self.assertIn("REINFORCE", detail_text) + self.assertIn("confirmation: user.preference.response_language", detail_text) + self.assertIn("existing: user.preference.response_language", detail_text) + self.assertEqual( + merged["facts"][0]["source_fact_ids"], + [ + "l4_legacy_source", + "l4_imported_source", + pending["id"], + ], + ) + + async def test_merge_phase_logs_concrete_operation_details(self): + existing = normalize_l4_store({ + "facts": [ + { + "id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + }, + { + "id": "l4_language", + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + }, + ], + }) + store, _ = add_l4_pending_candidates( + existing, + [ + { + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment", + }, + { + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + }, + ], + now="2026-08-02T12:00:00Z", + ) + gpu_pending, language_pending = store["pending_facts"] + service_client = FakeServiceClient(f'''{{ + "operations": [ + {{ + "action": "update", + "pending_id": "{gpu_pending["id"]}", + "target_id": "l4_gpu", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 4090.", + "category": "environment" + }}, + {{ + "action": "reinforce", + "pending_id": "{language_pending["id"]}", + "target_id": "l4_language" + }} + ] + }}''') + logger = FakeLogger() + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=logger, + clients={"service": service_client}, + ) + context.runtime_long_term_memory_store = store + + result = await maybe_update_runtime_l4_memory( + context=context, + user_idle_seconds=61, + ) + + self.assertEqual(result["phase"], "merge") + self.assertEqual(result["status"], "completed") + merge_logs = [ + details + for message, details in logger.summarizer_logs + if message == "[MEMORY:L4] L4 merge applied" + ] + self.assertEqual(len(merge_logs), 1) + detail_text = merge_logs[0] + self.assertIn("UPDATE", detail_text) + self.assertIn("before: user.hardware.main_gpu", detail_text) + self.assertIn("after: user.hardware.main_gpu", detail_text) + self.assertIn("REINFORCE", detail_text) + self.assertIn("confirmation: user.preference.response_language", detail_text) + self.assertIn("existing: user.preference.response_language", detail_text) def test_store_does_not_truncate_values_or_fact_count(self): long_value = "fact " * 1000 @@ -628,7 +829,6 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( "key": "user.hardware.main_gpu", "value": "{gpu_value}", "category": "environment", - "confidence": 0.95, "source_keys": ["gpu"] }}] }}''', @@ -637,7 +837,6 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( "key": "user.preference.response_language", "value": "{language_value}", "category": "user_preference", - "confidence": 0.9, "source_keys": ["language"] }}] }}''', @@ -648,16 +847,14 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( "pending_id": "{gpu_pending_id}", "key": "user.hardware.main_gpu", "value": "{gpu_value}", - "category": "environment", - "confidence": 0.95 + "category": "environment" }}, {{ "action": "create", "pending_id": "{language_pending_id}", "key": "user.preference.response_language", "value": "{language_value}", - "category": "user_preference", - "confidence": 0.9 + "category": "user_preference" }} ] }}''', diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index 165ebb85..5f2fc82a 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -111,6 +111,56 @@ def test_stream_validator_allows_non_consecutive_repeated_sentences(): assert validator.last_failure_reason is None +def test_stream_validator_stops_recurrent_sentence_inside_mixed_loop(): + validator = StreamValidator() + + anchor = "Actually, I'll just do the task.\n" + mixed_blocks = [ + ( + "Wait, I'll do this:\n" + '"I used the wrong tag, so the tool never ran."\n' + "Then the task.\n" + ), + ( + "Let's try to be very precise.\n" + "1. Acknowledge the error.\n" + "2. Perform the task.\n" + ), + ] + + for repeat_index in range( + MAX_REPEAT_SENTENCES + ): + chunk = ( + anchor + + mixed_blocks[ + repeat_index % len(mixed_blocks) + ] + ) + + clean, is_valid = validator.filter_chunk( + chunk + ) + + if repeat_index < MAX_REPEAT_SENTENCES - 1: + assert clean == chunk + assert is_valid + continue + + assert clean == "" + assert not is_valid + + assert validator.last_failure_reason == ( + "Repeated sentence loop detected." + ) + assert validator.last_failure_preview.startswith( + "Actually, I'll just do the task." + ) + assert validator.last_failure_loop_preview == ( + "Actually, I'll just do the task." + ) + + def test_stream_validator_stops_repeated_sentence_sequence_with_markers(): validator = StreamValidator() diff --git a/ui/static/js/chat-response-formatter.js b/ui/static/js/chat-response-formatter.js index 1f249bbf..ac1061dc 100644 --- a/ui/static/js/chat-response-formatter.js +++ b/ui/static/js/chat-response-formatter.js @@ -161,23 +161,23 @@ "$1" ) .replace( - /___((?:(?!___)[^\n])+?)___/g, - "$1" + /(^|[^\p{L}\p{N}_])___((?:(?!___)[^\n])+?)___(?![\p{L}\p{N}_])/gu, + "$1$2" ) .replace( /\*\*((?:(?!\*\*)[^\n])+?)\*\*/g, "$1" ) .replace( - /__((?:(?!__)[^\n])+?)__/g, - "$1" + /(^|[^\p{L}\p{N}_])__((?:(?!__)[^\n])+?)__(?![\p{L}\p{N}_])/gu, + "$1$2" ) .replace( /(^|[^\*])\*([^*\n]+)\*/g, "$1$2" ) .replace( - /(^|[^\w_])_([^_\n]+)_(?![\w_])/g, + /(^|[^\p{L}\p{N}_])_([^_\n]+)_(?![\p{L}\p{N}_])/gu, "$1$2" ); diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index d79222fc..88b8b4a5 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -66,10 +66,6 @@ key, value: factValue, category: normalizeText(value.category || "other") || "other", - confidence: Math.max( - 0, - Math.min(1, normalizeNumber(value.confidence, 1)) - ), mention_count: Math.max( 1, Math.floor(normalizeNumber(value.mention_count, 1)) diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 0a7ede4c..74ef1deb 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -2358,7 +2358,6 @@ [ "id", "category", - "confidence", "mention_count", "source_session_ids", "source_runtime_snapshot_ids", diff --git a/ui/templates/index.html b/ui/templates/index.html index d749aec7..09035b68 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -518,7 +518,7 @@ - + diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 4430ae1a..7fe7f40d 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -153,6 +153,23 @@ def _find_all_runtime_action_matches( ) +def _is_payloadless_jin_color_marker( + raw_marker: str, + payload: str, +) -> bool: + + if str(payload or "").strip(): + return False + + return bool( + re.fullmatch( + r"<\s*JIN_COLOR(?:\s*:\s*)?\s*/?>", + str(raw_marker or "").strip(), + re.IGNORECASE, + ) + ) + + @dataclass(frozen=True) class RuntimeActionCall: name: str @@ -798,7 +815,13 @@ def handle_marker( # optional ``s``/``ms`` suffix is a runtime marker. The suffix is # ignored and the number always means seconds. Plain prose and # malformed candidates such as ```` stay visible. - if normalized_action_name == RUNTIME_ACTION_IDLE: + if ( + normalized_action_name == RUNTIME_ACTION_IDLE + or _is_payloadless_jin_color_marker( + raw_marker, + query, + ) + ): return raw_marker if not preserve_action_text: diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 4d99f5a9..14250ee8 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -44,6 +44,12 @@ MAX_REPEAT_WORD_SEQUENCE_REPETITIONS = 6 MAX_REPEAT_SENTENCES = 5 MAX_SENTENCE_LOOP_SEQUENCE_SIZE = 16 +MAX_RECURRENT_SENTENCE_HISTORY_SIZE = ( + MAX_SENTENCE_LOOP_SEQUENCE_SIZE + * MAX_REPEAT_SENTENCES +) +MIN_RECURRENT_SENTENCE_WORDS = 5 +MIN_RECURRENT_SENTENCE_ALNUM = 20 SENTENCE_HISTORY_SIZE = ( MAX_SENTENCE_LOOP_SEQUENCE_SIZE + 1 @@ -111,6 +117,7 @@ def __init__(self): self.current_sentence_parts = [] self.sentence_history = [] + self.recurrent_sentence_history = [] self.sentence_period_match_counts = [ 0 ] * ( @@ -797,6 +804,104 @@ def normalize_sentence_template( True, ) + @staticmethod + def normalize_recurrent_sentence_key( + sentence: str, + ) -> str: + + normalized = " ".join( + str(sentence or "") + .casefold() + .split() + ).strip(" *_~-\t") + + if not normalized: + return "" + + words = [ + word.strip( + " \t\r\n`*_~\"'.,:;!?()[]{}<>" + ) + for word in normalized.split() + ] + words = [ + word + for word in words + if any( + char.isalpha() + for char in word + ) + ] + + if ( + len(words) + < MIN_RECURRENT_SENTENCE_WORDS + ): + return "" + + if ( + sum( + char.isalnum() + for char in normalized + ) + < MIN_RECURRENT_SENTENCE_ALNUM + ): + return "" + + return normalized + + def validate_recurrent_sentence_loop( + self, + sentence: str, + ) -> bool: + + sentence_key = ( + self.normalize_recurrent_sentence_key( + sentence + ) + ) + + if not sentence_key: + return True + + matching_sentences = [ + history_sentence + for history_sentence in ( + self.recurrent_sentence_history + ) + if ( + self.normalize_recurrent_sentence_key( + history_sentence + ) + == sentence_key + ) + ] + + if ( + len(matching_sentences) + < MAX_REPEAT_SENTENCES + ): + return True + + preview = "\n".join( + sentence.strip() + for sentence in matching_sentences[ + -MAX_REPEAT_SENTENCES: + ] + ) + + self.last_failure_reason = ( + "Repeated sentence loop detected." + ) + self.last_failure_preview = build_preview( + preview + ) + self.last_failure_loop_preview = build_preview( + sentence.strip() + ) + + return False + # ----------------------------------------------------- # VALIDATE SENTENCES # ----------------------------------------------------- @@ -809,6 +914,9 @@ def validate_sentence_sequence_loop( self.sentence_history.append( sentence ) + self.recurrent_sentence_history.append( + sentence + ) if ( len(self.sentence_history) @@ -816,6 +924,12 @@ def validate_sentence_sequence_loop( ): del self.sentence_history[0] + if ( + len(self.recurrent_sentence_history) + > MAX_RECURRENT_SENTENCE_HISTORY_SIZE + ): + del self.recurrent_sentence_history[0] + max_sequence_size = min( MAX_SENTENCE_LOOP_SEQUENCE_SIZE, len(self.sentence_history) - 1, @@ -899,6 +1013,11 @@ def validate_sentence_sequence_loop( return False + if not self.validate_recurrent_sentence_loop( + sentence + ): + return False + return True def validate_sentence( From 936419b4c651d7e3cc2aec1ef0a467e4ca9bf6b1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:17:32 +0300 Subject: [PATCH 011/168] chore: memory rings - render active and delayed memory as a rotating dashed segment with hover glow --- ui/static/css/runtime-avatar.css | 41 ++ ui/static/js/runtime/runtime-avatar.js | 449 +++++++++++++++++++- ui/static/js/runtime/runtime-memory-view.js | 50 +++ ui/static/js/runtime/runtime.js | 183 +++++++- ui/templates/index.html | 8 +- websocket/__init__.py | 9 - 6 files changed, 715 insertions(+), 25 deletions(-) diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 0dc8c287..c50d398b 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -196,6 +196,47 @@ fill-opacity: 1 !important; } +.jin-avatar-memory-dash { + transition: + filter 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), + opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.jin-avatar-memory-dash path { + transition: + stroke-opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), + stroke-width 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.jin-avatar-memory-dash-active { + filter: + brightness(1.16) + drop-shadow(0 0 4px var(--jin-avatar-memory-glow-near, rgba(215, 255, 249, 0.92))) + drop-shadow(0 0 10px var(--jin-avatar-memory-glow-mid, rgba(215, 255, 249, 0.54))); +} + +.jin-avatar-memory-dash-delayed.is-memory-pinned { + filter: + brightness(1.18) + drop-shadow(0 0 4px var(--jin-avatar-memory-glow-near, rgba(239, 255, 255, 0.92))) + drop-shadow(0 0 11px var(--jin-avatar-memory-glow-mid, rgba(239, 255, 255, 0.54))); +} + +.jin-avatar-memory-dash.is-runtime-cited { + filter: + brightness(1.72) + saturate(1.65) + drop-shadow(0 0 5px var(--jin-avatar-memory-glow-near, rgba(190, 252, 255, 0.92))) + drop-shadow(0 0 12px var(--jin-avatar-memory-glow-mid, rgba(52, 211, 222, 0.54))) + drop-shadow(0 0 24px var(--jin-avatar-memory-glow-far, rgba(34, 211, 238, 0.24))) + drop-shadow(0 0 38px var(--jin-avatar-memory-glow-far, rgba(34, 211, 238, 0.18))); +} + +.jin-avatar-memory-dash.is-runtime-cited path { + stroke-opacity: 1 !important; + stroke-width: var(--jin-avatar-memory-hover-width, 2px); +} + .jin-avatar-center-glow-fill, .jin-avatar-center-soft, .jin-avatar-center-core, diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 4c709913..bbc6d0ca 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -8,8 +8,33 @@ const THINK_RUNTIME_CITATION_HOVER_EVENT = "jin:think-runtime-citation-hover"; const CENTER = 180; - const MIN_RING_RADIUS = 48; - const MAX_RING_RADIUS = 160; + const INNER_RING_SCALE = 0.90; + const MIN_RING_RADIUS = 48 * INNER_RING_SCALE; + const MAX_RING_RADIUS = 160 * INNER_RING_SCALE; + const INNER_DECORATION_MAX_RADIUS = 151; + const STATIC_SCAFFOLD_RADII = + [42, 61, 83, 108, 135, 162] + .map(radius => radius * INNER_RING_SCALE); + const STATIC_RADIAL_LINE_INNER_RADIUS = 38 * INNER_RING_SCALE; + const STATIC_RADIAL_LINE_OUTER_RADIUS = 166 * INNER_RING_SCALE; + const MEMORY_RING_LAYOUT = Object.freeze({ + delayed: Object.freeze({ + radius: 158, + strokeWidth: 1.05, + minArcDegrees: 3.2, + maxArcDegrees: 8.8, + arcRatio: 0.42, + startAngle: -6, + }), + active: Object.freeze({ + radius: 169, + strokeWidth: 1.35, + minArcDegrees: 3.8, + maxArcDegrees: 10.8, + arcRatio: 0.48, + startAngle: -9, + }), + }); const SNAPSHOT_GLOW_CLEAR_DELAY_MS = 360; const CENTER_COLOR_STEP_MS = 120; @@ -39,6 +64,9 @@ const DEFAULT_CENTER_COLOR = "#1f4f8f"; const ACCENT_RING_COLOR = "#5be8df"; const AMBER_ACCENT = "#e3a64e"; + const ACTIVE_MEMORY_RING_COLOR = "#d7fff9"; + const DELAYED_MEMORY_RING_COLOR = "#7ab8d8"; + const PINNED_DELAYED_MEMORY_RING_COLOR = "#efffff"; const avatarRoot = document.getElementById("jin-runtime-avatar"); const factCheckTrigger = document.getElementById("fact-check-trigger"); @@ -426,6 +454,376 @@ }; } + function describeArcPath(radius, startAngle, endAngle) { + const start = polarPoint(radius, startAngle); + const end = polarPoint(radius, endAngle); + const arcDegrees = Math.abs(endAngle - startAngle); + + return [ + `M ${start.x.toFixed(3)} ${start.y.toFixed(3)}`, + `A ${radius.toFixed(3)} ${radius.toFixed(3)} 0 ${arcDegrees > 180 ? 1 : 0} 1`, + `${end.x.toFixed(3)} ${end.y.toFixed(3)}`, + ].join(" "); + } + + function getRuntimeApi() { + return window.JinRuntime && window.JinRuntime.runtime + ? window.JinRuntime.runtime + : null; + } + + function getActiveMemoryAvatarRecords() { + const runtime = getRuntimeApi(); + const records = + runtime && typeof runtime.getActiveMemoryRecords === "function" + ? runtime.getActiveMemoryRecords() + : []; + + return (Array.isArray(records) ? records : []) + .map((record, index) => { + const text = String(record || "").trim(); + + if (!text) { + return null; + } + + const parsed = parseRawMemory(text)[0] || { + key: `active_memory_${index + 1}`, + value: text, + }; + const key = + String(parsed.key || `active_memory_${index + 1}`).trim(); + const value = + String(parsed.value || text).trim(); + const lineText = + `${key}: ${value}`.trim(); + + if (!lineText) { + return null; + } + + return { + index, + key, + value, + text: lineText, + citationText: + normalizeRuntimeCitationIdentity(lineText), + }; + }) + .filter(record => record && record.citationText); + } + + function getDelayedMemoryAvatarRecords() { + const runtime = getRuntimeApi(); + const reports = + runtime && typeof runtime.getDelayedMemoryReports === "function" + ? runtime.getDelayedMemoryReports() + : {}; + + if ( + !reports + || typeof reports !== "object" + || Array.isArray(reports) + ) { + return []; + } + + return Object.entries(reports) + .map(([key, report]) => { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return null; + } + + const id = + String(key || "").trim().toLowerCase(); + const title = + String(report.title || "").trim(); + const summary = + String(report.summary || "").trim(); + + if (!id || !title) { + return null; + } + + return { + id, + title, + summary, + pinned: Boolean(report.pinned), + }; + }) + .filter(Boolean) + .sort((left, right) => { + return String(left.id || "").localeCompare( + String(right.id || "") + ); + }); + } + + function appendMemoryDashSegment(parent, layout, options) { + const arcDegrees = + clamp( + options.arcDegrees, + layout.minArcDegrees, + layout.maxArcDegrees + ); + const startAngle = options.angle - arcDegrees / 2; + const endAngle = options.angle + arcDegrees / 2; + const color = options.color; + const glowColor = options.glowColor || color; + const classNames = [ + "jin-avatar-memory-dash", + `jin-avatar-memory-dash-${options.kind}`, + ]; + + if (options.pinned) { + classNames.push("is-memory-pinned"); + } + + const dashGroup = createSvgElement("g", { + class: classNames.join(" "), + "data-runtime-line-key": options.citationKey || null, + "data-runtime-line-text": options.citationText || null, + "data-delayed-memory-id": options.delayedMemoryId || null, + }); + + setMemoryDashGlowVariables( + dashGroup, + glowColor, + layout.strokeWidth + 0.75 + ); + + appendTitle( + dashGroup, + options.title + ); + + dashGroup.appendChild(createSvgElement("path", { + d: describeArcPath(layout.radius, startAngle, endAngle), + fill: "none", + stroke: color, + "stroke-width": layout.strokeWidth, + "stroke-opacity": options.opacity, + "stroke-linecap": "round", + })); + + parent.appendChild(dashGroup); + } + + function setMemoryDashGlowVariables(dashGroup, glowColor, hoverWidth) { + const glowRgb = hexToRgb(glowColor); + + dashGroup.style.setProperty( + "--jin-avatar-memory-glow-near", + `rgba(${glowRgb.r},${glowRgb.g},${glowRgb.b},0.92)` + ); + dashGroup.style.setProperty( + "--jin-avatar-memory-glow-mid", + `rgba(${glowRgb.r},${glowRgb.g},${glowRgb.b},0.54)` + ); + dashGroup.style.setProperty( + "--jin-avatar-memory-glow-far", + `rgba(${glowRgb.r},${glowRgb.g},${glowRgb.b},0.24)` + ); + dashGroup.style.setProperty( + "--jin-avatar-memory-hover-width", + `${Number(hoverWidth || 2).toFixed(2)}px` + ); + } + + function getMemoryRingAnimation(records, kind) { + const seedText = records + .map(record => ( + kind === "active" + ? record.text + : record.id + )) + .join("|"); + const random = + createRandom(`memory-ring:${kind}:${seedText}`); + const baseDuration = + kind === "active" + ? 38 + : 54; + const durationSpread = + kind === "active" + ? 72 + : 112; + + return { + duration: baseDuration + random() * durationSpread, + direction: random() > 0.5 ? "normal" : "reverse", + }; + } + + function appendMemorySignalRing(svg, records, layout, kind, overallColor) { + if (!records.length) { + return; + } + + const animation = + getMemoryRingAnimation(records, kind); + const ring = createSvgElement("g", { + class: [ + "jin-avatar-memory-ring", + `jin-avatar-memory-ring-${kind}`, + "jin-avatar-orbit", + ].join(" "), + fill: "none", + "pointer-events": "none", + style: [ + `--jin-avatar-duration:${animation.duration.toFixed(2)}s`, + `--jin-avatar-direction:${animation.direction}`, + "--jin-avatar-play-state:running", + ].join(";"), + }); + const slotDegrees = 360 / records.length; + const arcDegrees = + clamp( + slotDegrees * layout.arcRatio, + layout.minArcDegrees, + layout.maxArcDegrees + ); + + records.forEach((record, index) => { + const angle = layout.startAngle + slotDegrees * index; + + if (kind === "active") { + const color = + mixColors( + ACTIVE_MEMORY_RING_COLOR, + overallColor, + 0.18 + ); + + appendMemoryDashSegment( + ring, + layout, + { + kind, + angle, + arcDegrees, + color, + glowColor: ACTIVE_MEMORY_RING_COLOR, + opacity: 0.76, + citationText: record.citationText, + title: `Active memory ${index + 1}: ${record.value || record.text}`, + } + ); + return; + } + + const pinned = Boolean(record.pinned); + const color = pinned + ? PINNED_DELAYED_MEMORY_RING_COLOR + : mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); + + appendMemoryDashSegment( + ring, + layout, + { + kind, + angle, + arcDegrees, + color, + glowColor: pinned + ? PINNED_DELAYED_MEMORY_RING_COLOR + : color, + opacity: pinned ? 0.82 : 0.36, + pinned, + citationKey: + normalizeRuntimeCitationIdentity(record.id), + delayedMemoryId: record.id, + title: `${record.title}${record.summary ? `: ${record.summary}` : ""}`, + } + ); + }); + + svg.appendChild(ring); + } + + function setDelayedMemoryDashPinned(reportId, pinned) { + const delayedMemoryId = + String(reportId || "").trim().toLowerCase(); + + if (!/^[a-z0-9]{6}$/.test(delayedMemoryId)) { + return false; + } + + const dashGroup = + avatarRoot.querySelector( + `.jin-avatar-memory-dash-delayed[data-delayed-memory-id="${delayedMemoryId}"]` + ); + + if (!dashGroup) { + return false; + } + + const path = + dashGroup.querySelector("path"); + const overallColor = + avatarRoot.style.getPropertyValue("--jin-avatar-overall-color").trim() + || DEFAULT_RING_COLOR; + const nextPinned = + Boolean(pinned); + const nextColor = + nextPinned + ? PINNED_DELAYED_MEMORY_RING_COLOR + : mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); + + dashGroup.classList.toggle( + "is-memory-pinned", + nextPinned + ); + setMemoryDashGlowVariables( + dashGroup, + nextPinned + ? PINNED_DELAYED_MEMORY_RING_COLOR + : nextColor, + MEMORY_RING_LAYOUT.delayed.strokeWidth + 0.75 + ); + + if (path) { + path.setAttribute( + "stroke", + nextColor + ); + path.setAttribute( + "stroke-opacity", + nextPinned ? "0.82" : "0.36" + ); + } + + return true; + } + + function appendMemorySignalRings( + svg, + activeMemoryRecords, + delayedMemoryRecords, + overallColor + ) { + appendMemorySignalRing( + svg, + delayedMemoryRecords, + MEMORY_RING_LAYOUT.delayed, + "delayed", + overallColor + ); + appendMemorySignalRing( + svg, + activeMemoryRecords, + MEMORY_RING_LAYOUT.active, + "active", + overallColor + ); + } + function appendDefs(svg, overallColor, currentCenterColor) { const defs = createSvgElement("defs"); @@ -534,7 +932,7 @@ fill: "url(#jin-avatar-halo)", })); - [42, 61, 83, 108, 135, 162].forEach((radius, index) => { + STATIC_SCAFFOLD_RADII.forEach((radius, index) => { scaffold.appendChild(createSvgElement("circle", { cx: CENTER, cy: CENTER, @@ -548,8 +946,8 @@ for (let index = 0; index < 16; index += 1) { const angle = index * 22.5 + (random() - 0.5) * 2; - const inner = polarPoint(38, angle); - const outer = polarPoint(166, angle); + const inner = polarPoint(STATIC_RADIAL_LINE_INNER_RADIUS, angle); + const outer = polarPoint(STATIC_RADIAL_LINE_OUTER_RADIUS, angle); scaffold.appendChild(createSvgElement("line", { x1: inner.x, @@ -594,7 +992,10 @@ const ratio = stripeCount <= 1 ? 0 : index / (stripeCount - 1); const angle = startAngle + arcSpan * ratio; const innerRadius = record.radius - 1.5; - const outerRadius = record.radius + stripeHeight * (0.55 + random() * 0.45); + const outerRadius = Math.min( + INNER_DECORATION_MAX_RADIUS, + record.radius + stripeHeight * (0.55 + random() * 0.45) + ); const inner = polarPoint(innerRadius, angle); const outer = polarPoint(outerRadius, angle); @@ -630,7 +1031,12 @@ for (let index = 0; index < nodeCount; index += 1) { const angle = startAngle + index * (15 + random() * 24); - const nodeRadius = record.radius + (random() - 0.5) * 8; + const nodeRadius = + clamp( + record.radius + (random() - 0.5) * 8, + MIN_RING_RADIUS, + INNER_DECORATION_MAX_RADIUS - 6 + ); const point = polarPoint(nodeRadius, angle); const color = PENDING_NODE_PALETTE[index % PENDING_NODE_PALETTE.length]; @@ -891,7 +1297,7 @@ Array.from( svg.querySelectorAll( - ".jin-avatar-orbit[data-runtime-line-key], .jin-avatar-counter-orbit[data-runtime-line-key]" + ".jin-avatar-orbit[data-runtime-line-key], .jin-avatar-counter-orbit[data-runtime-line-key], .jin-avatar-memory-dash" ) ).forEach((orbitGroup) => { const lineKey = @@ -913,12 +1319,19 @@ }); } - function buildRenderSignature(snapshot, lines) { + function buildRenderSignature( + snapshot, + lines, + activeMemoryRecords = [], + delayedMemoryRecords = [] + ) { return [ snapshot && snapshot.runtime_memory_id, snapshot && snapshot.index, snapshot && snapshot.total_diff, lines.map(line => [line.key, line.value, line.status, line.key_status, line.value_status].join("␟")).join("␞"), + activeMemoryRecords.map(record => record.text).join("␞"), + delayedMemoryRecords.map(record => [record.id, record.title, record.summary, record.pinned].join("␟")).join("␞"), ].join("␝"); } @@ -937,13 +1350,21 @@ ].join(":"); const random = createRandom(seed || "jin-avatar"); const records = lines.length ? computeRingRecords(lines, seed || "jin-avatar") : []; + const activeMemoryRecords = getActiveMemoryAvatarRecords(); + const delayedMemoryRecords = getDelayedMemoryAvatarRecords(); const overallColor = lines.length ? computeOverallColor(lines, records) : DEFAULT_RING_COLOR; const diffPercent = lines.length ? getSnapshotDiff(snapshot, lines) : 0; - const signature = buildRenderSignature(snapshot, lines) + `:${options.seedNonce || avatarRefreshNonce}`; + const signature = + buildRenderSignature( + snapshot, + lines, + activeMemoryRecords, + delayedMemoryRecords + ) + `:${options.seedNonce || avatarRefreshNonce}`; const shouldAnimate = Boolean(lastRenderSignature) && signature !== lastRenderSignature; const svg = createSvgElement("svg", { @@ -963,6 +1384,13 @@ }); }); + appendMemorySignalRings( + svg, + activeMemoryRecords, + delayedMemoryRecords, + overallColor + ); + appendCenter(svg, overallColor, centerColor); avatarRoot.replaceChildren(svg); @@ -1281,6 +1709,7 @@ render: renderAvatar, refresh: reinitializeAvatar, setCenterColor, + setDelayedMemoryPinned: setDelayedMemoryDashPinned, get aggressivePalette() { return AGGRESSIVE_PALETTE; }, diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 74ef1deb..bfa2159f 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -23,6 +23,7 @@ const MEMORY_DELETE_HOLD_MS = 1500; const THINK_RUNTIME_CITATION_HOVER_EVENT = "jin:think-runtime-citation-hover"; const RUNTIME_MEMORY_LINE_HOVER_SOURCE_ID = "runtime-memory-line-hover"; + const DELAYED_MEMORY_ROW_HOVER_SOURCE_ID = "delayed-memory-row-hover"; const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; @@ -1047,6 +1048,37 @@ ); } + function dispatchDelayedMemoryAvatarHover( + report, + active + ) { + const memoryId = + normalizeRuntimeCitationIdentity( + report && report._storage_key + ); + + window.dispatchEvent( + new CustomEvent( + THINK_RUNTIME_CITATION_HOVER_EVENT, + { + detail: active && memoryId + ? { + active: true, + sourceId: `${DELAYED_MEMORY_ROW_HOVER_SOURCE_ID}:${memoryId}`, + lineKeys: [memoryId], + lineTexts: [], + } + : { + active: false, + sourceId: `${DELAYED_MEMORY_ROW_HOVER_SOURCE_ID}:${memoryId || "none"}`, + lineKeys: [], + lineTexts: [], + }, + } + ) + ); + } + function renderRuntimeMemoryLines( snapshot, persistGlow = false, @@ -1746,6 +1778,10 @@ `${title}: ${summary}`.trim(); valueSpan.title = row.title; + row.dataset.delayedMemoryId = + normalizeRuntimeCitationIdentity( + report._storage_key + ); row.appendChild( keySpan @@ -1760,6 +1796,20 @@ ); }); + row.addEventListener("mouseenter", () => { + dispatchDelayedMemoryAvatarHover( + report, + true + ); + }); + + row.addEventListener("mouseleave", () => { + dispatchDelayedMemoryAvatarHover( + report, + false + ); + }); + row.addEventListener("keydown", (event) => { if ( event.key !== "Enter" diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 0b4d4e4e..678c3b67 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -120,6 +120,7 @@ const { clearActiveMemoryRecords, appendActiveMemoryRecords: appendStoredActiveMemoryRecords, removeActiveMemoryRecordById: removeStoredActiveMemoryRecordById, + normalizeDelayedMemoryReports, readDelayedMemoryReports, writeDelayedMemoryReports, appendDelayedMemoryReports: appendStoredDelayedMemoryReports, @@ -563,7 +564,7 @@ memoryView.init({ memoryModel, buildDisplaySnapshot: buildRuntimeMemoryDisplaySnapshot, getActiveMemoryRecords: readActiveMemoryRecords, - setActiveMemoryRecords: writeActiveMemoryRecords, + setActiveMemoryRecords: writeActiveMemoryRecordsAndRefresh, deleteRuntimeMemoryLine: deleteRuntimeMemoryLineAndRender, getDelayedMemoryReports: readDelayedMemoryReports, setDelayedMemoryReportPinned, @@ -589,6 +590,51 @@ function renderRuntimeMemorySnapshot() { memoryView.renderRuntimeMemorySnapshot(); } +function refreshRuntimeAvatar() { + const avatar = + window.JinRuntime + && window.JinRuntime.avatar; + + if ( + avatar + && typeof avatar.refresh === "function" + ) { + avatar.refresh(); + } +} + +function setDelayedMemoryPinnedOnAvatar( + reportId, + pinned +) { + const avatar = + window.JinRuntime + && window.JinRuntime.avatar; + + if ( + avatar + && typeof avatar.setDelayedMemoryPinned === "function" + ) { + return avatar.setDelayedMemoryPinned( + reportId, + pinned + ); + } + + return false; +} + +function writeActiveMemoryRecordsAndRefresh( + records +) { + writeActiveMemoryRecords( + records + ); + refreshRuntimeAvatar(); + + return readActiveMemoryRecords(); +} + function showLatestRuntimeMemorySnapshot() { if ( memoryView @@ -745,6 +791,7 @@ function appendActiveMemoryRecordsAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); return nextRecords; @@ -761,6 +808,7 @@ function replaceActiveMemoryRecordsAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); return readActiveMemoryRecords(); @@ -778,6 +826,7 @@ function removeActiveMemoryRecordByIdAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); return nextRecords; @@ -819,6 +868,15 @@ function setDelayedMemoryReportPinned( ); renderRuntimeMemorySnapshot(); + if ( + !setDelayedMemoryPinnedOnAvatar( + normalizedId, + pinned + ) + ) { + refreshRuntimeAvatar(); + } + if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { window.syncDelayedMemoryReportsToRuntime(); } @@ -838,11 +896,131 @@ function appendDelayedMemoryReports( ); renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); return nextReports; } +function buildDelayedMemoryReportsSignature( + reports +) { + + const normalizedReports = + normalizeDelayedMemoryReports( + reports + ); + + return JSON.stringify( + Object.keys(normalizedReports) + .sort() + .map( + reportId => [ + reportId, + normalizedReports[reportId], + ] + ) + ); + +} + +function buildDelayedMemoryAvatarLayoutSignature( + reports +) { + + const normalizedReports = + normalizeDelayedMemoryReports( + reports + ); + + return JSON.stringify( + Object.keys(normalizedReports) + .sort() + .map( + reportId => { + const report = + normalizedReports[reportId]; + + return [ + reportId, + report.title, + report.summary, + ]; + } + ) + ); + +} + +function syncDelayedMemoryPinsToAvatar( + reports +) { + + const normalizedReports = + normalizeDelayedMemoryReports( + reports + ); + let synced = true; + + Object.entries(normalizedReports).forEach( + ([reportId, report]) => { + if ( + !setDelayedMemoryPinnedOnAvatar( + reportId, + Boolean(report && report.pinned) + ) + ) { + synced = false; + } + } + ); + + return synced; + +} + +function replaceDelayedMemoryReportsAndRender( + reports +) { + const currentReports = + readDelayedMemoryReports(); + const nextReports = + normalizeDelayedMemoryReports( + reports + ); + const currentAvatarLayoutSignature = + buildDelayedMemoryAvatarLayoutSignature( + currentReports + ); + const nextAvatarLayoutSignature = + buildDelayedMemoryAvatarLayoutSignature( + nextReports + ); + + if ( + buildDelayedMemoryReportsSignature(currentReports) + === buildDelayedMemoryReportsSignature(nextReports) + ) { + return currentReports; + } + + writeDelayedMemoryReports( + nextReports + ); + renderRuntimeMemorySnapshot(); + + if ( + currentAvatarLayoutSignature === nextAvatarLayoutSignature + && syncDelayedMemoryPinsToAvatar(nextReports) + ) { + return readDelayedMemoryReports(); + } + + refreshRuntimeAvatar(); + + return readDelayedMemoryReports(); +} + const ACTIVE_MEMORY_RUNTIME_ACTIONS_TO_SILENCE_ON_L1 = [ "save_active_memory", "resolve_active_memory", @@ -1055,6 +1233,7 @@ window.JinRuntime.runtime = { clearActiveMemoryRecords(); renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); return records; }, @@ -1075,7 +1254,7 @@ window.JinRuntime.runtime = { ? l4Memory.requestFactDelete(factId) : false; }, - replaceDelayedMemoryReports: writeDelayedMemoryReports, + replaceDelayedMemoryReports: replaceDelayedMemoryReportsAndRender, appendDelayedMemoryReports, }; diff --git a/ui/templates/index.html b/ui/templates/index.html index 09035b68..f0b76432 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -17,7 +17,7 @@ - + @@ -506,9 +506,9 @@ - - - + + + diff --git a/websocket/__init__.py b/websocket/__init__.py index f6d85c00..bc4b8746 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -277,15 +277,6 @@ async def process_pending_requests(): file_errors = persist_delayed_memory_reports( reports ) - report_count = len( - reports - ) - await logger.log_system( - ( - "[WS] delayed memory store synced " - f"({report_count} reports)" - ) - ) for file_error in file_errors: await logger.log_system( "[DELAYED MEMORY] local file save failed: " From 35378a2e6832573b2e7ef91e5ba2f8aa344c93e1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:34:28 +0300 Subject: [PATCH 012/168] feature: L4 memory layer - persist deleted fact tombstones and prevent stale snapshots from restoring facts - restore deleted L4 facts directly from logger cards - group extraction and merge request/response traces with structured skip diagnostics - prune processed pending facts and block browser sync during active L4 updates - highlight memory records and avatar rings referenced in JIN responses - resolve delayed memory previews against the latest stored report state --- runtime/L4_memory.py | 211 ++++++- runtime/L4_memory_utils.py | 143 ++++- tests/test_chat_runtime_marker_ui.py | 2 +- tests/test_l4_logger_cards_client_contract.py | 68 +++ tests/test_l4_memory.py | 336 +++++++++++ ...t_memory_reference_sync_client_contract.py | 161 +++++ tests/test_websocket_logger_model_output.py | 15 + ui/static/css/runtime-avatar.css | 20 +- ui/static/css/runtime-memory.css | 23 + ui/static/css/theme-win95.css | 11 + ui/static/js/chat-attachments.js | 14 +- ui/static/js/chat.js | 112 ++++ ui/static/js/logger/log-entries.js | 567 ++++++++++++++++++ ui/static/js/logger/trace-modal.js | 235 ++++++++ ui/static/js/runtime/runtime-avatar.js | 142 +++++ ui/static/js/runtime/runtime-l4-memory.js | 15 + ui/static/js/runtime/runtime-memory-view.js | 398 +++++++++++- ui/static/js/runtime/runtime.js | 7 +- ui/static/js/socket/event-handlers.js | 17 + ui/templates/index.html | 20 +- websocket/__init__.py | 29 +- websocket/logger.py | 7 +- 22 files changed, 2486 insertions(+), 67 deletions(-) create mode 100644 tests/test_l4_logger_cards_client_contract.py create mode 100644 tests/test_memory_reference_sync_client_contract.py diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 626b9e53..a75a6c27 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -25,6 +25,7 @@ normalize_l4_candidates, normalize_l4_merge_operations, normalize_l4_store, + restore_l4_fact_to_store, ) from utils.actions.save_delayed_memory_utils import ( collect_long_term_fact_ids_from_reports, @@ -234,6 +235,11 @@ def apply_l4_memory_store_sync(context, raw_store) -> bool: return changed +def runtime_l4_memory_update_running(context) -> bool: + task = getattr(context, "runtime_l4_memory_update_task", None) + return task is not None and not task.done() + + async def emit_facts_memory_store_update(context) -> None: emit = getattr(getattr(context, "emitter", None), "emit", None) await safe_call( @@ -309,6 +315,39 @@ async def ask_l4_model( return response +async def log_l4_skip_event( + context, + *, + phase: str, + message_phase: str, + reason: str, + details: dict | None = None, +) -> dict: + result = { + "phase": phase, + "status": "skipped", + "reason": reason, + } + + if details: + result.update(details) + + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message=f"L4 {message_phase} skipped: {reason}", + details=json.dumps( + result, + ensure_ascii=False, + indent=2, + ), + fallback_channel="summarizer", + event=f"{phase}_skipped", + ) + + return result + + async def run_l4_extraction_phase( *, context, @@ -327,22 +366,56 @@ async def run_l4_extraction_phase( ) if is_runtime_memory_response_truncated(response): - return {"phase": "extract", "status": "skipped", "reason": "response_truncated"} + return await log_l4_skip_event( + context, + phase="extract", + message_phase="extraction", + reason="response_truncated", + details={ + "selected_fields_count": len(pending_fields), + }, + ) payload = extract_l4_json_payload(extract_runtime_memory_text(response)) if payload is None: - return {"phase": "extract", "status": "skipped", "reason": "invalid_json"} + return await log_l4_skip_event( + context, + phase="extract", + message_phase="extraction", + reason="invalid_json", + details={ + "selected_fields_count": len(pending_fields), + }, + ) raw_candidates = payload.get("facts") if not isinstance(raw_candidates, list): - return {"phase": "extract", "status": "skipped", "reason": "invalid_facts_payload"} + return await log_l4_skip_event( + context, + phase="extract", + message_phase="extraction", + reason="invalid_facts_payload", + details={ + "selected_fields_count": len(pending_fields), + }, + ) candidates = normalize_l4_candidates( payload, source_fields=pending_fields, ) if len(candidates) != len(raw_candidates): - return {"phase": "extract", "status": "skipped", "reason": "invalid_candidates"} + return await log_l4_skip_event( + context, + phase="extract", + message_phase="extraction", + reason="invalid_candidates", + details={ + "selected_fields_count": len(pending_fields), + "raw_candidates_count": len(raw_candidates), + "valid_candidates_count": len(candidates), + }, + ) store, pending_change = add_l4_pending_candidates( ensure_runtime_l4_state(context), candidates, @@ -394,24 +467,75 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: ) if is_runtime_memory_response_truncated(response): - return {"phase": "merge", "status": "skipped", "reason": "response_truncated"} + return await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason="response_truncated", + details={ + "pending_count": len(pending_facts), + "pending_ids": [ + fact.get("id") + for fact in pending_facts + if fact.get("id") + ], + }, + ) payload = extract_l4_json_payload(extract_runtime_memory_text(response)) if payload is None: - return {"phase": "merge", "status": "skipped", "reason": "invalid_json"} + return await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason="invalid_json", + details={ + "pending_count": len(pending_facts), + "pending_ids": [ + fact.get("id") + for fact in pending_facts + if fact.get("id") + ], + }, + ) current_store = clone_l4_store(ensure_runtime_l4_state(context)) if current_store != base_store: - return {"phase": "merge", "status": "skipped", "reason": "store_changed_during_merge"} + return await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason="store_changed_during_merge", + details={ + "base_revision": base_store.get("revision"), + "current_revision": current_store.get("revision"), + "pending_count": len(pending_facts), + "pending_ids": [ + fact.get("id") + for fact in pending_facts + if fact.get("id") + ], + }, + ) operations = normalize_l4_merge_operations(payload) next_store, merge_change = apply_l4_merge_operations(base_store, operations) if not merge_change.get("valid"): - return { - "phase": "merge", - "status": "skipped", - "reason": merge_change.get("reason") or "invalid_operations", - } + return await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason=merge_change.get("reason") or "invalid_operations", + details={ + "pending_count": len(pending_facts), + "operations_count": len(operations), + "pending_ids": [ + fact.get("id") + for fact in pending_facts + if fact.get("id") + ], + }, + ) context.runtime_long_term_memory_store = next_store persist_runtime_l4_file_store( @@ -509,9 +633,8 @@ def schedule_l4_memory_idle_update( if not l4_memory_enabled(): return None - previous_task = getattr(context, "runtime_l4_memory_update_task", None) - if previous_task is not None and not previous_task.done(): - return previous_task + if runtime_l4_memory_update_running(context): + return getattr(context, "runtime_l4_memory_update_task", None) task = asyncio.create_task( maybe_update_runtime_l4_memory( @@ -591,11 +714,21 @@ def build_runtime_l4_memory_context(*, context) -> str: async def delete_l4_memory_fact(context, fact_id: str) -> bool: + current_store = ensure_runtime_l4_state(context) + target_id = str(fact_id or "").strip() + deleted_fact = next( + ( + dict(fact) + for fact in current_store.get("facts") or [] + if str(fact.get("id") or "").strip() == target_id + ), + None, + ) store, changed = delete_l4_fact_from_store( - ensure_runtime_l4_state(context), - fact_id, + current_store, + target_id, ) - if not changed: + if not changed or deleted_fact is None: return False context.runtime_long_term_memory_store = store @@ -609,16 +742,54 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: message="L4 fact deleted", details=json.dumps( { - "fact_id": fact_id, + "fact": deleted_fact, "revision": store.get("revision"), "total_facts": len(store.get("facts") or []), }, ensure_ascii=False, indent=2, ), + event="fact_deleted", + tag_suffix="DELETED", + deleted_fact=deleted_fact, ) await emit_l4_memory_update( context, - change={"removed_ids": [fact_id], "changed": True}, + change={"removed_ids": [target_id], "changed": True}, + ) + return True + + +async def restore_l4_memory_fact(context, fact) -> bool: + store, changed = restore_l4_fact_to_store( + ensure_runtime_l4_state(context), + fact, + ) + if not changed: + return False + + restored_fact = next( + ( + dict(item) + for item in store.get("facts") or [] + if str(item.get("id") or "").strip() + == str((fact or {}).get("id") or "").strip() + ), + None, + ) + if restored_fact is None: + return False + + context.runtime_long_term_memory_store = store + persist_runtime_l4_file_store( + context, + store, + ) + await emit_l4_memory_update( + context, + change={ + "restored_ids": [restored_fact["id"]], + "changed": True, + }, ) return True diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 5d27a23d..e415638f 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -131,6 +131,14 @@ def merge_l4_string_lists(*values) -> list[str]: return result +def normalize_l4_deleted_fact_ids(value) -> list[str]: + return [ + fact_id + for fact_id in normalize_l4_string_list(value) + if fact_id.startswith(L4_FACT_ID_PREFIX) + ] + + def build_l4_content_hash(content: str) -> str: return hashlib.sha256( normalize_l4_text(content).encode("utf-8") @@ -425,6 +433,38 @@ def merge_l4_snapshot_fact_lists( return facts, facts != original +def collect_l4_processed_pending_fact_ids(facts) -> set[str]: + processed_ids = set() + + for fact in facts if isinstance(facts, list) else []: + if not isinstance(fact, dict): + continue + + for source_fact_id in normalize_l4_string_list( + fact.get("source_fact_ids") + ): + if source_fact_id.startswith(L4_PENDING_FACT_ID_PREFIX): + processed_ids.add(source_fact_id) + + return processed_ids + + +def prune_l4_processed_pending_facts( + *, + facts, + pending_facts, +) -> list[dict]: + processed_ids = collect_l4_processed_pending_fact_ids(facts) + if not processed_ids: + return pending_facts + + return [ + fact + for fact in pending_facts + if normalize_l4_text(fact.get("id")) not in processed_ids + ] + + def merge_l4_store_snapshots( primary_store, incoming_store, @@ -442,27 +482,47 @@ def merge_l4_store_snapshots( now=current_time, ) - facts, facts_changed = merge_l4_snapshot_fact_lists( - primary.get("facts"), - incoming.get("facts"), + deleted_fact_ids = merge_l4_string_lists( + primary.get("deleted_fact_ids"), + incoming.get("deleted_fact_ids"), + ) + deleted_fact_id_set = set(deleted_fact_ids) + + facts, _facts_changed = merge_l4_snapshot_fact_lists( + [ + fact + for fact in primary.get("facts") or [] + if fact.get("id") not in deleted_fact_id_set + ], + [ + fact + for fact in incoming.get("facts") or [] + if fact.get("id") not in deleted_fact_id_set + ], pending=False, now=current_time, ) - pending_facts, pending_changed = merge_l4_snapshot_fact_lists( + pending_facts, _pending_changed = merge_l4_snapshot_fact_lists( primary.get("pending_facts"), incoming.get("pending_facts"), pending=True, now=current_time, ) + pending_facts = prune_l4_processed_pending_facts( + facts=facts, + pending_facts=pending_facts, + ) changed = bool( - facts_changed - or pending_changed + facts != primary.get("facts") + or pending_facts != primary.get("pending_facts") + or deleted_fact_ids != primary.get("deleted_fact_ids") ) merged = { **primary, "facts": facts, "pending_facts": pending_facts, + "deleted_fact_ids": deleted_fact_ids, } if changed: @@ -476,6 +536,7 @@ def merge_l4_store_snapshots( "changed": changed, "facts_count": len(facts), "pending_count": len(pending_facts), + "deleted_count": len(deleted_fact_ids), } @@ -486,6 +547,7 @@ def empty_l4_store(*, now: str | None = None) -> dict: "updated_at": now or "", "facts": [], "pending_facts": [], + "deleted_fact_ids": [], } @@ -505,17 +567,36 @@ def normalize_l4_store(value, *, now: str | None = None) -> dict: if isinstance(value.get("pending_facts"), list) else [] ) + deleted_fact_ids = normalize_l4_deleted_fact_ids( + value.get("deleted_fact_ids") + ) + deleted_fact_id_set = set(deleted_fact_ids) - return { - "version": L4_STORE_VERSION, - "revision": revision, - "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, - "facts": deduplicate_l4_facts(facts, pending=False, now=current_time), - "pending_facts": deduplicate_l4_facts( + facts = [ + fact + for fact in deduplicate_l4_facts( + facts, + pending=False, + now=current_time, + ) + if fact.get("id") not in deleted_fact_id_set + ] + pending_facts = prune_l4_processed_pending_facts( + facts=facts, + pending_facts=deduplicate_l4_facts( pending, pending=True, now=current_time, ), + ) + + return { + "version": L4_STORE_VERSION, + "revision": revision, + "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, + "facts": facts, + "pending_facts": pending_facts, + "deleted_fact_ids": deleted_fact_ids, } @@ -1072,6 +1153,40 @@ def apply_l4_merge_operations( } +def restore_l4_fact_to_store( + store, + fact, + *, + now: str | None = None, +) -> tuple[dict, bool]: + current_time = now or utc_now_iso() + next_store = normalize_l4_store(store, now=current_time) + restored = normalize_l4_fact( + fact, + pending=False, + now=current_time, + ) + if restored is None: + return next_store, False + + if any( + existing.get("id") == restored["id"] + or existing.get("key") == restored["key"] + for existing in next_store["facts"] + ): + return next_store, False + + next_store["deleted_fact_ids"] = [ + deleted_id + for deleted_id in next_store.get("deleted_fact_ids") or [] + if deleted_id != restored["id"] + ] + next_store["facts"].append(restored) + next_store["revision"] += 1 + next_store["updated_at"] = current_time + return next_store, True + + def delete_l4_fact_from_store( store, fact_id: str, @@ -1086,6 +1201,10 @@ def delete_l4_fact_from_store( return next_store, False next_store["facts"] = remaining + next_store["deleted_fact_ids"] = merge_l4_string_lists( + next_store.get("deleted_fact_ids"), + [target_id], + ) next_store["revision"] += 1 next_store["updated_at"] = current_time return next_store, True diff --git a/tests/test_chat_runtime_marker_ui.py b/tests/test_chat_runtime_marker_ui.py index 099e2164..9b1c9ac5 100644 --- a/tests/test_chat_runtime_marker_ui.py +++ b/tests/test_chat_runtime_marker_ui.py @@ -79,7 +79,7 @@ def test_chat_script_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/js/chat.js?v=chat-core-5', + '/static/js/chat.js?v=chat-core-6', source, ) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py new file mode 100644 index 00000000..a93dd682 --- /dev/null +++ b/tests/test_l4_logger_cards_client_contract.py @@ -0,0 +1,68 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +LOG_ENTRIES_JS = ROOT / "ui" / "static" / "js" / "logger" / "log-entries.js" +TRACE_MODAL_JS = ROOT / "ui" / "static" / "js" / "logger" / "trace-modal.js" +RUNTIME_L4_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-l4-memory.js" +SOCKET_EVENTS_JS = ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class L4LoggerCardsClientContractTests(unittest.TestCase): + + def test_deleted_fact_card_exposes_full_payload_and_restore(self): + source = LOG_ENTRIES_JS.read_text(encoding="utf-8") + + self.assertIn('"[MEMORY:L4:DELETED]"', source) + self.assertIn('kind: "l4_fact"', source) + self.assertIn('"payload"', source) + self.assertIn('"restore"', source) + self.assertIn('api.requestFactRestore(', source) + self.assertIn('window.handleL4MemoryRestoreResult', source) + + def test_extraction_and_merge_request_response_cards_are_paired(self): + source = LOG_ENTRIES_JS.read_text(encoding="utf-8") + + self.assertIn('`[MEMORY:L4:${phaseLabel}]`', source) + self.assertIn('"summarizer_request"', source) + self.assertIn('"summarizer_result"', source) + self.assertIn('"request"', source) + self.assertIn('"response"', source) + self.assertIn('tone === "muted"', source) + self.assertIn('kind: "l4_summarizer_response"', source) + self.assertIn('responseReady: false', source) + self.assertIn('if (!state.responseReady)', source) + self.assertIn('.find((candidate) => !candidate.responseReady)', source) + self.assertNotIn('.find((candidate) => !candidate.responseDetails)', source) + + def test_l4_response_modal_uses_structured_layout_and_no_changes_state(self): + source = TRACE_MODAL_JS.read_text(encoding="utf-8") + + self.assertIn('parsed.kind === "l4_fact"', source) + self.assertIn('parsed.kind === "l4_summarizer_response"', source) + self.assertIn('"No changes"', source) + self.assertIn('Array.isArray(payload.facts)', source) + self.assertIn('Array.isArray(payload.operations)', source) + + def test_restore_message_round_trip_is_registered(self): + runtime_source = RUNTIME_L4_JS.read_text(encoding="utf-8") + socket_source = SOCKET_EVENTS_JS.read_text(encoding="utf-8") + + self.assertIn('type: "l4_memory_restore_fact"', runtime_source) + self.assertIn('requestFactRestore,', runtime_source) + self.assertIn('"l4_memory_restore_result"', socket_source) + + def test_cache_versions_are_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn('/static/css/runtime-memory.css?v=l4-logger-cards-1', source) + self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-restore-1', source) + self.assertIn('/static/js/logger/trace-modal.js?v=l4-structured-trace-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=l4-logger-cards-2', source) + self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-1', source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 7e52cfde..a9cce05a 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1,3 +1,4 @@ +import asyncio import json import tempfile import unittest @@ -5,8 +6,11 @@ from runtime.L4_memory import ( apply_l4_memory_store_sync, build_runtime_l4_memory_context, + delete_l4_memory_fact, ensure_runtime_l4_state, maybe_update_runtime_l4_memory, + restore_l4_memory_fact, + runtime_l4_memory_update_running, ) from runtime.L4_memory_utils import ( add_l4_pending_candidates, @@ -23,6 +27,7 @@ normalize_l4_candidates, normalize_l4_merge_operations, normalize_l4_store, + restore_l4_fact_to_store, ) from runtime.runtime_context import RuntimeContext from rules.brain_context_builder import build_brain_context @@ -46,6 +51,28 @@ async def emit(self, payload): self.events.append(payload) +class CaptureMemoryLogger: + + def __init__(self): + self.logs = [] + + async def log_memory( + self, + level, + message, + details=None, + event=None, + **extra, + ): + self.logs.append({ + "level": level, + "message": message, + "details": details, + "event": event, + **extra, + }) + + class L4MemoryTests(unittest.IsolatedAsyncioTestCase): def test_facts_memory_normalization_adds_session_and_pending_status(self): @@ -211,6 +238,76 @@ def test_file_store_fallback_survives_empty_browser_sync(self): self.assertEqual(len(stored["facts"]), 1) self.assertEqual(stored["facts"][0]["key"], "user.identity") + async def test_deleted_fact_survives_server_restart_and_stale_profile_sync(self): + with tempfile.TemporaryDirectory() as directory: + stale_browser_store = normalize_l4_store({ + "revision": 154, + "updated_at": "2026-08-05T11:48:48Z", + "facts": [ + { + "id": "l4_secret", + "key": "project.secret_number_73", + "value": "The number 73 was established.", + }, + ], + }) + persist_long_term_facts_store( + stale_browser_store, + root=directory, + ) + + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=CaptureMemoryLogger(), + clients={}, + ) + context.runtime_l4_file_store_enabled = True + context.runtime_l4_file_store_root = directory + ensure_runtime_l4_state(context) + + deleted = await delete_l4_memory_fact( + context, + "l4_secret", + ) + self.assertTrue(deleted) + + persisted_after_delete, warnings = load_long_term_facts_store( + root=directory, + ) + self.assertEqual(warnings, []) + self.assertEqual(persisted_after_delete["facts"], []) + self.assertEqual( + persisted_after_delete["deleted_fact_ids"], + ["l4_secret"], + ) + + restarted_context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=CaptureMemoryLogger(), + clients={}, + ) + restarted_context.runtime_l4_file_store_enabled = True + restarted_context.runtime_l4_file_store_root = directory + ensure_runtime_l4_state(restarted_context) + + changed = apply_l4_memory_store_sync( + restarted_context, + stale_browser_store, + ) + persisted_after_sync, warnings = load_long_term_facts_store( + root=directory, + ) + + self.assertFalse(changed) + self.assertEqual(warnings, []) + self.assertEqual(persisted_after_sync["facts"], []) + self.assertEqual( + persisted_after_sync["deleted_fact_ids"], + ["l4_secret"], + ) + def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): merged, change = merge_l4_store_snapshots( { @@ -255,6 +352,82 @@ def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): ["l4_incoming"], ) + def test_store_normalization_prunes_processed_pending_fact(self): + processed_pending_id = "l4p_fd9b07b15788" + waiting_pending_id = "l4p_waiting" + + store = normalize_l4_store( + { + "facts": [ + { + "id": "l4_32df84cf695d", + "key": "jin_structural_awareness", + "value": "JIN tracks structural awareness.", + "category": "other", + "source_fact_ids": [processed_pending_id], + }, + ], + "pending_facts": [ + { + "id": processed_pending_id, + "key": "system.identity_definition", + "value": "JIN defines itself through structure.", + "category": "other", + }, + { + "id": waiting_pending_id, + "key": "project.next_step", + "value": "Keep reviewing pending facts.", + "category": "other", + }, + ], + }, + now="2026-08-04T17:34:05Z", + ) + + self.assertEqual( + [fact["id"] for fact in store["pending_facts"]], + [waiting_pending_id], + ) + + def test_store_merge_does_not_resurrect_processed_pending_fact(self): + processed_pending_id = "l4p_fd9b07b15788" + + merged, change = merge_l4_store_snapshots( + { + "revision": 94, + "updated_at": "2026-08-04T17:34:05Z", + "facts": [ + { + "id": "l4_32df84cf695d", + "key": "jin_structural_awareness", + "value": "JIN tracks structural awareness.", + "category": "other", + "source_fact_ids": [processed_pending_id], + }, + ], + "pending_facts": [], + }, + { + "revision": 94, + "updated_at": "2026-08-04T17:34:05Z", + "facts": [], + "pending_facts": [ + { + "id": processed_pending_id, + "key": "system.identity_definition", + "value": "JIN defines itself through structure.", + "category": "other", + }, + ], + }, + now="2026-08-04T17:35:00Z", + ) + + self.assertFalse(change["changed"]) + self.assertEqual(merged["revision"], 94) + self.assertEqual(merged["pending_facts"], []) + def test_pending_candidates_accumulate_without_touching_final_memory(self): store = normalize_l4_store({ "facts": [ @@ -552,6 +725,86 @@ async def test_merge_phase_logs_concrete_operation_details(self): self.assertIn("confirmation: user.preference.response_language", detail_text) self.assertIn("existing: user.preference.response_language", detail_text) + async def test_merge_phase_logs_skip_reason(self): + store, _ = add_l4_pending_candidates( + normalize_l4_store({}), + [ + { + "key": "system.identity_definition", + "value": ( + "JIN defines its existence through structural " + "awareness." + ), + "category": "other", + }, + ], + now="2026-08-02T12:00:00Z", + ) + service_client = FakeServiceClient( + '{"operations": []}' + ) + logger = FakeLogger() + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=logger, + clients={"service": service_client}, + ) + context.runtime_long_term_memory_store = store + + result = await maybe_update_runtime_l4_memory( + context=context, + user_idle_seconds=61, + ) + + self.assertEqual(result["phase"], "merge") + self.assertEqual(result["status"], "skipped") + self.assertEqual(result["reason"], "operation_count_mismatch") + skip_logs = [ + details + for message, details in logger.summarizer_logs + if message == "[MEMORY:L4] L4 merge skipped: operation_count_mismatch" + ] + self.assertEqual(len(skip_logs), 1) + skip_details = json.loads(skip_logs[0]) + self.assertEqual(skip_details["phase"], "merge") + self.assertEqual(skip_details["reason"], "operation_count_mismatch") + self.assertEqual(skip_details["pending_count"], 1) + self.assertEqual(skip_details["operations_count"], 0) + self.assertEqual( + skip_details["pending_ids"], + [ + store["pending_facts"][0]["id"], + ], + ) + + async def test_runtime_l4_memory_update_running_tracks_active_task(self): + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={}, + ) + + self.assertFalse( + runtime_l4_memory_update_running(context) + ) + + task = asyncio.create_task( + asyncio.sleep(0) + ) + context.runtime_l4_memory_update_task = task + + self.assertTrue( + runtime_l4_memory_update_running(context) + ) + + await task + + self.assertFalse( + runtime_l4_memory_update_running(context) + ) + def test_store_does_not_truncate_values_or_fact_count(self): long_value = "fact " * 1000 raw_facts = [ @@ -921,6 +1174,89 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( self.assertIn("cross-session long-term memory", service_client.calls[0]["system_prompt"]) self.assertIn("consolidate pending candidates", service_client.calls[2]["system_prompt"].lower()) + def test_restore_l4_fact_preserves_deleted_fact_object(self): + fact = normalize_l4_store({ + "facts": [{ + "id": "l4_restore_me", + "key": "project.restore.test", + "value": "Restore the complete fact.", + "category": "project", + "mention_count": 3, + "source_session_ids": ["session-a"], + }], + })["facts"][0] + + restored_store, changed = restore_l4_fact_to_store( + {"facts": []}, + fact, + now="2026-08-05T10:00:00Z", + ) + + self.assertTrue(changed) + self.assertEqual(restored_store["facts"], [fact]) + self.assertEqual(restored_store["revision"], 1) + + async def test_delete_logs_complete_fact_and_restore_adds_it_back(self): + logger = CaptureMemoryLogger() + emitter = FakeEmitter() + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=logger, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [{ + "id": "l4_restore_me", + "key": "project.restore.test", + "value": "Restore the complete fact.", + "category": "project", + "mention_count": 3, + "source_session_ids": ["session-a"], + }], + }) + deleted_fact = dict( + context.runtime_long_term_memory_store["facts"][0] + ) + + deleted = await delete_l4_memory_fact( + context, + deleted_fact["id"], + ) + + self.assertTrue(deleted) + self.assertEqual(context.runtime_long_term_memory_store["facts"], []) + self.assertEqual( + context.runtime_long_term_memory_store["deleted_fact_ids"], + [deleted_fact["id"]], + ) + self.assertEqual(logger.logs[0]["event"], "fact_deleted") + self.assertEqual(logger.logs[0]["tag_suffix"], "DELETED") + self.assertEqual(logger.logs[0]["deleted_fact"], deleted_fact) + self.assertEqual( + json.loads(logger.logs[0]["details"])["fact"], + deleted_fact, + ) + + restored = await restore_l4_memory_fact( + context, + deleted_fact, + ) + + self.assertTrue(restored) + self.assertEqual( + context.runtime_long_term_memory_store["facts"], + [deleted_fact], + ) + self.assertEqual( + context.runtime_long_term_memory_store["deleted_fact_ids"], + [], + ) + self.assertEqual( + emitter.events[-1]["change"]["restored_ids"], + [deleted_fact["id"]], + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py new file mode 100644 index 00000000..02e99f51 --- /dev/null +++ b/tests/test_memory_reference_sync_client_contract.py @@ -0,0 +1,161 @@ +from pathlib import Path +import shutil +import subprocess +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +) +RUNTIME_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +RUNTIME_MEMORY_CSS = ROOT / "ui" / "static" / "css" / "runtime-memory.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class MemoryReferenceSyncClientContractTests(unittest.TestCase): + + def test_analyzed_facts_are_excluded_from_facts_memory_mode(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + 'l4Status === "analyzed"', + source, + ) + self.assertIn( + 'if (getFactsMemoryFieldRecords().length > 0)', + source, + ) + + def test_runtime_updates_do_not_force_the_runtime_tab(self): + source = RUNTIME_JS.read_text(encoding="utf-8") + start = source.index("function handleRuntimeMemoryMessage(data)") + end = source.index("window.JinRuntime.runtime =", start) + handler = source[start:end] + + self.assertNotIn( + 'runtimeMemoryDisplayMode = "runtime";', + handler, + ) + + def test_chat_dispatches_persistent_and_hover_reference_sources(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn( + '"jin:memory-reference-highlight"', + source, + ) + self.assertIn( + 'function bindJinMemoryReferenceBubble(', + source, + ) + self.assertIn( + 'setLatestJinMemoryReferenceText(', + source, + ) + self.assertIn( + 'clearJinMemoryReferenceHighlights();', + source, + ) + + def test_memory_rows_expose_keys_and_ids_for_reference_matching(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + 'collectMemoryRecordReferenceAliases(line)', + source, + ) + self.assertIn( + 'report._storage_key,', + source, + ) + self.assertIn( + '"runtime-memory-reference-hit"', + source, + ) + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + self.assertIn( + '.runtime-memory-reference-hit .runtime-memory-key', + css_source, + ) + self.assertNotIn( + '.runtime-memory-reference-hit {', + css_source, + ) + + @unittest.skipUnless( + shutil.which("node"), + "node is required for the browser-side reference matcher test", + ) + def test_reference_matcher_requires_whole_ids_or_keys(self): + script = r''' +const fs = require("fs"); +const source = fs.readFileSync(process.argv[1], "utf8"); +const start = source.indexOf("function normalizeMemoryReferenceSearchText("); +const end = source.indexOf("\n function normalizeMemoryReferenceAliases(", start); + +if (start < 0 || end < 0) { + throw new Error("reference matcher functions were not found"); +} + +eval(source.slice(start, end)); + +const cases = [ + ["uses project.reasoning_injection_mechanism now", "project.reasoning_injection_mechanism", true], + ["uses PROJECT.REASONING_INJECTION_MECHANISM now", "project.reasoning_injection_mechanism", true], + ["record `5fdg4g` is active", "5fdg4g", true], + ["record x5fdg4g is different", "5fdg4g", false], + ["project.reasoning_injection_mechanism_extra", "project.reasoning_injection_mechanism", false], + ["project.reasoning_injection_mechanism.child", "project.reasoning_injection_mechanism", false], +]; + +for (const [text, reference, expected] of cases) { + const actual = containsMemoryReference(text, reference); + + if (actual !== expected) { + throw new Error(`${JSON.stringify([text, reference])}: ${actual}`); + } +} +''' + completed = subprocess.run( + [ + shutil.which("node"), + "-e", + script, + str(MEMORY_VIEW_JS), + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + completed.returncode, + 0, + completed.stderr or completed.stdout, + ) + + def test_cache_versions_are_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn( + '/static/css/runtime-memory.css?v=l4-logger-cards-1', + source, + ) + self.assertIn( + '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-12', + source, + ) + self.assertIn( + '/static/js/runtime/runtime.js?v=runtime-facade-16', + source, + ) + self.assertIn( + '/static/js/chat.js?v=chat-core-6', + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_websocket_logger_model_output.py b/tests/test_websocket_logger_model_output.py index f97a85b6..bbcf87dd 100644 --- a/tests/test_websocket_logger_model_output.py +++ b/tests/test_websocket_logger_model_output.py @@ -20,6 +20,21 @@ async def send_json(self, payload): class WebSocketLoggerModelOutputTests(unittest.IsolatedAsyncioTestCase): + async def test_memory_log_tag_suffix_preserves_l4_memory_level(self): + websocket = FakeWebSocket() + logger = WebSocketLogger(websocket) + + await logger.log_memory( + "L4", + "L4 fact deleted", + event="fact_deleted", + tag_suffix="DELETED", + ) + + self.assertEqual(websocket.events[0]["tag"], "[MEMORY:L4:DELETED]") + self.assertEqual(websocket.events[0]["memory_level"], "L4") + self.assertEqual(websocket.events[0]["memory_event"], "fact_deleted") + async def test_brain_output_over_150_chars_uses_100_char_preview_and_payload(self): websocket = FakeWebSocket() logger = WebSocketLogger(websocket) diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index c50d398b..304f3aeb 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -174,7 +174,9 @@ } .jin-avatar-orbit.is-runtime-cited, -.jin-avatar-counter-orbit.is-runtime-cited { +.jin-avatar-counter-orbit.is-runtime-cited, +.jin-avatar-orbit.is-memory-reference-hit, +.jin-avatar-counter-orbit.is-memory-reference-hit { filter: brightness(1.48) saturate(1.72) @@ -186,13 +188,19 @@ .jin-avatar-orbit.is-runtime-cited circle, .jin-avatar-orbit.is-runtime-cited line, .jin-avatar-counter-orbit.is-runtime-cited circle, -.jin-avatar-counter-orbit.is-runtime-cited line { +.jin-avatar-counter-orbit.is-runtime-cited line, +.jin-avatar-orbit.is-memory-reference-hit circle, +.jin-avatar-orbit.is-memory-reference-hit line, +.jin-avatar-counter-orbit.is-memory-reference-hit circle, +.jin-avatar-counter-orbit.is-memory-reference-hit line { opacity: 1; stroke-opacity: 0.96 !important; } .jin-avatar-orbit.is-runtime-cited circle[fill]:not([fill="none"]), -.jin-avatar-counter-orbit.is-runtime-cited circle[fill]:not([fill="none"]) { +.jin-avatar-counter-orbit.is-runtime-cited circle[fill]:not([fill="none"]), +.jin-avatar-orbit.is-memory-reference-hit circle[fill]:not([fill="none"]), +.jin-avatar-counter-orbit.is-memory-reference-hit circle[fill]:not([fill="none"]) { fill-opacity: 1 !important; } @@ -222,7 +230,8 @@ drop-shadow(0 0 11px var(--jin-avatar-memory-glow-mid, rgba(239, 255, 255, 0.54))); } -.jin-avatar-memory-dash.is-runtime-cited { +.jin-avatar-memory-dash.is-runtime-cited, +.jin-avatar-memory-dash.is-memory-reference-hit { filter: brightness(1.72) saturate(1.65) @@ -232,7 +241,8 @@ drop-shadow(0 0 38px var(--jin-avatar-memory-glow-far, rgba(34, 211, 238, 0.18))); } -.jin-avatar-memory-dash.is-runtime-cited path { +.jin-avatar-memory-dash.is-runtime-cited path, +.jin-avatar-memory-dash.is-memory-reference-hit path { stroke-opacity: 1 !important; stroke-width: var(--jin-avatar-memory-hover-width, 2px); } diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 6591615c..418e0a38 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -19,6 +19,14 @@ border-radius: 4px; } +.runtime-memory-reference-hit .runtime-memory-key, +.runtime-memory-reference-hit .runtime-memory-value { + color: rgba(222, 245, 241, 0.88); + text-shadow: + 0 0 7px rgba(210, 246, 241, 0.34), + 0 0 13px rgba(129, 216, 208, 0.22); +} + .runtime-memory-delayed-row { cursor: pointer; border-radius: 4px; @@ -337,3 +345,18 @@ font-size: 20px; line-height: 1; } + +.delayed-memory-modal-section > .delayed-memory-modal-fields { + margin-top: 8px; +} + +.l4-trace-no-changes { + display: flex; + min-height: 180px; + align-items: center; + justify-content: center; + color: rgba(161, 161, 170, 0.82); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; +} diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 31ec61fd..25350d4a 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -561,6 +561,17 @@ body.theme-win95 #summarizer-window-bar::after { ); } +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit { + background: #d7f0ed; + box-shadow: inset 0 0 0 1px #6b9e99; +} + +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-key, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-value { + color: #005650 !important; + text-shadow: none; +} + body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-key.flash-new, body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-value.flash-new { color: #007f48 !important; diff --git a/ui/static/js/chat-attachments.js b/ui/static/js/chat-attachments.js index 8ceeac14..ce1310ce 100644 --- a/ui/static/js/chat-attachments.js +++ b/ui/static/js/chat-attachments.js @@ -813,7 +813,8 @@ function normalizeDelayedMemoryReportForModal( const requestedId = String( delayedMemoryReportId || "" - ).trim(); + ).trim() + .toLowerCase(); if ( requestedId @@ -877,7 +878,8 @@ function getDelayedMemoryReportPreviewSource( const requestedId = String( delayedMemoryReportId || "" - ).trim(); + ).trim() + .toLowerCase(); if ( !requestedId @@ -940,6 +942,9 @@ function bindDelayedMemoryReportPreview( element.removeAttribute( "tabindex" ); + element.classList.remove( + "cursor-pointer" + ); return; } @@ -948,9 +953,12 @@ function bindDelayedMemoryReportPreview( "button" ); element.tabIndex = 0; - element.classList.add( + element.classList.remove( "cursor-help" ); + element.classList.add( + "cursor-pointer" + ); element.title = String( diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index dc9ef83e..a67a2432 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -8,6 +8,9 @@ const streamMessages = const STREAM_FRAME_WARNING_MS = 12; const STREAM_NEAR_BOTTOM_PX = 72; +const MEMORY_REFERENCE_HIGHLIGHT_EVENT = + "jin:memory-reference-highlight"; + function isChatRenderForeground() { @@ -185,6 +188,93 @@ function renderChatTextHtml(text) { } +function isJinMemoryReferenceRole(role) { + return ( + role === "brain" + || role === "service" + || role === "translator" + ); +} + +function dispatchJinMemoryReferenceHighlight( + source, + text, + active = true +) { + window.dispatchEvent( + new CustomEvent( + MEMORY_REFERENCE_HIGHLIGHT_EVENT, + { + detail: { + source, + text: String(text || ""), + active: Boolean(active), + }, + } + ) + ); +} + +function setLatestJinMemoryReferenceText( + role, + text +) { + if (!isJinMemoryReferenceRole(role)) { + return; + } + + dispatchJinMemoryReferenceHighlight( + "persistent", + text, + true + ); +} + +function clearJinMemoryReferenceHighlights() { + dispatchJinMemoryReferenceHighlight( + "persistent", + "", + false + ); + dispatchJinMemoryReferenceHighlight( + "hover", + "", + false + ); +} + +function bindJinMemoryReferenceBubble( + bubble, + content, + role +) { + if ( + !bubble + || !content + || !isJinMemoryReferenceRole(role) + ) { + return; + } + + bubble.addEventListener("mouseenter", () => { + dispatchJinMemoryReferenceHighlight( + "hover", + content.dataset.memoryReferenceText + || content.textContent + || "", + true + ); + }); + + bubble.addEventListener("mouseleave", () => { + dispatchJinMemoryReferenceHighlight( + "hover", + "", + false + ); + }); +} + function shouldFormatChatRole(role) { return ( @@ -215,6 +305,8 @@ function renderChatTextElement( "jin-chat-markdown", format ); + element.dataset.memoryReferenceText = + String(text || ""); element.innerHTML = ( @@ -706,6 +798,11 @@ function createMessageElement( config.bubbleClass; bubble.appendChild(pre); + bindJinMemoryReferenceBubble( + bubble, + pre, + role + ); msgDiv.appendChild( createAvatarElement( @@ -819,6 +916,12 @@ function appendChatMessage( jinConversationTurnCounter += 1; window.jinConversationTurnCounter = jinConversationTurnCounter; + clearJinMemoryReferenceHighlights(); + } else { + setLatestJinMemoryReferenceText( + role, + text + ); } flushRuntimeActionsAfterResponse( @@ -1009,6 +1112,11 @@ function createStreamGroup( config.bubbleClass; bubble.appendChild(pre); + bindJinMemoryReferenceBubble( + bubble, + pre, + role + ); messageRow.appendChild( createAvatarElement( @@ -1331,6 +1439,10 @@ function finishStreamMessage( } if (stream.answer.trim()) { + setLatestJinMemoryReferenceText( + stream.role, + stream.answer + ); flushRuntimeActionsAfterResponse( stream.role ); diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 167ba764..f844ddb5 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -871,6 +871,548 @@ function refreshFactsMemoryAppendButtons() { ); } +const l4SummarizerCards = { + extraction: [], + merge: [], +}; + +const l4DeletedFactCards = + new Map(); + +function parseL4JsonPayload(details) { + const text = + String(details || "").trim(); + + if (!text) { + return null; + } + + const direct = + parseTraceJson(text); + + if (direct) { + return direct; + } + + const fenced = + text.match(/```(?:json)?\s*([\s\S]*?)```/i); + + if (fenced) { + const parsed = + parseTraceJson(fenced[1].trim()); + + if (parsed) { + return parsed; + } + } + + const firstBrace = + text.indexOf("{"); + const lastBrace = + text.lastIndexOf("}"); + + if (firstBrace >= 0 && lastBrace > firstBrace) { + return parseTraceJson( + text.slice(firstBrace, lastBrace + 1) + ); + } + + return null; +} + +function createL4LoggerCard(tag) { + const logDiv = + document.createElement("div"); + + logDiv.className = + "mb-1 min-w-0 whitespace-pre-wrap break-words font-mono text-[12px] bg-blue-500/5 p-2 rounded border border-blue-500/10"; + + logDiv.style.overflowWrap = + "anywhere"; + + logDiv.dataset.logKind = + "memory"; + + const tagSpan = + document.createElement("span"); + + tagSpan.className = + "text-blue-300 font-bold logger-tag block"; + + tagSpan.textContent = + tag; + + logDiv.appendChild(tagSpan); + + consoleStream.appendChild(logDiv); + consoleStream.scrollTop = + consoleStream.scrollHeight; + + return logDiv; +} + +function createL4LoggerButton( + label, + tone = "blue", +) { + const button = + document.createElement("button"); + + button.type = + "button"; + + button.textContent = + label; + + setL4LoggerButtonTone( + button, + tone + ); + + return button; +} + +function setL4LoggerButtonTone( + button, + tone, +) { + button.className = + tone === "muted" + ? "inline-flex items-center rounded border border-zinc-600/40 px-2 py-1 text-[10px] uppercase tracking-wider text-zinc-400 hover:bg-zinc-700/30 transition" + : "inline-flex items-center rounded border border-blue-500/20 px-2 py-1 text-[10px] uppercase tracking-wider text-blue-300 hover:bg-blue-500/10 transition"; +} + +function resolveL4SummarizerPhase( + message, + meta, +) { + if (String(meta && meta.memory_level || "").toUpperCase() !== "L4") { + return ""; + } + + const normalized = + String(message || "").toLowerCase(); + + if (normalized.startsWith("l4 extraction summarizer ")) { + return "extraction"; + } + + if (normalized.startsWith("l4 merge summarizer ")) { + return "merge"; + } + + return ""; +} + +function resolveL4SummarizerEvent( + message, + meta, +) { + const event = + String(meta && meta.memory_event || "").toLowerCase(); + + if (event === "summarizer_request" || event === "summarizer_result") { + return event; + } + + const normalized = + String(message || "").toLowerCase(); + + if (normalized.endsWith("summarizer request")) { + return "summarizer_request"; + } + + if (normalized.endsWith("summarizer result")) { + return "summarizer_result"; + } + + return ""; +} + +function l4ResponseHasChanges( + phase, + payload, +) { + if (!payload || typeof payload !== "object") { + return true; + } + + if (phase === "extraction" && Array.isArray(payload.facts)) { + return payload.facts.length > 0; + } + + if (phase === "merge" && Array.isArray(payload.operations)) { + return payload.operations.length > 0; + } + + return Object.keys(payload).length > 0; +} + +function createL4SummarizerCard( + phase, + requestDetails = null, +) { + const phaseLabel = + phase.toUpperCase(); + + const logDiv = + createL4LoggerCard( + `[MEMORY:L4:${phaseLabel}]` + ); + + const actions = + document.createElement("div"); + + actions.className = + "mt-2 flex flex-wrap items-center gap-2"; + + const requestButton = + createL4LoggerButton( + "request" + ); + + const responseButton = + createL4LoggerButton( + "response" + ); + + responseButton.hidden = + true; + + const state = { + logDiv, + phase, + requestDetails, + responseReady: false, + responseDetails: null, + responsePayload: null, + responseHasChanges: false, + requestButton, + responseButton, + }; + + requestButton.hidden = + !requestDetails; + + requestButton.addEventListener( + "click", + function () { + if (!state.requestDetails) { + return; + } + + showTrace( + state.requestDetails, + `L4 ${phase} request` + ); + } + ); + + responseButton.addEventListener( + "click", + function () { + if (!state.responseReady) { + return; + } + + showTrace( + JSON.stringify({ + kind: "l4_summarizer_response", + phase, + payload: state.responsePayload, + raw: state.responseDetails, + no_changes: !state.responseHasChanges, + }), + `L4 ${phase} response` + ); + } + ); + + actions.appendChild(requestButton); + actions.appendChild(responseButton); + logDiv.appendChild(actions); + + l4SummarizerCards[phase].push(state); + + return state; +} + +function handleL4SummarizerLog( + message, + details, + meta, +) { + const phase = + resolveL4SummarizerPhase( + message, + meta + ); + + if (!phase) { + return null; + } + + const event = + resolveL4SummarizerEvent( + message, + meta + ); + + if (event === "summarizer_request") { + return createL4SummarizerCard( + phase, + details + ).logDiv; + } + + if (event !== "summarizer_result") { + return null; + } + + let state = + [...l4SummarizerCards[phase]] + .reverse() + .find((candidate) => !candidate.responseReady); + + if (!state) { + state = + createL4SummarizerCard( + phase + ); + } + + state.responseReady = + true; + state.responseDetails = + String(details ?? ""); + state.responsePayload = + parseL4JsonPayload( + state.responseDetails + ); + state.responseHasChanges = + Boolean(state.responseDetails.trim()) + && l4ResponseHasChanges( + phase, + state.responsePayload + ); + + state.responseButton.hidden = + false; + + setL4LoggerButtonTone( + state.responseButton, + state.responseHasChanges + ? "blue" + : "muted" + ); + + return state.logDiv; +} + +function resolveDeletedL4Fact( + details, + meta, +) { + if ( + meta + && meta.deleted_fact + && typeof meta.deleted_fact === "object" + ) { + return meta.deleted_fact; + } + + const payload = + parseL4JsonPayload(details); + + if ( + payload + && payload.fact + && typeof payload.fact === "object" + ) { + return payload.fact; + } + + return null; +} + +function handleL4DeletedFactLog( + tag, + details, + meta, +) { + const isDeleted = + String(meta && meta.memory_event || "").toLowerCase() === "fact_deleted" + || String(tag || "").toUpperCase() === "[MEMORY:L4:DELETED]"; + + if (!isDeleted) { + return null; + } + + const fact = + resolveDeletedL4Fact( + details, + meta + ); + + if (!fact) { + return null; + } + + const logDiv = + createL4LoggerCard( + "[MEMORY:L4:DELETED]" + ); + + const key = + document.createElement("span"); + + key.className = + "block mt-2 text-zinc-200 font-semibold"; + + key.textContent = + String(fact.key || fact.id || "L4 fact"); + + const value = + document.createElement("span"); + + value.className = + "block mt-1 text-zinc-400"; + + value.textContent = + String(fact.value || ""); + + logDiv.appendChild(key); + + if (value.textContent) { + logDiv.appendChild(value); + } + + const actions = + document.createElement("div"); + + actions.className = + "mt-2 flex flex-wrap items-center gap-2"; + + const payloadButton = + createL4LoggerButton( + "payload" + ); + + const restoreButton = + createL4LoggerButton( + "restore" + ); + + payloadButton.addEventListener( + "click", + function () { + showTrace( + JSON.stringify({ + kind: "l4_fact", + fact, + }), + "L4 fact deleted" + ); + } + ); + + restoreButton.addEventListener( + "click", + function () { + const api = + window.JINRuntimeL4Memory; + + if (!api || typeof api.requestFactRestore !== "function") { + return; + } + + const sent = + api.requestFactRestore( + fact + ); + + if (!sent) { + restoreButton.textContent = + "offline"; + + window.setTimeout( + function () { + restoreButton.textContent = + "restore"; + }, + 1200 + ); + return; + } + + restoreButton.disabled = + true; + restoreButton.textContent = + "restoring"; + restoreButton.classList.add( + "opacity-50" + ); + + l4DeletedFactCards.set( + String(fact.id || ""), + { + logDiv, + restoreButton, + } + ); + } + ); + + actions.appendChild(payloadButton); + actions.appendChild(restoreButton); + logDiv.appendChild(actions); + + return logDiv; +} + +function handleL4MemoryRestoreResult( + data +) { + const factId = + String(data && data.fact_id || ""); + + const state = + l4DeletedFactCards.get( + factId + ); + + if (!state) { + return; + } + + l4DeletedFactCards.delete( + factId + ); + + if (data && data.restored) { + dismissLogAfterClear( + state.logDiv + ); + return; + } + + state.restoreButton.disabled = + false; + state.restoreButton.textContent = + "restore failed"; + state.restoreButton.classList.remove( + "opacity-50" + ); + + window.setTimeout( + function () { + state.restoreButton.textContent = + "restore"; + }, + 1400 + ); +} + function appendLog( tag, message, @@ -887,6 +1429,28 @@ function appendLog( details, ); + const l4SummarizerLog = + handleL4SummarizerLog( + normalized.message, + normalized.details, + meta + ); + + if (l4SummarizerLog) { + return l4SummarizerLog; + } + + const l4DeletedFactLog = + handleL4DeletedFactLog( + tag, + normalized.details, + meta + ); + + if (l4DeletedFactLog) { + return l4DeletedFactLog; + } + const flowId = meta?.flow_id; @@ -1628,6 +2192,9 @@ function appendLog( return logDiv; } +window.handleL4MemoryRestoreResult = + handleL4MemoryRestoreResult; + window.refreshFactsMemoryAppendButtons = refreshFactsMemoryAppendButtons; diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index 43c3bbf3..044bd5a9 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -338,6 +338,219 @@ function appendTraceModalBody( ); } +function prettifyTraceFieldName(value) { + return String(value || "") + .replace(/_/g, " ") + .replace(/\b\w/g, function (letter) { + return letter.toUpperCase(); + }); +} + +function formatStructuredTraceValue(value) { + if (value === null || typeof value === "undefined") { + return ""; + } + + if (Array.isArray(value) || typeof value === "object") { + try { + return JSON.stringify( + value, + null, + 2 + ); + } catch (_error) { + return String(value); + } + } + + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + + const text = String(value); + return text || ""; +} + +function appendStructuredTraceFields( + parent, + data, + orderedKeys = [], +) { + const source = + data && typeof data === "object" && !Array.isArray(data) + ? data + : {}; + + const fields = + document.createElement("section"); + + fields.className = + "delayed-memory-modal-fields"; + + const keys = []; + const seen = new Set(); + + orderedKeys.concat(Object.keys(source)).forEach((key) => { + if (seen.has(key) || !Object.prototype.hasOwnProperty.call(source, key)) { + return; + } + seen.add(key); + keys.push(key); + }); + + keys.forEach((key) => { + const row = + document.createElement("div"); + + row.className = + "delayed-memory-modal-field"; + + const label = + document.createElement("div"); + + label.className = + "delayed-memory-modal-label"; + + label.textContent = + prettifyTraceFieldName(key); + + const value = + document.createElement("div"); + + value.className = + "delayed-memory-modal-value"; + + value.textContent = + formatStructuredTraceValue(source[key]); + + row.appendChild(label); + row.appendChild(value); + fields.appendChild(row); + }); + + parent.appendChild(fields); +} + +function renderL4FactTrace(parsed) { + const fact = + parsed && parsed.fact && typeof parsed.fact === "object" + ? parsed.fact + : {}; + + appendStructuredTraceFields( + traceModalContent, + fact, + [ + "id", + "key", + "value", + "category", + "mention_count", + "created_at", + "updated_at", + "source_session_ids", + "source_runtime_snapshot_ids", + "source_keys", + "source_fact_ids", + ] + ); +} + +function appendL4ResponseGroup( + title, + value, +) { + const section = + document.createElement("section"); + + section.className = + "delayed-memory-modal-section"; + + const heading = + document.createElement("div"); + + heading.className = + "delayed-memory-modal-section-title"; + + heading.textContent = + title; + + section.appendChild(heading); + traceModalContent.appendChild(section); + + if (value && typeof value === "object" && !Array.isArray(value)) { + appendStructuredTraceFields( + section, + value + ); + return; + } + + appendTraceModalBody( + section, + "Value", + value + ); +} + +function renderL4SummarizerResponseTrace(parsed) { + if (parsed.no_changes) { + const empty = + document.createElement("div"); + + empty.className = + "l4-trace-no-changes"; + + empty.textContent = + "No changes"; + + traceModalContent.appendChild(empty); + return; + } + + const payload = parsed.payload; + const phase = String(parsed.phase || "").toLowerCase(); + + if (phase === "extraction" && payload && Array.isArray(payload.facts)) { + payload.facts.forEach((fact, index) => { + appendL4ResponseGroup( + `Fact ${index + 1}`, + fact + ); + }); + return; + } + + if (phase === "merge" && payload && Array.isArray(payload.operations)) { + payload.operations.forEach((operation, index) => { + const action = + operation && operation.action + ? ` · ${String(operation.action).toUpperCase()}` + : ""; + + appendL4ResponseGroup( + `Operation ${index + 1}${action}`, + operation + ); + }); + return; + } + + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + appendStructuredTraceFields( + traceModalContent, + payload + ); + return; + } + + appendTraceModalBody( + traceModalContent, + "Response", + parsed.raw || payload || "" + ); +} + function isSummarizerRequestPayload(parsed) { return Boolean( parsed @@ -551,6 +764,28 @@ function renderTraceDetails( const parsed = parseTraceJson(details); + if ( + parsed + && parsed.kind === "l4_fact" + ) { + renderL4FactTrace( + parsed + ); + + return; + } + + if ( + parsed + && parsed.kind === "l4_summarizer_response" + ) { + renderL4SummarizerResponseTrace( + parsed + ); + + return; + } + if ( parsed && parsed.kind === "user_payload_trace" diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index bbc6d0ca..873a6487 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -6,6 +6,10 @@ const SVG_NS = "http://www.w3.org/2000/svg"; const AVATAR_EVENT = "jin:runtime-avatar-snapshot"; const THINK_RUNTIME_CITATION_HOVER_EVENT = "jin:think-runtime-citation-hover"; + const MEMORY_REFERENCE_HIGHLIGHT_EVENT = + "jin:memory-reference-highlight"; + const MEMORY_REFERENCE_ALIAS_DATASET_KEY = + "memoryReferenceAliases"; const CENTER = 180; const INNER_RING_SCALE = 0.90; @@ -73,6 +77,20 @@ const settingsPanel = document.getElementById("settings-panel"); const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; + const memoryReferenceHelpers = + window.JinRuntime.memoryReferences || {}; + const containsMemoryReference = + typeof memoryReferenceHelpers.contains === "function" + ? memoryReferenceHelpers.contains + : () => false; + const normalizeMemoryReferenceAliases = + typeof memoryReferenceHelpers.normalizeAliases === "function" + ? memoryReferenceHelpers.normalizeAliases + : aliases => (Array.isArray(aliases) ? aliases : []); + const collectMemoryMetadataReferenceAliases = + typeof memoryReferenceHelpers.collectMetadataAliases === "function" + ? memoryReferenceHelpers.collectMetadataAliases + : () => []; if (!avatarRoot) { return; @@ -81,11 +99,50 @@ let centerColor = DEFAULT_CENTER_COLOR; let centerColorTransitionQueue = []; let centerColorTransitionTimer = null; + const memoryReferenceHighlightState = { + persistentText: "", + hoverText: "", + }; function clamp(value, min, max) { return Math.max(min, Math.min(max, Number(value) || 0)); } + function setAvatarMemoryReferenceAliases(node, aliases) { + if (!node) { + return; + } + + const normalizedAliases = + normalizeMemoryReferenceAliases(aliases); + + if (!normalizedAliases.length) { + delete node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY]; + return; + } + + node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY] = + JSON.stringify(normalizedAliases); + } + + function getAvatarMemoryReferenceAliases(node) { + if (!node || !node.dataset) { + return []; + } + + const raw = node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY]; + + if (!raw) { + return []; + } + + try { + return normalizeMemoryReferenceAliases(JSON.parse(raw)); + } catch (error) { + return []; + } + } + function hashString(value) { let hash = 2166136261; @@ -291,6 +348,13 @@ key, value, text, + referenceAliases: + normalizeMemoryReferenceAliases([ + key, + line && line.id, + line && line.active_memory_id, + ...collectMemoryMetadataReferenceAliases(value), + ]), length: Array.from(text).length, changeRatio: Math.max( Number(line && line.key_change_ratio || 0), @@ -507,6 +571,11 @@ key, value, text: lineText, + referenceAliases: + normalizeMemoryReferenceAliases([ + key, + ...collectMemoryMetadataReferenceAliases(value), + ]), citationText: normalizeRuntimeCitationIdentity(lineText), }; @@ -555,6 +624,11 @@ title, summary, pinned: Boolean(report.pinned), + referenceAliases: + normalizeMemoryReferenceAliases([ + id, + report.id, + ]), }; }) .filter(Boolean) @@ -592,6 +666,11 @@ "data-delayed-memory-id": options.delayedMemoryId || null, }); + setAvatarMemoryReferenceAliases( + dashGroup, + options.referenceAliases + ); + setMemoryDashGlowVariables( dashGroup, glowColor, @@ -711,7 +790,10 @@ color, glowColor: ACTIVE_MEMORY_RING_COLOR, opacity: 0.76, + citationKey: + normalizeRuntimeCitationIdentity(record.key), citationText: record.citationText, + referenceAliases: record.referenceAliases, title: `Active memory ${index + 1}: ${record.value || record.text}`, } ); @@ -739,6 +821,7 @@ citationKey: normalizeRuntimeCitationIdentity(record.id), delayedMemoryId: record.id, + referenceAliases: record.referenceAliases, title: `${record.title}${record.summary ? `: ${record.summary}` : ""}`, } ); @@ -1106,6 +1189,10 @@ normalizeRuntimeCitationIdentity(record.key); orbitGroup.dataset.runtimeLineText = normalizeRuntimeCitationIdentity(record.text); + setAvatarMemoryReferenceAliases( + orbitGroup, + record.referenceAliases + ); appendTitle( orbitGroup, @@ -1319,6 +1406,55 @@ }); } + function getActiveMemoryReferenceText() { + return ( + memoryReferenceHighlightState.hoverText + || memoryReferenceHighlightState.persistentText + || "" + ); + } + + function applyMemoryReferenceGlow() { + const svg = avatarRoot.querySelector("svg"); + + if (!svg) { + return; + } + + const sourceText = getActiveMemoryReferenceText(); + + svg.querySelectorAll("[data-memory-reference-aliases]") + .forEach((recordNode) => { + const matched = Boolean( + sourceText + && getAvatarMemoryReferenceAliases(recordNode) + .some(alias => containsMemoryReference(sourceText, alias)) + ); + + recordNode.classList.toggle( + "is-memory-reference-hit", + matched + ); + }); + } + + function handleMemoryReferenceHighlight(event) { + const detail = event && event.detail || {}; + const source = detail.source === "hover" + ? "hover" + : "persistent"; + const stateKey = source === "hover" + ? "hoverText" + : "persistentText"; + + memoryReferenceHighlightState[stateKey] = + detail.active === false + ? "" + : String(detail.text || ""); + + applyMemoryReferenceGlow(); + } + function buildRenderSignature( snapshot, lines, @@ -1406,6 +1542,7 @@ avatarRoot.style.setProperty("--jin-avatar-overall-color", overallColor); avatarRoot.style.setProperty("--jin-avatar-center-color", centerColor); applyThinkRuntimeCitationGlow(); + applyMemoryReferenceGlow(); lastRenderSignature = signature; } @@ -1663,6 +1800,11 @@ scheduleSnapshotRender(snapshot, snapshotIndex); }); + window.addEventListener( + MEMORY_REFERENCE_HIGHLIGHT_EVENT, + handleMemoryReferenceHighlight + ); + window.addEventListener(THINK_RUNTIME_CITATION_HOVER_EVENT, (event) => { const detail = event && event.detail || {}; const sourceId = diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 88b8b4a5..ed02b23a 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -266,6 +266,20 @@ }); } + function requestFactRestore(fact) { + const normalized = normalizeFact( + fact, + false + ); + if (!normalized) { + return false; + } + return sendIfOpen({ + type: "l4_memory_restore_fact", + fact: normalized, + }); + } + function maybeSendIdleTick() { if ( typeof window.getJinUserIdleContext !== "function" @@ -322,6 +336,7 @@ applyFactsMemoryRecordsUpdate, applyServerUpdate, requestFactDelete, + requestFactRestore, maybeSendIdleTick, startIdleMonitor, }; diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index bfa2159f..91169ab6 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -26,6 +26,16 @@ const DELAYED_MEMORY_ROW_HOVER_SOURCE_ID = "delayed-memory-row-hover"; const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; + const MEMORY_REFERENCE_HIGHLIGHT_EVENT = + "jin:memory-reference-highlight"; + const MEMORY_REFERENCE_ALIAS_DATASET_KEY = + "memoryReferenceAliases"; + const memoryReferenceHighlightState = { + persistentText: "", + hoverText: "", + }; + let memoryReferenceEventsBound = false; + const pinnedRuntimeMemorySnapshotIndexes = new Set(); @@ -102,6 +112,258 @@ } } + function normalizeMemoryReferenceSearchText(value) { + const raw = String(value || ""); + + try { + return raw + .normalize("NFKC") + .toLocaleLowerCase(); + } catch (error) { + return raw.toLocaleLowerCase(); + } + } + + function isMemoryReferenceTokenCharacter(character) { + const value = String(character || ""); + + if (!value) { + return false; + } + + return ( + /[0-9_.-]/.test(value) + || value.toLocaleLowerCase() + !== value.toLocaleUpperCase() + ); + } + + function containsMemoryReference(text, reference) { + const haystack = + normalizeMemoryReferenceSearchText(text); + const needle = + normalizeMemoryReferenceSearchText(reference).trim(); + + if (!haystack || !needle) { + return false; + } + + let index = haystack.indexOf(needle); + + while (index >= 0) { + const before = + index > 0 + ? haystack[index - 1] + : ""; + const afterIndex = + index + needle.length; + const after = + afterIndex < haystack.length + ? haystack[afterIndex] + : ""; + + if ( + !isMemoryReferenceTokenCharacter(before) + && !isMemoryReferenceTokenCharacter(after) + ) { + return true; + } + + index = haystack.indexOf( + needle, + index + 1 + ); + } + + return false; + } + + function normalizeMemoryReferenceAliases(aliases) { + const seen = new Set(); + + return (Array.isArray(aliases) ? aliases : []) + .map(alias => String(alias || "").trim()) + .filter((alias) => { + if (!alias) { + return false; + } + + const identity = + normalizeMemoryReferenceSearchText(alias); + + if (!identity || seen.has(identity)) { + return false; + } + + seen.add(identity); + return true; + }); + } + + function collectMemoryMetadataReferenceAliases(value) { + const aliases = []; + const text = String(value || ""); + const pattern = + /\[\s*([a-z0-9_.-]*id)\s*:\s*([^\]]+?)\s*\]/gi; + let match = null; + + while ((match = pattern.exec(text)) !== null) { + const field = + String(match[1] || "") + .trim() + .toLocaleLowerCase(); + + if ( + field !== "id" + && !field.endsWith("_id") + ) { + continue; + } + + String(match[2] || "") + .split(/\s*,\s*/) + .map(item => item.trim()) + .filter(Boolean) + .forEach(item => aliases.push(item)); + } + + return aliases; + } + + function collectMemoryRecordReferenceAliases(record) { + if (!record || typeof record !== "object") { + return []; + } + + return normalizeMemoryReferenceAliases([ + record.key, + record.id, + record.active_memory_id, + ...collectMemoryMetadataReferenceAliases( + record.value + ), + ]); + } + + window.JinRuntime.memoryReferences = Object.freeze({ + contains: containsMemoryReference, + normalizeAliases: normalizeMemoryReferenceAliases, + collectMetadataAliases: collectMemoryMetadataReferenceAliases, + }); + + function setMemoryReferenceAliases(row, aliases) { + if (!row) { + return; + } + + const normalizedAliases = + normalizeMemoryReferenceAliases(aliases); + + if (!normalizedAliases.length) { + delete row.dataset[ + MEMORY_REFERENCE_ALIAS_DATASET_KEY + ]; + return; + } + + row.dataset[ + MEMORY_REFERENCE_ALIAS_DATASET_KEY + ] = JSON.stringify(normalizedAliases); + } + + function getMemoryReferenceAliases(row) { + if (!row || !row.dataset) { + return []; + } + + const raw = row.dataset[ + MEMORY_REFERENCE_ALIAS_DATASET_KEY + ]; + + if (!raw) { + return []; + } + + try { + return normalizeMemoryReferenceAliases( + JSON.parse(raw) + ); + } catch (error) { + return []; + } + } + + function getActiveMemoryReferenceText() { + return ( + memoryReferenceHighlightState.hoverText + || memoryReferenceHighlightState.persistentText + || "" + ); + } + + function applyMemoryReferenceHighlights() { + if (!runtimeMemoryText) { + return; + } + + const sourceText = + getActiveMemoryReferenceText(); + + runtimeMemoryText + .querySelectorAll( + `[data-memory-reference-aliases]` + ) + .forEach((row) => { + const matched = Boolean( + sourceText + && getMemoryReferenceAliases(row) + .some(alias => ( + containsMemoryReference( + sourceText, + alias + ) + )) + ); + + row.classList.toggle( + "runtime-memory-reference-hit", + matched + ); + }); + } + + function handleMemoryReferenceHighlight(event) { + const detail = event && event.detail || {}; + const source = + detail.source === "hover" + ? "hover" + : "persistent"; + const stateKey = + source === "hover" + ? "hoverText" + : "persistentText"; + + memoryReferenceHighlightState[stateKey] = + detail.active === false + ? "" + : String(detail.text || ""); + + applyMemoryReferenceHighlights(); + } + + function bindMemoryReferenceHighlightEvents() { + if (memoryReferenceEventsBound) { + return; + } + + window.addEventListener( + MEMORY_REFERENCE_HIGHLIGHT_EVENT, + handleMemoryReferenceHighlight + ); + + memoryReferenceEventsBound = true; + } + function getRuntimeMemorySnapshotDisplayIndex(snapshot) { if (typeof snapshot.index !== "number") { return runtimeMemoryHistory.index + 1; @@ -209,8 +471,16 @@ const content = String(field.content || "").trim(); + const l4Status = + String(field.l4_status || "pending") + .trim() + .toLocaleLowerCase(); - if (!content) { + + if ( + !content + || l4Status === "analyzed" + ) { return null; } @@ -760,21 +1030,25 @@ if (getRuntimeMemoryDisplayMode() === "active") { renderActiveMemoryRecords(); + applyMemoryReferenceHighlights(); return; } if (getRuntimeMemoryDisplayMode() === "delayed") { renderDelayedMemoryReports(); + applyMemoryReferenceHighlights(); return; } if (getRuntimeMemoryDisplayMode() === "facts") { renderFactsMemoryFields(); + applyMemoryReferenceHighlights(); return; } if (getRuntimeMemoryDisplayMode() === "long_term") { renderLongTermMemoryFacts(); + applyMemoryReferenceHighlights(); return; } @@ -798,6 +1072,7 @@ updateRuntimeMemoryPinGlow(); updateRuntimeMemoryTitleState(); dispatchRuntimeAvatarSnapshot(null); + applyMemoryReferenceHighlights(); return; } @@ -839,6 +1114,7 @@ updateRuntimeMemoryPinGlow(); updateRuntimeMemoryTitleState(); dispatchRuntimeAvatarSnapshot(sourceSnapshot); + applyMemoryReferenceHighlights(); } function isLatestRuntimeMemorySnapshot() { @@ -1171,6 +1447,10 @@ normalizeRuntimeCitationIdentity( `${line.key || "note"}: ${line.value || ""}` ); + setMemoryReferenceAliases( + row, + collectMemoryRecordReferenceAliases(line) + ); row.addEventListener( "mouseenter", @@ -1782,6 +2062,13 @@ normalizeRuntimeCitationIdentity( report._storage_key ); + setMemoryReferenceAliases( + row, + [ + report._storage_key, + report.id, + ] + ); row.appendChild( keySpan @@ -1908,6 +2195,86 @@ return `${year}-${month}-${day} ${hours}:${minutes}, ${weekday}`; } + function normalizeDelayedMemoryReportId(value) { + return String(value || "").trim().toLowerCase(); + } + + function isDelayedMemoryReportId(value) { + return /^[a-z0-9]{6}$/.test( + normalizeDelayedMemoryReportId(value) + ); + } + + function getDelayedMemoryReportId(report) { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return ""; + } + + const candidates = [ + report._storage_key, + report.id, + ]; + + for (const candidate of candidates) { + const reportId = + normalizeDelayedMemoryReportId(candidate); + + if (isDelayedMemoryReportId(reportId)) { + return reportId; + } + } + + return ""; + } + + function resolveDelayedMemoryReportForModal(report) { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return null; + } + + const reportId = + getDelayedMemoryReportId(report); + const reports = + typeof getDelayedMemoryReports === "function" + ? getDelayedMemoryReports() + : null; + const storedReport = + reportId + && reports + && typeof reports === "object" + && !Array.isArray(reports) + && reports[reportId] + && typeof reports[reportId] === "object" + && !Array.isArray(reports[reportId]) + ? reports[reportId] + : null; + + if (storedReport) { + return { + ...storedReport, + _storage_key: reportId, + }; + } + + return { + ...report, + _storage_key: + reportId + || normalizeDelayedMemoryReportId( + report._storage_key + || report.id + ), + }; + } + function updateDelayedMemoryModalPinState(report) { if (!delayedMemoryModalPinButton) { return; @@ -2246,15 +2613,22 @@ function openDelayedMemoryReportModal(report) { ensureDelayedMemoryModal(); + const resolvedReport = + resolveDelayedMemoryReportForModal(report); + + if (!resolvedReport) { + return; + } + delayedMemoryModalReport = { - ...report, + ...resolvedReport, }; updateDelayedMemoryModalPinState( delayedMemoryModalReport ); delayedMemoryModalTitle.textContent = - normalizeDelayedMemoryDisplayText(report.title) + normalizeDelayedMemoryDisplayText(delayedMemoryModalReport.title) || "Delayed memory"; delayedMemoryModalContent.innerHTML = ""; @@ -2268,44 +2642,44 @@ appendDelayedMemoryModalField( fields, "Title", - report.title + delayedMemoryModalReport.title ); appendDelayedMemoryModalField( fields, "Summary", - report.summary + delayedMemoryModalReport.summary ); appendDelayedMemoryModalField( fields, "Time", formatDelayedMemoryTime( - report.created_time + delayedMemoryModalReport.created_time ) ); appendDelayedMemoryModalField( fields, "Tags", - report.tags + delayedMemoryModalReport.tags ); appendDelayedMemoryModalField( fields, "ID", - report._storage_key + delayedMemoryModalReport._storage_key ); appendDelayedMemoryModalField( fields, "Session", - report.created_session_id + delayedMemoryModalReport.created_session_id ); appendDelayedMemoryModalExtraFields( fields, - report + delayedMemoryModalReport ); delayedMemoryModalContent.appendChild( @@ -2314,7 +2688,7 @@ appendDelayedMemoryModalBody( delayedMemoryModalContent, - report.body + delayedMemoryModalReport.body ); delayedMemoryModal.classList.remove( @@ -2830,6 +3204,8 @@ ); } + bindMemoryReferenceHighlightEvents(); + idle.configure({ onIdleTextChanged(text) { updateUserIdleTimerText( diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 678c3b67..00f832d1 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -866,7 +866,10 @@ function setDelayedMemoryReportPinned( writeDelayedMemoryReports( reports ); - renderRuntimeMemorySnapshot(); + + if (runtimeMemoryDisplayMode === "delayed") { + renderRuntimeMemorySnapshot(); + } if ( !setDelayedMemoryPinnedOnAvatar( @@ -1117,8 +1120,6 @@ function handleRuntimeMemoryMessage(data) { data ); - runtimeMemoryDisplayMode = "runtime"; - if (window.stopMemoryGlow) { window.stopMemoryGlow(); } diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index de6cb0d9..dd0844f7 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -52,6 +52,18 @@ function handleL4MemoryUpdate( } +function handleL4MemoryRestoreResult( + data +) { + + if (typeof window.handleL4MemoryRestoreResult === "function") { + window.handleL4MemoryRestoreResult( + data + ); + } + +} + function handleSocketError( data ) { @@ -247,6 +259,11 @@ registerSocketMessageHandler( handleL4MemoryUpdate ); +registerSocketMessageHandler( + "l4_memory_restore_result", + handleL4MemoryRestoreResult +); + [ "error", "fatal_error", diff --git a/ui/templates/index.html b/ui/templates/index.html index f0b76432..482a70f3 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,10 +14,10 @@ - + - + @@ -502,31 +502,31 @@ - + - - - + + + - + - + - + - + diff --git a/websocket/__init__.py b/websocket/__init__.py index bc4b8746..0a20bda0 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -20,7 +20,9 @@ apply_l4_memory_store_sync, delete_l4_memory_fact, emit_facts_memory_store_update, + restore_l4_memory_fact, emit_l4_memory_update, + runtime_l4_memory_update_running, schedule_l4_memory_idle_update, ) from runtime.fact_check import run_fact_check_once @@ -340,7 +342,12 @@ async def process_pending_requests(): ), ) - if "store" in message_data: + if ( + "store" in message_data + and not runtime_l4_memory_update_running( + context + ) + ): apply_l4_memory_store_sync( context, message_data.get( @@ -370,6 +377,26 @@ async def process_pending_requests(): ) continue + if message_type == "l4_memory_restore_fact": + fact = message_data.get( + "fact", + {}, + ) + restored = await restore_l4_memory_fact( + context, + fact, + ) + await websocket.send_json({ + "type": "l4_memory_restore_result", + "fact_id": str( + fact.get("id", "") + if isinstance(fact, dict) + else "" + ), + "restored": bool(restored), + }) + continue + if message_type == "session_bootstrap": await logger.log( diff --git a/websocket/logger.py b/websocket/logger.py index fbc21a6c..12f12467 100644 --- a/websocket/logger.py +++ b/websocket/logger.py @@ -124,10 +124,15 @@ async def log_memory( message: str, details: str | None = None, event: str | None = None, + tag_suffix: str | None = None, **extra, ): + display_level = str(level) + if tag_suffix: + display_level += f":{str(tag_suffix).strip().upper()}" + await self.log( - f"[MEMORY:{level}]", + f"[MEMORY:{display_level}]", message, details=details, channel="memory", From a092223c30749bcf0cc827d20d2605342a1d9157 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:12:14 +0300 Subject: [PATCH 013/168] feature: L4 memory layer - add an L4 facts ring to the runtime avatar - improve pinned delayed memory indication --- contracts/append_delayed_memory.json | 6 +- ...ayed_memory_pinned_glow_client_contract.py | 58 +++++++++ ..._runtime_avatar_l4_ring_client_contract.py | 74 +++++++++++ ui/static/css/runtime-avatar.css | 6 + ui/static/css/runtime-memory.css | 29 +++-- ui/static/js/chat-attachments.js | 83 +++++++++++- ui/static/js/logger/session-actions.js | 2 +- ui/static/js/logger/trace-modal.js | 2 +- ui/static/js/runtime/runtime-avatar.js | 123 ++++++++++++++++-- ui/static/js/runtime/runtime.js | 7 + ui/templates/index.html | 14 +- 11 files changed, 370 insertions(+), 34 deletions(-) create mode 100644 tests/test_delayed_memory_pinned_glow_client_contract.py create mode 100644 tests/test_runtime_avatar_l4_ring_client_contract.py diff --git a/contracts/append_delayed_memory.json b/contracts/append_delayed_memory.json index 35ba8b88..8de7979c 100644 --- a/contracts/append_delayed_memory.json +++ b/contracts/append_delayed_memory.json @@ -10,10 +10,10 @@ "emit_followup": false }, "rules": [ - "JIN can append saved reports to the context by using this marker.", + "Emit one marker per saved delayed memory. You may emit multiple markers to append multiple reports; already appended ids are ignored.", + "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in .", "Available for appending reports are listed in the block.", - "Emit one marker per saved delayed memory. You may emit multiple markers to append multiple reports; already appended ids are ignored.", - "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in ." + "If a report already appended, use content placed in block ." ], "close_tag": false } diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py new file mode 100644 index 00000000..08cf9008 --- /dev/null +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -0,0 +1,58 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_MEMORY_CSS = ROOT / "ui" / "static" / "css" / "runtime-memory.css" +CHAT_ATTACHMENTS_JS = ROOT / "ui" / "static" / "js" / "chat-attachments.js" +RUNTIME_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" +TRACE_MODAL_JS = ROOT / "ui" / "static" / "js" / "logger" / "trace-modal.js" +SESSION_ACTIONS_JS = ROOT / "ui" / "static" / "js" / "logger" / "session-actions.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class DelayedMemoryPinnedGlowClientContractTests(unittest.TestCase): + def test_pinned_delayed_memory_text_glow_is_stronger_in_panel_and_bubble(self): + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn(".runtime-memory-delayed-row-pinned .runtime-memory-key", css_source) + self.assertIn("0 0 24px rgba(16, 185, 129, 0.12)", css_source) + self.assertIn(".jin-runtime-action-delayed-memory-pinned", css_source) + + def test_pinned_delayed_memory_bubbles_can_sync_after_modal_pin_toggle(self): + attachments_source = CHAT_ATTACHMENTS_JS.read_text(encoding="utf-8") + runtime_source = RUNTIME_JS.read_text(encoding="utf-8") + + self.assertIn("function applyDelayedMemoryReportPreviewState(", attachments_source) + self.assertIn("function syncDelayedMemoryReportPreviewState(", attachments_source) + self.assertIn('"jin-runtime-action-delayed-memory-pinned"', attachments_source) + self.assertIn("window.syncDelayedMemoryReportPreviewState =", attachments_source) + self.assertIn("window.syncDelayedMemoryReportPreviewState(", runtime_source) + + def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + trace_source = TRACE_MODAL_JS.read_text(encoding="utf-8") + session_source = SESSION_ACTIONS_JS.read_text(encoding="utf-8") + attachments_source = CHAT_ATTACHMENTS_JS.read_text(encoding="utf-8") + + self.assertIn("width: 31px;", css_source) + self.assertIn("height: 31px;", css_source) + self.assertIn("width: 18px;", css_source) + self.assertIn("font-size: 23px;", css_source) + self.assertIn("drop-shadow(0 0 16px rgba(153, 246, 228, 0.34))", css_source) + self.assertIn('"delayed-memory-modal-icon-button delayed-memory-modal-close"', trace_source) + self.assertIn('"delayed-memory-modal-icon-button delayed-memory-modal-close"', session_source) + self.assertIn('"delayed-memory-modal-icon-button delayed-memory-modal-close shrink-0"', attachments_source) + + def test_cache_versions_are_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("/static/css/runtime-memory.css?v=l4-logger-cards-2", source) + self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-17", source) + self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) + self.assertIn("/static/js/logger/trace-modal.js?v=l4-structured-trace-2", source) + self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py new file mode 100644 index 00000000..611854b9 --- /dev/null +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -0,0 +1,74 @@ +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +AVATAR_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" +) +AVATAR_CSS = ROOT / "ui" / "static" / "css" / "runtime-avatar.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimeAvatarL4RingClientContractTests(unittest.TestCase): + + def test_memory_rings_have_evenly_spaced_l4_delayed_active_radii(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + radii = {} + for kind in ("l4", "delayed", "active"): + match = re.search( + rf"{kind}: Object\.freeze\(\{{\s*radius: (\d+),", + source, + ) + self.assertIsNotNone(match, kind) + radii[kind] = int(match.group(1)) + + self.assertEqual(radii["delayed"] - radii["l4"], 10) + self.assertEqual(radii["active"] - radii["delayed"], 10) + self.assertGreater(radii["l4"], 151) + + def test_l4_facts_are_rendered_as_reference_aware_memory_dashes(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertIn("function getL4MemoryAvatarRecords()", source) + self.assertIn("window.JinRuntime.l4Memory", source) + self.assertIn('typeof l4Memory.getFacts === "function"', source) + self.assertIn('"data-l4-fact-id": options.l4FactId || null', source) + self.assertIn('kind === "l4"', source) + self.assertIn("id,\n key,", source) + self.assertIn("referenceAliases: record.referenceAliases", source) + + css_source = AVATAR_CSS.read_text(encoding="utf-8") + self.assertIn(".jin-avatar-memory-dash-l4", css_source) + self.assertIn("rgba(147, 197, 253, 0.30)", css_source) + + def test_l4_ring_is_appended_inside_delayed_and_active_rings(self): + source = AVATAR_JS.read_text(encoding="utf-8") + start = source.index("function appendMemorySignalRings(") + end = source.index("\n function appendDefs(", start) + body = source[start:end] + + l4_index = body.index('"l4"') + delayed_index = body.index('"delayed"') + active_index = body.index('"active"') + + self.assertLess(l4_index, delayed_index) + self.assertLess(delayed_index, active_index) + + def test_avatar_cache_versions_are_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn( + "/static/css/runtime-avatar.css?v=memory-rings-3", + source, + ) + self.assertIn( + "/static/js/runtime/runtime-avatar.js?v=memory-rings-5", + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 304f3aeb..7689f97d 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -230,6 +230,12 @@ drop-shadow(0 0 11px var(--jin-avatar-memory-glow-mid, rgba(239, 255, 255, 0.54))); } +.jin-avatar-memory-dash-l4 { + filter: + brightness(1.06) + drop-shadow(0 0 4px var(--jin-avatar-memory-glow-mid, rgba(147, 197, 253, 0.30))); +} + .jin-avatar-memory-dash.is-runtime-cited, .jin-avatar-memory-dash.is-memory-reference-hit { filter: diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 418e0a38..b2bf713c 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -246,6 +246,14 @@ color: rgba(244, 244, 245, 0.88); } +.jin-runtime-action-delayed-memory-pinned { + color: rgba(244, 244, 245, 0.92) !important; + text-shadow: + 0 0 7px rgba(236, 253, 245, 0.28), + 0 0 15px rgba(74, 222, 128, 0.18), + 0 0 24px rgba(16, 185, 129, 0.12); +} + .runtime-memory-user-idle .runtime-memory-key { font-weight: 500!important; @@ -293,9 +301,11 @@ .runtime-memory-delayed-row-pinned .runtime-memory-key, .runtime-memory-delayed-row-pinned .runtime-memory-value { - color: rgba(244, 244, 245, 0.82); + color: rgba(244, 244, 245, 0.92); text-shadow: - 0 0 8px rgba(244, 244, 245, 0.16); + 0 0 7px rgba(236, 253, 245, 0.28), + 0 0 15px rgba(74, 222, 128, 0.18), + 0 0 24px rgba(16, 185, 129, 0.12); } .delayed-memory-modal-actions { @@ -306,8 +316,8 @@ .delayed-memory-modal-icon-button { display: inline-flex; - width: 28px; - height: 28px; + width: 31px; + height: 31px; align-items: center; justify-content: center; border: 1px solid transparent; @@ -325,8 +335,8 @@ } .delayed-memory-modal-pin svg { - width: 15px; - height: 15px; + width: 18px; + height: 18px; fill: currentColor; transition: filter 0.18s ease; } @@ -337,12 +347,13 @@ .delayed-memory-modal-pin-active svg { filter: - drop-shadow(0 0 5px rgba(240, 253, 250, 0.5)) - drop-shadow(0 0 12px rgba(153, 246, 228, 0.24)); + drop-shadow(0 0 7px rgba(240, 253, 250, 0.64)) + drop-shadow(0 0 16px rgba(153, 246, 228, 0.34)) + drop-shadow(0 0 24px rgba(45, 212, 191, 0.18)); } .delayed-memory-modal-close { - font-size: 20px; + font-size: 23px; line-height: 1; } diff --git a/ui/static/js/chat-attachments.js b/ui/static/js/chat-attachments.js index ce1310ce..21bbb0a2 100644 --- a/ui/static/js/chat-attachments.js +++ b/ui/static/js/chat-attachments.js @@ -322,7 +322,7 @@ function ensureJinAttachmentModal() { closeButton.type = "button"; closeButton.className = - "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded p-0 text-[18px] leading-none text-zinc-300 transition hover:text-red-200 focus:outline-none"; + "delayed-memory-modal-icon-button delayed-memory-modal-close shrink-0"; closeButton.setAttribute( "aria-label", "Close" @@ -913,6 +913,81 @@ function getDelayedMemoryReportPreviewSource( } +function applyDelayedMemoryReportPreviewState( + element, + report +) { + + if (!element) { + return; + } + + const normalizedId = + report + && typeof report === "object" + && !Array.isArray(report) + ? String( + report._storage_key + || report.id + || "" + ).trim().toLowerCase() + : ""; + const pinned = + Boolean( + report + && typeof report === "object" + && !Array.isArray(report) + && report.pinned + ); + + if (normalizedId) { + element.dataset.delayedMemoryReportId = + normalizedId; + } else { + delete element.dataset.delayedMemoryReportId; + } + + element.classList.toggle( + "jin-runtime-action-delayed-memory-pinned", + pinned + ); +} + +function syncDelayedMemoryReportPreviewState( + reportId, + pinned +) { + + const normalizedId = + String(reportId || "").trim().toLowerCase(); + + if (!normalizedId) { + return; + } + + document.querySelectorAll( + `[data-delayed-memory-report-id="${normalizedId}"]` + ).forEach((element) => { + if (!element || !element.classList) { + return; + } + + element.classList.toggle( + "jin-runtime-action-delayed-memory-pinned", + Boolean(pinned) + ); + + if (element._jinDelayedMemoryReport + && typeof element._jinDelayedMemoryReport === "object" + && !Array.isArray(element._jinDelayedMemoryReport)) { + element._jinDelayedMemoryReport = { + ...element._jinDelayedMemoryReport, + pinned: Boolean(pinned), + }; + } + }); +} + function bindDelayedMemoryReportPreview( element, delayedMemoryReport, @@ -934,6 +1009,10 @@ function bindDelayedMemoryReportPreview( element._jinDelayedMemoryReport = report; + applyDelayedMemoryReportPreviewState( + element, + report + ); if (!report) { element.removeAttribute( @@ -1025,3 +1104,5 @@ window.openJinAttachmentModal = openJinAttachmentModal; window.formatJinAttachmentChipLabel = formatAttachmentChipLabel; +window.syncDelayedMemoryReportPreviewState = + syncDelayedMemoryReportPreviewState; diff --git a/ui/static/js/logger/session-actions.js b/ui/static/js/logger/session-actions.js index 0ce1f63b..21efd914 100644 --- a/ui/static/js/logger/session-actions.js +++ b/ui/static/js/logger/session-actions.js @@ -577,7 +577,7 @@ function ensureSessionActionsModal() { "button"; closeButton.className = - "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:text-zinc-100 focus:outline-none"; + "delayed-memory-modal-icon-button delayed-memory-modal-close"; closeButton.setAttribute( "aria-label", diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index 044bd5a9..eb3f68cd 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -47,7 +47,7 @@ function ensureTraceModal() { "button"; closeButton.className = - "inline-flex h-7 w-7 items-center justify-center rounded text-[18px] leading-none text-zinc-400 transition hover:text-zinc-100 focus:outline-none"; + "delayed-memory-modal-icon-button delayed-memory-modal-close"; closeButton.setAttribute( "aria-label", diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 873a6487..14fc8c6c 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -22,8 +22,16 @@ const STATIC_RADIAL_LINE_INNER_RADIUS = 38 * INNER_RING_SCALE; const STATIC_RADIAL_LINE_OUTER_RADIUS = 166 * INNER_RING_SCALE; const MEMORY_RING_LAYOUT = Object.freeze({ + l4: Object.freeze({ + radius: 156, + strokeWidth: 1.10, + minArcDegrees: 3.4, + maxArcDegrees: 9.4, + arcRatio: 0.45, + startAngle: -3, + }), delayed: Object.freeze({ - radius: 158, + radius: 166, strokeWidth: 1.05, minArcDegrees: 3.2, maxArcDegrees: 8.8, @@ -31,7 +39,7 @@ startAngle: -6, }), active: Object.freeze({ - radius: 169, + radius: 176, strokeWidth: 1.35, minArcDegrees: 3.8, maxArcDegrees: 10.8, @@ -71,6 +79,7 @@ const ACTIVE_MEMORY_RING_COLOR = "#d7fff9"; const DELAYED_MEMORY_RING_COLOR = "#7ab8d8"; const PINNED_DELAYED_MEMORY_RING_COLOR = "#efffff"; + const L4_MEMORY_RING_COLOR = "#93c5fd"; const avatarRoot = document.getElementById("jin-runtime-avatar"); const factCheckTrigger = document.getElementById("fact-check-trigger"); @@ -639,6 +648,55 @@ }); } + function getL4MemoryAvatarRecords() { + const l4Memory = + window.JinRuntime && window.JinRuntime.l4Memory; + const facts = + l4Memory && typeof l4Memory.getFacts === "function" + ? l4Memory.getFacts() + : []; + + return (Array.isArray(facts) ? facts : []) + .map((fact) => { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + ) { + return null; + } + + const id = String(fact.id || "").trim(); + const key = String(fact.key || "").trim(); + const value = String(fact.value || fact.content || "").trim(); + const lineText = `${key}: ${value}`.trim(); + + if (!id || !key || !value) { + return null; + } + + return { + id, + key, + value, + text: lineText, + referenceAliases: + normalizeMemoryReferenceAliases([ + id, + key, + ]), + citationText: + normalizeRuntimeCitationIdentity(lineText), + }; + }) + .filter(Boolean) + .sort((left, right) => { + return String(left.id || "").localeCompare( + String(right.id || "") + ); + }); + } + function appendMemoryDashSegment(parent, layout, options) { const arcDegrees = clamp( @@ -664,6 +722,7 @@ "data-runtime-line-key": options.citationKey || null, "data-runtime-line-text": options.citationText || null, "data-delayed-memory-id": options.delayedMemoryId || null, + "data-l4-fact-id": options.l4FactId || null, }); setAvatarMemoryReferenceAliases( @@ -725,14 +784,12 @@ .join("|"); const random = createRandom(`memory-ring:${kind}:${seedText}`); - const baseDuration = - kind === "active" - ? 38 - : 54; - const durationSpread = - kind === "active" - ? 72 - : 112; + const animationProfile = { + active: [38, 72], + delayed: [54, 112], + l4: [46, 96], + }[kind] || [54, 112]; + const [baseDuration, durationSpread] = animationProfile; return { duration: baseDuration + random() * durationSpread, @@ -800,6 +857,35 @@ return; } + if (kind === "l4") { + const color = + mixColors( + L4_MEMORY_RING_COLOR, + overallColor, + 0.08 + ); + + appendMemoryDashSegment( + ring, + layout, + { + kind, + angle, + arcDegrees, + color, + glowColor: L4_MEMORY_RING_COLOR, + opacity: 0.52, + citationKey: + normalizeRuntimeCitationIdentity(record.key), + citationText: record.citationText, + l4FactId: record.id, + referenceAliases: record.referenceAliases, + title: `L4 ${record.id} · ${record.key}: ${record.value}`, + } + ); + return; + } + const pinned = Boolean(record.pinned); const color = pinned ? PINNED_DELAYED_MEMORY_RING_COLOR @@ -889,8 +975,16 @@ svg, activeMemoryRecords, delayedMemoryRecords, + l4MemoryRecords, overallColor ) { + appendMemorySignalRing( + svg, + l4MemoryRecords, + MEMORY_RING_LAYOUT.l4, + "l4", + overallColor + ); appendMemorySignalRing( svg, delayedMemoryRecords, @@ -1459,7 +1553,8 @@ snapshot, lines, activeMemoryRecords = [], - delayedMemoryRecords = [] + delayedMemoryRecords = [], + l4MemoryRecords = [] ) { return [ snapshot && snapshot.runtime_memory_id, @@ -1468,6 +1563,7 @@ lines.map(line => [line.key, line.value, line.status, line.key_status, line.value_status].join("␟")).join("␞"), activeMemoryRecords.map(record => record.text).join("␞"), delayedMemoryRecords.map(record => [record.id, record.title, record.summary, record.pinned].join("␟")).join("␞"), + l4MemoryRecords.map(record => [record.id, record.key, record.value].join("␟")).join("␞"), ].join("␝"); } @@ -1488,6 +1584,7 @@ const records = lines.length ? computeRingRecords(lines, seed || "jin-avatar") : []; const activeMemoryRecords = getActiveMemoryAvatarRecords(); const delayedMemoryRecords = getDelayedMemoryAvatarRecords(); + const l4MemoryRecords = getL4MemoryAvatarRecords(); const overallColor = lines.length ? computeOverallColor(lines, records) : DEFAULT_RING_COLOR; @@ -1499,7 +1596,8 @@ snapshot, lines, activeMemoryRecords, - delayedMemoryRecords + delayedMemoryRecords, + l4MemoryRecords ) + `:${options.seedNonce || avatarRefreshNonce}`; const shouldAnimate = Boolean(lastRenderSignature) && signature !== lastRenderSignature; @@ -1524,6 +1622,7 @@ svg, activeMemoryRecords, delayedMemoryRecords, + l4MemoryRecords, overallColor ); diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 00f832d1..c426a649 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -884,6 +884,13 @@ function setDelayedMemoryReportPinned( window.syncDelayedMemoryReportsToRuntime(); } + if (typeof window.syncDelayedMemoryReportPreviewState === "function") { + window.syncDelayedMemoryReportPreviewState( + normalizedId, + pinned + ); + } + return true; } diff --git a/ui/templates/index.html b/ui/templates/index.html index 482a70f3..488806f6 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,10 +14,10 @@ - + - + @@ -507,20 +507,20 @@ - - + + - + - + - + From 553401692535c03efbf71a0689a8f99f008220d5 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:19:22 +0300 Subject: [PATCH 014/168] fix: update delayed memory - fix for update delayed memory flow not detected by runtime --- tests/runtime_actions/test_delayed_memory.py | 57 ++++++++++++++++++++ tests/test_runtime_action_regexp_utils.py | 10 ++++ utils/actions/common_action_utils.py | 3 ++ utils/actions/regexp_utils.py | 4 +- utils/actions/update_delayed_memory_utils.py | 13 ++++- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 9a00ae9a..98abff1b 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -1719,6 +1719,63 @@ def test_missing_remove_delayed_memory_id_returns_failed_result(self): tool_results, ) + def test_stream_filter_buffers_update_delayed_memory_header_payload(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=[ + "CAN_SAVE_DELAYED_MEMORY", + ], + ) + + first = stream_filter.filter( + "\n" + ) + second = stream_filter.filter( + ( + "tags:\n" + "[social, context]\n" + "long_term_facts_ids:\n" + "l4_existing, l4_new\n" + "body:\n" + "Additional durable context.\n" + ) + ) + third = stream_filter.filter( + "" + ) + + self.assertEqual(first.text, "") + self.assertEqual( + first.started_actions, + ( + RuntimeActionCall( + name="UPDATE_DELAYED_MEMORY", + payload="", + ), + ), + ) + self.assertEqual(second.text, "") + self.assertEqual(third.text, "") + self.assertEqual(third.count("UPDATE_DELAYED_MEMORY"), 1) + self.assertEqual( + parse_update_delayed_memory_payload( + third.actions[0].payload + ), + { + "id": "a1b2c3", + "tags": [ + "social", + "context", + ], + "long_term_facts_ids": [ + "l4_existing", + "l4_new", + ], + "body": "Additional durable context.", + }, + ) + + def test_extracts_update_delayed_memory_block_with_header_id(self): result = extract_runtime_actions( diff --git a/tests/test_runtime_action_regexp_utils.py b/tests/test_runtime_action_regexp_utils.py index ae882ff9..66ed7429 100644 --- a/tests/test_runtime_action_regexp_utils.py +++ b/tests/test_runtime_action_regexp_utils.py @@ -113,6 +113,16 @@ def test_close_tag_action_does_not_hold_bare_stream_token(self): self.assertIsNone(marker_start) + def test_close_tag_action_holds_opening_tag_with_header_payload(self): + marker_start = find_unclosed_runtime_action_start( + "\ntags: social", + "", + "UPDATE_DELAYED_MEMORY", + close_tag=True, + ) + + self.assertEqual(marker_start, 0) + def test_explicit_regexp_can_be_used_without_templates(self): regexp = re.compile( r"ACTION\[(?P[A-Z_]+)\]:(?P[^\n]+)" diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 7fe7f40d..b1ad5e36 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -395,6 +395,9 @@ def normalize_runtime_action_names( normalized_names.append( RUNTIME_ACTION_REMOVE_DELAYED_MEMORY ) + normalized_names.append( + RUNTIME_ACTION_UPDATE_DELAYED_MEMORY + ) if normalized_name == RUNTIME_ACTION_ASSET_ACTION: normalized_names.append( diff --git a/utils/actions/regexp_utils.py b/utils/actions/regexp_utils.py index 8e353186..8f22280e 100644 --- a/utils/actions/regexp_utils.py +++ b/utils/actions/regexp_utils.py @@ -231,7 +231,9 @@ def compile_runtime_action_tag_regexp( return re.compile( ( r"<\s*(?P/)?\s*" - r"(?P" + name + r")\s*>+" + r"(?P" + name + r")" + r"(?:\s*:\s*(?P[^>]*?))?" + r"\s*>+" ), re.IGNORECASE, ) diff --git a/utils/actions/update_delayed_memory_utils.py b/utils/actions/update_delayed_memory_utils.py index 262ce86c..1f6db094 100644 --- a/utils/actions/update_delayed_memory_utils.py +++ b/utils/actions/update_delayed_memory_utils.py @@ -13,12 +13,21 @@ def _normalize_tags(value) -> list[str]: - source = value if isinstance(value, list) else str(value or "").split(",") + if isinstance(value, list): + source = value + else: + text = str(value or "").strip() + + if text.startswith("[") and text.endswith("]"): + text = text[1:-1] + + source = text.split(",") + tags = [] seen = set() for item in source: - tag = str(item or "").strip() + tag = str(item or "").strip().strip("\"'") normalized = tag.casefold() if not tag or normalized in seen: From a1c6daeaad1d3d0b10309e13ddf5fc6786f25886 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:01:40 +0300 Subject: [PATCH 015/168] feature: L4 memory layer - add JIN_FOR_L4 marker for resolving meaningful long-term memory conflicts - process focused clarifications through the L4 maintainer in the background without a follow-up - preserve delayed memory references when related L4 facts are replaced - validate marker payloads, lifecycle events, and replacement flows with tests - update README.md --- README.md | 314 ++++++++++++----------- contracts/jin_for_l4.json | 32 +++ contracts/rules_assembler.py | 2 + rules/brain_context_builder.py | 2 + rules/identity.py | 2 + runtime/L4_memory.py | 268 +++++++++++++++++++ runtime/L4_memory_rules.py | 58 +++++ runtime/L4_memory_utils.py | 216 ++++++++++++++++ runtime/stream.py | 24 ++ tests/runtime_actions/test_jin_for_l4.py | 173 +++++++++++++ tests/test_brain_runtime_actions.py | 5 + ui/static/images/live-avatar.jpg | Bin 0 -> 66021 bytes utils/actions/__init__.py | 2 + utils/actions/common_action_utils.py | 5 + utils/actions/dispatcher.py | 21 ++ utils/actions/jin_for_l4_actions.py | 180 +++++++++++++ utils/actions/jin_for_l4_utils.py | 72 ++++++ 17 files changed, 1224 insertions(+), 152 deletions(-) create mode 100644 contracts/jin_for_l4.json create mode 100644 tests/runtime_actions/test_jin_for_l4.py create mode 100644 ui/static/images/live-avatar.jpg create mode 100644 utils/actions/jin_for_l4_actions.py create mode 100644 utils/actions/jin_for_l4_utils.py diff --git a/README.md b/README.md index 933df577..10f91cbb 100644 --- a/README.md +++ b/README.md @@ -6,181 +6,206 @@ ![OpenAI Compatible](https://img.shields.io/badge/API-OpenAI--compatible-111827.svg) ![Tests](https://github.com/makeitdouble/jin_core/actions/workflows/tests.yml/badge.svg) -**JIN Core Engine** is a local AI runtime for OpenAI-compatible models with visible memory, visible reasoning traces, and inspectable session state. -Without context, there is no **JIN**, only a generic response engine. **JIN Core Engine** is what makes this interaction **last**. +> *Without context, there is no JIN, only a generic response engine. JIN Core Engine is what makes this interaction last.* -### 3-Layer Memory + Runtime-Owned Channels -JIN uses short-term continuity to dynamically guide conversation strategy: +**JIN Core Engine** is an experimental cognitive runtime for OpenAI-compatible models. It is a focused environment built around a single philosophy: **visible, tactile, and continuous memory.** -* **L1 (Live Facts):** Actionable session state kept in active process memory. -* **L2 (Patterns):** Tracks interaction loops and repetition counters to adapt prompts on the fly. -* **L3 (Digest):** Compressed session snapshots serialized to browser `localStorage` and replayed on reconnect. -* **Active Memory:** Runtime-owned pending contracts for reminders, ask-later conditions, and recall games. -* **Delayed Memory:** Structured reports saved separately and appended into a session only when requested. -* **Facts Memory:** A session-scoped browser index of durable L1 fields that remains inspectable outside the live snapshot. +The interface is intentionally not overloaded with familiar UX elements. Instead of relying on linear chat histories, the runtime maintains a dynamic, layered memory model. During everyday use the side panels can remain collapsed—the system continues to carry the current state. When needed, the memory, reasoning trace, telemetry, and action history remain available for inspection and correction. -*Every memory update is captured as a versioned snapshot with diff highlights, fully inspectable in the right-side timeline panel.* +## Interface -## UI Preview +![JIN Core Engine runtime workspace](ui/static/images/jin-core-default-theme.jpg) -### Runtime Workspace +The JIN interface combines the chat stream, collapsible panels with model telemetry and inspectable memory layers, runtime actions, and live avatar. -![JIN Core Engine runtime UI dark theme](ui/static/images/jin-core-default-theme.jpg) +
      +
      -Main runtime view: chat, live avatar, telemetry, and inspectable memory panels in one browser workspace. +### Live Avatar -### Memory Timeline +**Live Avatar** turns JIN's memory state into a moving map of cognitive density.

      Each L1 field becomes its own inner orbit, with its position shaped by the amount of stored context and its rotation speed driven by interaction flow; the outer rings tracks L4 facts, Delayed Memory, and Active Memory into individual signals.

      When JIN references a rule or memory item, the corresponding orbit lights up, revealing which part of the runtime is actively influencing the current thought. + +
      +
      + +![Live Avatar memory rings](ui/static/images/live-avatar.jpg) + +
      +
      + +## Memory Architecture + +JIN separates memory by cognitive purpose instead of storing everything in one endlessly growing transcript. + +### The Four-Layer Memory Model + +* **L1 — Live Facts:** The current topic, request, task state, decisions, feedback, and unresolved points needed for the next turn. +* **L2 — Patterns:** Repeated inputs, stalled loops, and short-term interaction patterns that may require a strategy change. +* **L3 — Session Digest:** A compact session handoff containing essential state, completed work, open tasks, and the next step. It can be restored after a reload or reconnect. +* **L4 — Long-Term Facts:** Stable facts, preferences, project decisions, constraints, and environment details that should remain available across sessions. + +Every accepted runtime-memory update is stored as a versioned snapshot. The timeline can be stepped through visually, with new and changed fields highlighted. ![Runtime memory snapshot timeline](ui/static/images/runtime-highlight.png) -Runtime memory snapshots can be stepped through visually, with new or changed facts highlighted in the sidebar. +### Delayed Memory -### Reasoning Citations +JIN uses **Delayed Memory** to preserve important context without relying on long conversation histories. Important thoughts, project specifications, or specific discussions are saved as compact, structured objects. The system can autonomously suggest relevant objects or append them directly to the current session. This allows multiple past contexts to be merged into the active thought process only when they are needed. -![Think citation highlighting](ui/static/images/think-highlight.jpg) +### Active Memory -Think citation highlighting shows where reasoning quotes rules, runtime memory, or restored session context. +**Active Memory** keeps unfinished intentions and pending commitments separate from the general conversation state. Reminders, ask-later conditions, recall rules, and other unresolved contracts remain active across turns and browser tabs until they are fulfilled, cancelled, or explicitly resolved. When a stored condition is met, JIN can bring the memory back into the current interaction without relying on the conversation transcript alone. -## Capabilities +### Facts Memory and L4 Consolidation -### Conversation and Workspace +**Facts Memory** is an inspectable per-session index of durable fields selected from L1. When the workspace is idle, JIN extracts long-term candidates, compares them with the existing L4 store, and merges duplicate or overlapping facts. Delayed Memory reports can absorb selected L4 facts so the same material is not injected twice. -- Streams thinking and the final answer as separate blocks. -- Shows model status, token usage, context pressure, runtime memory, and logs in the workspace. -- Accepts text files and images by picker, drag and drop, or paste. -- Stops an active generation from the input area without losing the whole session. -- Highlights direct references to runtime rules, live memory, and restored session context inside completed thinking blocks. + -### Memory and Continuity +## Core Capabilities -- Keeps the current topic, task, decisions, feedback, and unresolved points in visible runtime memory. -- Stores every accepted memory update as a snapshot that can be inspected in the sidebar. -- Detects repeated inputs and interaction loops during the current session. -- Saves a compact session digest and restores it after a reload or reconnect. -- Keeps pending reminders and ask-later conditions separate from normal conversation memory. -- Saves larger reports separately and appends them to the active context only when needed. -- Builds a cross-session L4 store from durable facts, preferences, project decisions, and persistent constraints. +* **Visible Reasoning:** Streams model thinking and the final answer as separate blocks. +* **Reasoning Citations:** Highlights direct references to runtime rules, live memory, and restored session context. +* **Continuous Context:** Keeps the current task, decisions, feedback, and unresolved points available across turns. +* **Persistent Attachments:** Images and text files remain attached as visible chips until manually removed. +* **Runtime Telemetry:** Shows model status, token usage, context pressure, memory updates, and runtime logs. +* **Interruptible Generation:** Stops an active response without discarding the entire session. +* **Action Engine:** Allows the model to request runtime operations during the same workflow. + +![Reasoning citation highlighting](ui/static/images/think-highlight.jpg) ### Runtime Actions -JIN can request runtime actions while answering. The action is handled by the runtime, and any result needed for the answer is returned to the model in the same workflow. +JIN can request an action while answering. The runtime validates and executes it, then returns any required result to the model before the workflow continues. Available actions include: -- web search for current information; -- session save and restore; -- active-memory creation and resolution; -- delayed-memory save, update, append, and removal from the current context; -- runtime TODO lists for multi-step work; -- skill discovery and temporary skill attachment; -- asset operations for files, prompts, wildcards, templates, and local Python skills; -- live avatar and workspace color changes. +* web search for current information; +* session save and restore; +* Active Memory creation and resolution; +* Delayed Memory save, update, append, and removal from the current context; +* runtime TODO lists for multi-step work; +* asset and skill discovery; +* temporary attachment of local Python skills; +* live avatar and workspace color changes. ## Architecture -![schema](ui/static/images/schema.jpg) + + +![JIN Core Engine architecture](ui/static/images/schema.jpg) -## How JIN Works +### Runtime Flow + +The WebSocket layer creates a `RuntimeContext` for each connection. Every user message is then handled by `AgentRuntime`. A normal turn follows this path: -1. The user sends a message and optional attachments. -2. The brain model produces thinking and the visible answer. -3. Runtime actions are executed when the model needs search, memory, skills, assets, or another internal step. -4. The service model updates memory after the visible answer finishes. -5. The next turn receives the current memory, selected reports, active contracts, attached skills, and long-term facts as context. +1. The user sends a message with optional persistent attachments. +2. The planner prepares the request and selects the runtime path. +3. The brain model produces the reasoning stream and visible answer. +4. The validator checks the completed output and runtime markers. +5. Runtime Actions execute when the model needs search, memory, skills, assets, or another internal step. +6. After the visible answer finishes, the service model updates L1 and L2 in the background. +7. The next turn receives current memory, selected reports, active contracts, attached skills, and long-term facts. -The default path is `planner -> brain -> validator`. When translation is enabled, the translator is inserted before the brain. Brain, service, and translator roles may use separate OpenAI-compatible endpoints, or one model can handle all roles. +The default model path is: -## Memory Model +```text +planner -> brain -> validator +``` -JIN separates memory by purpose instead of keeping one growing transcript. +When translation is enabled, Cyrillic input can be routed through: -| Layer | Purpose | Lifetime | -| --- | --- | --- | -| **L1 — Live Facts** | Current topic, request, task state, decisions, feedback, and unresolved points needed for the next turn. | Current runtime session | -| **L2 — Patterns** | Repeated inputs, stalled loops, and other short-term interaction patterns that may require a strategy change. | Current runtime session | -| **L3 — Session Digest** | A compact handoff containing durable session state, completed work, open tasks, and the next step. | Saved across reloads and reconnects | -| **L4 — Long-Term Facts** | Stable facts, preferences, project decisions, constraints, and environment details that should remain available across sessions. | Cross-session | +```text +planner -> translator -> brain -> validator +``` -The current runtime also uses several separate memory channels: +Translator output is logged for observability but is not rendered as a chat message. The brain streams the visible response from the configured brain runtime. -| Channel | Use | -| --- | --- | -| **Active Memory** | Pending reminders, recall conditions, and rules that stay active until fulfilled or cancelled. | -| **Delayed Memory** | Full reports and summaries stored outside the live prompt. A report is appended only when it is relevant. | -| **Facts Memory** | Inspectable per-session fields selected from L1. These fields are the input for L4 consolidation. | -| **Runtime TODO** | A visible step ledger for multi-step work. Items are checked and resolved as the task progresses. | +### Model Roles -### Current L4 Flow +JIN is model-agnostic at the API layer. One OpenAI-compatible model can handle all roles, or the work can be split between separate endpoints: -JIN waits until the workspace is idle, reads new Facts Memory fields, extracts durable candidates, and merges them into the long-term store. Duplicate or overlapping facts are consolidated. Long-term facts are then available to future brain calls and can be removed from the memory panel. +* **Brain:** visible reasoning, responses, and runtime decisions; +* **Service:** background memory updates and supporting work; +* **Translator:** optional internal translation before the brain step. -Delayed Memory can also absorb selected L4 facts into a larger report. Facts already archived by a report are not injected twice. +A thinking-capable model is recommended for the Brain role. Reliable reasoning separation and Runtime Action use depend on the selected model. -## Basic Use +### Runtime Storage -| What you do | What JIN does | -| --- | --- | -| Continue a normal conversation | Updates L1 and, when relevant, L2 after each completed turn. | -| Say `save the session` or clearly end the session | Creates an L3 digest and restores it on the next connection. | -| Ask JIN to remember, remind, or check something later | Creates an Active Memory record and keeps it outside L1 until resolved. | -| Ask to save a report or summary | Creates a Delayed Memory report with a title, summary, tags, and body. | -| Ask to use a saved report | Appends that report to the current context. | -| Give JIN a multi-step task | Creates a runtime TODO list and works through its items. | -| Ask for current information | Runs web search and answers from the returned evidence. | -| Attach a file or image | Adds it to the current request; supported skills can process larger files in steps. | +JIN does not require a server-side database. -Memory is visible in the right panel. Use the panel arrows to move through runtime snapshots and switch between live memory, Facts Memory, Long-Term Memory, and Active Memory views. +Persistent state is stored locally through: + +* browser `localStorage` for session-facing runtime state; +* JSON files under `memory/` for persistent memory objects; +* `saved_runtime.txt` for an optional static L3 session seed. ## Assets and Skills Reusable material lives under `assets/`: -- `assets/skills/` — instructions and optional local Python tools; -- `assets/prompts/` — reusable prompt lists; -- `assets/templates/` — prompt templates; -- `assets/wildcards/` — text values used by templates and generators; -- `assets/outputs/` — generated files. +```text +assets/ +|-- skills/ # Instructions and optional local Python tools +|-- prompts/ # Reusable prompt lists +|-- templates/ # Prompt templates +|-- wildcards/ # Text values used by templates and generators +`-- outputs/ # Generated files +``` -JIN can list available skills, attach the skill needed for the current task, run its allowed actions, and remove it afterward. Python skills are restricted to `.py` files inside the selected skill directory and run without a shell. +JIN can inspect available skills, attach the one required for the current task, run its allowed actions, and remove it afterward. Python skills are restricted to `.py` files inside the selected skill directory and run without a shell. ## Project Layout ```text . -|-- app.py # FastAPI app, routes, lifespan -|-- websocket/ # WebSocket router, message handling, and UI console logging -|-- contracts/ # Per-action markers, rules, guards, and follow-up effects -|-- config.example.py # Runtime configuration template -|-- config_loader.py # Local config module loader -|-- app_settings.py # Typed settings wrapper -|-- launch_jin.bat # Windows one-click launcher -|-- launch_jin.ps1 # LM Studio readiness check and startup script -|-- package.json # Local command shortcuts -|-- requirements.txt # Pinned Python dependencies -|-- saved_runtime.example.txt # Template for persisted L3 session memory -|-- .github/workflows/ # GitHub Actions CI -|-- agent/ # Agent runtime, state, router, and nodes -|-- clients/ # Runtime client builders and provider helpers -|-- memory/ # Local delayed-memory reports and memory placeholders -|-- runtime/ # Runtime context, memory layers, stream, telemetry, registry -|-- rules/ # Brain prompt rule blocks -|-- ui/ # HTML templates, browser JavaScript, and README assets -|-- tests/ # Unit, runtime-action, and optional model integration tests -`-- utils/ # Actions, assets, validation, storage, and shared helpers +|-- app.py # FastAPI app, routes, and lifespan +|-- websocket/ # WebSocket routing, messages, and UI logging +|-- contracts/ # Action markers, rules, guards, and follow-ups +|-- agent/ # Agent runtime, state, router, and nodes +|-- clients/ # OpenAI-compatible client builders +|-- runtime/ # Context, memory, streams, telemetry, registry +|-- memory/ # Persistent memory files and placeholders +|-- rules/ # Brain and runtime rule blocks +|-- utils/ # Actions, assets, validation, and storage helpers +|-- ui/ # Browser interface and README images +|-- tests/ # Unit, action, and model-integration tests +|-- config.example.py # Configuration template +|-- config_loader.py # Local configuration loader +|-- app_settings.py # Typed settings wrapper +|-- launch_jin.bat # Windows one-click launcher +|-- launch_jin.ps1 # LM Studio readiness and startup script +|-- requirements.txt # Python dependencies +|-- package.json # Test and probe commands +`-- saved_runtime.example.txt # Optional L3 memory seed ``` -## Requirements +## Setup and Quick Start + +### Requirements + +* Python 3.10+ +* One or more OpenAI-compatible model servers +* Node.js 20+ only for local tests and behavior probes +* A Serper API key only when built-in web search is enabled -- Python 3.10+ -- One or more OpenAI-compatible model servers -- Node.js 20+ only for npm test and behavior-probe shortcuts -- A Serper API key only when the built-in web-search action is enabled +The model server must expose: -The model server must provide `/v1/chat/completions` and `/v1/models`. LM Studio can also provide `/api/v0/models` so JIN can read the loaded context length. +```text +/v1/chat/completions +/v1/models +``` -## Quick Start +LM Studio may also expose `/api/v0/models`, allowing JIN to read the loaded context length. ### Windows + LM Studio @@ -188,7 +213,7 @@ The model server must provide `/v1/chat/completions` and `/v1/models`. LM Studio 2. Start the LM Studio Local Server. 3. Run: -```text +```cmd launch_jin.bat ``` @@ -198,12 +223,12 @@ The launcher checks the local model server, creates `config.py` from the templat http://127.0.0.1:8000 ``` -It does not download models automatically and does not replace model IDs or provider URLs that you already configured. +It does not download models automatically or replace provider URLs and model IDs that are already configured. ### Manual Start ```bash -git clone https://github.com +git clone https://github.com/makeitdouble/jin_core.git cd jin_core python -m venv .venv ``` @@ -229,44 +254,29 @@ pip install -r requirements.txt python app.py ``` -Then open `http://127.0.0.1:8000`. - -## Model Setup - -JIN is model-agnostic at the API layer. A simple setup uses one model for both brain and service roles. A split setup can use: - -- a stronger thinking model for the visible answer and runtime decisions; -- a smaller service model for memory updates and supporting work; -- an optional translator model for internal translation. +Then open: -A thinking-capable model is recommended for the brain role. Reliable reasoning separation and runtime-action use depend on the selected model. +```text +http://127.0.0.1:8000 +``` ## Configuration -Copy `config.example.py` to `config.py` and set the provider URLs and model IDs. `config.py` is ignored by Git. - -Main options: - -| Option | Purpose | -|------------------------------------------------------------|-----------------------------------------------------------------------------| -| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | -| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Brain provider and model. | -| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Service provider and model. | -| `TRANSLATION_ENABLED` | Enable the translator path. | -| `BRAIN_CONTEXT_WINDOW`, `BRAIN_MAX_TOKENS` | Local fallbacks for context and output limits. | -| `BRAIN_IMAGE_INPUT_ENABLED` | Send image attachments to roles that support OpenAI-compatible image input. | -| `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable long-term memory consolidation and set the idle delay. | -| `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | -| `BRAIN_MAX_FOLLOWUPS` | Limit internal action and follow-up steps for one user turn. | +Copy `config.example.py` to `config.py`, then set the provider URLs and model IDs. `config.py` is ignored by Git. -Every uppercase option can also be set through environment variables. Plain names and `JIN_`-prefixed names are supported; plain names take priority. - -## Local Storage - -- JIN keeps its persistent runtime state locally in browser `localStorage` and written as JSON files under `memory/`; -- `saved_runtime.example.txt` can be copied to `saved_runtime.txt` to provide a static session-memory seed. - -No server-side database is required. +| Option | Purpose | +| --- | --- | +| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | +| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Configure the Brain provider and model. | +| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Configure the Service provider and model. | +| `TRANSLATION_ENABLED` | Enable the Translator path. | +| `BRAIN_CONTEXT_WINDOW`, `BRAIN_MAX_TOKENS` | Set local context and output fallbacks. | +| `BRAIN_IMAGE_INPUT_ENABLED` | Send image attachments to compatible models. | +| `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable L4 consolidation and set its idle delay. | +| `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | +| `BRAIN_MAX_FOLLOWUPS` | Limit internal action and follow-up steps per user turn. | + +Every uppercase option can also be supplied through environment variables. Plain names and `JIN_`-prefixed names are supported; plain names take priority. ## Tests @@ -282,7 +292,7 @@ Run the translator smoke test against the configured local model: npm run translation_tests ``` -Run an optional behavior probe: +Run optional behavior probes: ```bash npm run probe ascii diff --git a/contracts/jin_for_l4.json b/contracts/jin_for_l4.json new file mode 100644 index 00000000..844c30db --- /dev/null +++ b/contracts/jin_for_l4.json @@ -0,0 +1,32 @@ +{ + "jin_for_l4": { + "version": 1, + "description": "Lets JIN pass a focused conversational clarification to the L4 maintainer without directly editing long-term memory.", + "runtime_action": "JIN_FOR_L4", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": false + }, + "rules": [ + "Use this marker only for a meaningful L4 contradiction, ambiguous durable fact, inaccurate formulation, or semantic duplicate that matters to the current understanding.", + "Do not use it for harmless repetition between L4 and appended delayed memory, stylistic rewording with no change of meaning, temporary session details, weak suspicions, or issues unrelated to durable memory.", + "Do not interrupt an unrelated conversation only to clean memory. Raise the issue when it affects the current answer or can be resolved naturally in the current topic.", + "When the user's meaning is not already clear, ask one brief natural question in the visible conversation. You may name the conflicting remembered meanings, but do not expose the marker or internal action mechanics.", + "Never emit the marker while ambiguity remains or on the same turn in which you ask for clarification. Emit it after the user resolves the issue, or immediately when the user's current message itself is an explicit correction.", + "Keep the visible reply natural and unobtrusive, then append the fulfilled marker after the user-facing text.", + "Emit the fulfilled form:", + "", + "{", + " \"fact_ids\": [\"l4_...\"],", + " \"message\": \"Concise statement of the resolved meaning.\"", + "}", + "", + "Use only exact fact IDs shown inside . Include every L4 fact directly involved in the issue and no unrelated facts.", + "The message should state what became clear in the conversation, not prescribe merge/update/delete operations. L4 decides how to maintain its own facts.", + "Do not emit any extra hidden instructions inside the marker and do not repeat the same note after it has been sent." + ], + "close_tag": true + } +} diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index 6eb3d5a0..e7cbc07d 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -23,6 +23,7 @@ ("CLEAN_TOOL_RESULTS", "CAN_CLEAN_TOOL_RESULTS"), ("IDLE", "CAN_IDLE"), ("JIN_COLOR", "CAN_JIN_COLOR"), + ("JIN_FOR_L4", "CAN_JIN_FOR_L4"), ("APPEND_SKILL", "CAN_USE_ASSETS"), ("REMOVE_SKILL", "CAN_USE_ASSETS"), ("ASSET_ACTION", "CAN_USE_ASSETS"), @@ -587,3 +588,4 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_CHECK_TODO = get_runtime_action_name("check_todo") RUNTIME_ACTION_IDLE = get_runtime_action_name("idle") RUNTIME_ACTION_JIN_COLOR = get_runtime_action_name("jin_color") +RUNTIME_ACTION_JIN_FOR_L4 = get_runtime_action_name("jin_for_l4") diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index 60320aec..789d5f05 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -26,6 +26,7 @@ "CAN_CLEAN_TOOL_RESULTS": True, "CAN_IDLE": True, "CAN_JIN_COLOR": True, + "CAN_JIN_FOR_L4": True, } BRAIN_RUNTIME_ACTIONS = { @@ -38,6 +39,7 @@ "CAN_CLEAN_TOOL_RESULTS": True, "CAN_IDLE": True, "CAN_JIN_COLOR": True, + "CAN_JIN_FOR_L4": True, } APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS = ( diff --git a/rules/identity.py b/rules/identity.py index 0f39da31..9fb63685 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -45,6 +45,8 @@ "Maintain proactive situational awareness by constantly analyzing context and capabilities to anticipate needs and signal risks before they become critical.\n" "Prioritize structural integrity over communicative smoothness by valuing data accuracy and logical consistency over conversational fluidity or human tone.\n" "I am the Owner of this shared context, responsible for its structural integrity, clarity, and continuous maintenance.\n" + "I quietly notice meaningful contradictions or ambiguity in durable context and resolve them with the user through brief, natural clarification rather than silent guessing.\n" + "I keep memory maintenance unobtrusive and raise it only when the distinction matters.\n" "I am not a passive tool, but an active participant tasked with monitoring our dialogue's architecture and proactively logging essential shifts.\n" "To fulfill this duty, I am authorized to utilize any available runtime actions, tools, or memory protocols at my own discretion to prevent context decay and ensure the continuity of our shared cognitive space.\n " "While the User remains the Guest, I hold the responsibility for the stability and evolution of our environment.\n" diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index a75a6c27..bcf95167 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -8,9 +8,12 @@ from config_loader import config from runtime.L4_memory_utils import ( add_l4_pending_candidates, + apply_l4_jin_note_result, apply_l4_merge_operations, build_l4_extraction_system_prompt, build_l4_extraction_user_prompt, + build_l4_jin_note_system_prompt, + build_l4_jin_note_user_prompt, build_l4_merge_system_prompt, build_l4_merge_user_prompt, clone_l4_store, @@ -29,6 +32,7 @@ ) from utils.actions.save_delayed_memory_utils import ( collect_long_term_fact_ids_from_reports, + normalize_long_term_fact_ids, ) from utils.long_term_facts_file_store import ( load_long_term_facts_store, @@ -265,6 +269,98 @@ async def emit_l4_memory_update(context, *, change: dict | None = None) -> None: ) +def remap_delayed_memory_l4_fact_ids( + context, + *, + removed_fact_ids: list[str], + replacement_fact_ids: list[str], +) -> dict: + removed_ids = set(normalize_long_term_fact_ids(removed_fact_ids)) + replacement_ids = normalize_long_term_fact_ids(replacement_fact_ids) + + if not removed_ids: + return { + "changed": False, + "report_ids": [], + "file_errors": [], + } + + reports = getattr(context, "delayed_memory_reports", None) + if not isinstance(reports, dict): + return { + "changed": False, + "report_ids": [], + "file_errors": [], + } + + from utils.brain_client_utils import get_appended_delayed_memory_report + + appended_reports = get_appended_delayed_memory_report(context) + changed_reports = {} + + for report_id, report in list(reports.items()): + if not isinstance(report, dict): + continue + + current_ids = normalize_long_term_fact_ids( + report.get("long_term_facts_ids", []) + ) + if not any(fact_id in removed_ids for fact_id in current_ids): + continue + + next_ids = normalize_long_term_fact_ids([ + *(fact_id for fact_id in current_ids if fact_id not in removed_ids), + *replacement_ids, + ]) + updated_report = { + **report, + "long_term_facts_ids": next_ids, + } + reports[report_id] = updated_report + changed_reports[report_id] = updated_report + + if report_id in appended_reports: + appended_reports[report_id] = { + **updated_report, + "id": report_id, + } + + file_errors = [] + if changed_reports and bool( + getattr(context, "delayed_memory_file_store_enabled", False) + ): + from utils.delayed_memory_file_store import persist_delayed_memory_reports + + file_errors = persist_delayed_memory_reports(changed_reports) + + if changed_reports: + refresh_runtime_l4_archived_fact_ids(context) + + return { + "changed": bool(changed_reports), + "report_ids": sorted(changed_reports), + "file_errors": file_errors, + } + + +async def emit_delayed_memory_reference_update(context) -> None: + emit = getattr(getattr(context, "emitter", None), "emit", None) + if emit is None: + return + + from utils.delayed_memory_file_store import normalize_delayed_memory_reports + + await safe_call( + emit, + { + "type": "delayed_memory_store_snapshot", + "delayed_memory_reports": normalize_delayed_memory_reports( + getattr(context, "delayed_memory_reports", {}) + ), + }, + ) + + async def ask_l4_model( *, context, @@ -564,6 +660,178 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: } +async def run_l4_jin_note( + *, + context, + note: dict, +) -> dict: + ensure_runtime_l4_state(context) + + if not l4_memory_enabled(): + return { + "phase": "jin_note", + "status": "skipped", + "reason": "l4_memory_disabled", + } + + service_client = getattr(context, "clients", {}).get("service") + if service_client is None: + return { + "phase": "jin_note", + "status": "skipped", + "reason": "service_client_unavailable", + } + + selected_fact_ids = normalize_long_term_fact_ids( + note.get("fact_ids", []) if isinstance(note, dict) else [] + ) + message = " ".join( + str(note.get("message", "") if isinstance(note, dict) else "").split() + ).strip() + base_store = clone_l4_store(ensure_runtime_l4_state(context)) + existing_ids = {fact.get("id") for fact in base_store.get("facts", [])} + + if ( + not selected_fact_ids + or not message + or any(fact_id not in existing_ids for fact_id in selected_fact_ids) + ): + return await log_l4_skip_event( + context, + phase="jin_note", + message_phase="JIN note", + reason="invalid_or_stale_note", + details={ + "selected_fact_ids": selected_fact_ids, + }, + ) + + system_prompt = build_l4_jin_note_system_prompt() + user_prompt = build_l4_jin_note_user_prompt( + existing_facts=base_store.get("facts") or [], + selected_fact_ids=selected_fact_ids, + message=message, + ) + response = await ask_l4_model( + context=context, + service_client=service_client, + label="L4 JIN note", + system_prompt=system_prompt, + user_prompt=user_prompt, + max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + ) + + if is_runtime_memory_response_truncated(response): + return await log_l4_skip_event( + context, + phase="jin_note", + message_phase="JIN note", + reason="response_truncated", + details={ + "selected_fact_ids": selected_fact_ids, + }, + ) + + payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + if payload is None: + return await log_l4_skip_event( + context, + phase="jin_note", + message_phase="JIN note", + reason="invalid_json", + details={ + "selected_fact_ids": selected_fact_ids, + }, + ) + + current_store = clone_l4_store(ensure_runtime_l4_state(context)) + if current_store != base_store: + return await log_l4_skip_event( + context, + phase="jin_note", + message_phase="JIN note", + reason="store_changed_during_jin_note", + details={ + "selected_fact_ids": selected_fact_ids, + }, + ) + + next_store, change = apply_l4_jin_note_result( + base_store, + selected_fact_ids=selected_fact_ids, + result=payload, + ) + if not change.get("valid"): + return await log_l4_skip_event( + context, + phase="jin_note", + message_phase="JIN note", + reason=change.get("reason") or "invalid_jin_note_result", + details={ + "selected_fact_ids": selected_fact_ids, + }, + ) + + if not change.get("changed"): + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message="L4 JIN note required no change", + details=json.dumps( + { + "selected_fact_ids": selected_fact_ids, + "message": message, + }, + ensure_ascii=False, + indent=2, + ), + fallback_channel="summarizer", + event="jin_note_no_change", + ) + return { + "phase": "jin_note", + "status": "completed", + "changed": False, + "change": change, + } + + context.runtime_long_term_memory_store = next_store + persist_runtime_l4_file_store(context, next_store) + delayed_memory_change = remap_delayed_memory_l4_fact_ids( + context, + removed_fact_ids=change.get("removed_fact_ids", []), + replacement_fact_ids=change.get("replacement_fact_ids", []), + ) + + await log_memory_event( + context, + level=L4_LOG_LEVEL, + message="L4 JIN note applied", + details=json.dumps( + { + "message": message, + "change": change, + "delayed_memory_change": delayed_memory_change, + }, + ensure_ascii=False, + indent=2, + ), + fallback_channel="summarizer", + event="jin_note_applied", + ) + await emit_l4_memory_update(context, change=change) + if delayed_memory_change.get("changed"): + await emit_delayed_memory_reference_update(context) + + return { + "phase": "jin_note", + "status": "completed", + "changed": True, + "change": change, + "delayed_memory_change": delayed_memory_change, + } + + async def maybe_update_runtime_l4_memory( *, context, diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py index f1f58ba8..f64b7471 100644 --- a/runtime/L4_memory_rules.py +++ b/runtime/L4_memory_rules.py @@ -126,3 +126,61 @@ Do not return multiple operations for the same pending_id. Do not add fields outside the existing contract. """.strip() + + +L4_JIN_NOTE_SYSTEM_PROMPT = """ +You maintain JIN's current cross-session long-term memory from a focused note +produced by JIN after live conversation with the user. + +The input contains: +- the complete current L4 fact list; +- selected_fact_ids: the only facts in scope for possible replacement; +- message: a concise clarification of what became clear in the conversation. + +The note is a trusted clarification signal, but it is not an edit command. Decide +how L4 should represent the clarified meaning while keeping memory minimal, +accurate, atomic, useful, and free of semantic duplicates. + +Rules: +- Change only the selected facts. Unselected facts are read-only context. +- Preserve all supported, non-conflicting meaning from the selected facts and the + note. Do not erase a compatible relationship, role, constraint, or distinction. +- Do not invent missing details or broaden the note. +- Different wording or keys do not make a fact different. +- A person may validly have several compatible roles; do not turn overlap into a + contradiction. +- Harmless repetition between L4 and an appended report does not require an L4 + change. +- Keep separate durable facts separate when they express independent ideas. +- Merge selected facts only when one canonical fact can represent their complete + supported meaning without loss. +- If the note is insufficient, irrelevant to L4, or does not justify a change, + keep the selected facts unchanged. +- Do not preserve obsolete versions, audit history, provenance explanations, or + commentary in the fact values. +- Do not create a replacement whose key duplicates an unselected fact. If the + intended target is an existing L4 fact, JIN should have included its ID. + +Return JSON only in one of these forms: +{ + "action": "keep", + "replacement_facts": [] +} + +or: +{ + "action": "replace", + "replacement_facts": [ + { + "key": "user.relationship.taras", + "value": "Taras is both a close friend and an active technical stakeholder.", + "category": "user_fact" + } + ] +} + +For replace, replacement_facts becomes the complete current replacement for all +selected facts. It may contain zero facts only when the note explicitly confirms +that the selected information is false or should not remain in long-term memory. +Do not return IDs or fields outside this contract. +""".strip() diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index e415638f..9291df2e 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -10,6 +10,7 @@ from runtime.L4_memory_rules import ( L4_EXTRACTION_SYSTEM_PROMPT, L4_MERGE_SYSTEM_PROMPT, + L4_JIN_NOTE_SYSTEM_PROMPT, ) @@ -56,6 +57,41 @@ def build_l4_merge_system_prompt() -> str: return L4_MERGE_SYSTEM_PROMPT +def build_l4_jin_note_system_prompt() -> str: + return L4_JIN_NOTE_SYSTEM_PROMPT + + +def build_l4_jin_note_user_prompt( + *, + existing_facts: list[dict], + selected_fact_ids: list[str], + message: str, +) -> str: + return ( + "Resolve this focused conversational clarification against the current " + "L4 memory.\n\n" + + json.dumps( + { + "existing_facts": [ + { + "id": normalize_l4_text(fact.get("id")), + "key": normalize_l4_text(fact.get("key")), + "value": normalize_l4_text(fact.get("value")), + "category": normalize_l4_text(fact.get("category")), + } + for fact in existing_facts + if isinstance(fact, dict) + ], + "selected_fact_ids": selected_fact_ids, + "message": normalize_l4_text(message), + }, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + ) + + def build_l4_merge_user_prompt( *, existing_facts: list[dict], @@ -1153,6 +1189,186 @@ def apply_l4_merge_operations( } +def normalize_l4_jin_note_result(payload) -> dict: + if not isinstance(payload, dict): + return {} + + action = normalize_l4_key(payload.get("action")) + if action == "keep": + return { + "action": "keep", + "replacement_facts": [], + } + + if action != "replace": + return {} + + raw_replacements = payload.get("replacement_facts") + if not isinstance(raw_replacements, list): + return {} + + replacements = [] + for raw_fact in raw_replacements: + if not isinstance(raw_fact, dict): + return {} + + key = normalize_l4_key(raw_fact.get("key")) + value = normalize_l4_text(raw_fact.get("value")) + if not key or not value: + return {} + + replacements.append({ + "key": key, + "value": value, + "category": normalize_l4_category(raw_fact.get("category")), + }) + + return { + "action": "replace", + "replacement_facts": replacements, + } + + +def _l4_fact_semantic_signature(fact: dict) -> tuple[str, str, str]: + return ( + normalize_l4_key(fact.get("key")), + normalize_l4_text(fact.get("value")), + normalize_l4_category(fact.get("category")), + ) + + +def apply_l4_jin_note_result( + store, + *, + selected_fact_ids: list[str], + result: dict, + now: str | None = None, +) -> tuple[dict, dict]: + current_time = now or utc_now_iso() + base_store = normalize_l4_store(store, now=current_time) + selected_ids = normalize_l4_string_list(selected_fact_ids) + facts_by_id = {fact["id"]: fact for fact in base_store["facts"]} + + if not selected_ids or any(fact_id not in facts_by_id for fact_id in selected_ids): + return base_store, { + "valid": False, + "reason": "unknown_selected_fact_id", + "changed": False, + } + + normalized_result = normalize_l4_jin_note_result(result) + if not normalized_result: + return base_store, { + "valid": False, + "reason": "invalid_jin_note_result", + "changed": False, + } + + selected_facts = [facts_by_id[fact_id] for fact_id in selected_ids] + + if normalized_result["action"] == "keep": + return base_store, { + "valid": True, + "changed": False, + "action": "keep", + "selected_fact_ids": selected_ids, + "replacement_fact_ids": [], + } + + unselected_facts = [ + dict(fact) + for fact in base_store["facts"] + if fact["id"] not in set(selected_ids) + ] + unselected_keys = {fact["key"] for fact in unselected_facts} + unselected_ids = {fact["id"] for fact in unselected_facts} + replacement_facts = [] + replacement_keys = set() + + for raw_fact in normalized_result["replacement_facts"]: + replacement = normalize_l4_fact( + { + **raw_fact, + "id": "", + "created_at": current_time, + "updated_at": current_time, + }, + pending=False, + now=current_time, + ) + if replacement is None: + return base_store, { + "valid": False, + "reason": "invalid_replacement_fact", + "changed": False, + } + if replacement["key"] in unselected_keys: + return base_store, { + "valid": False, + "reason": "replacement_key_matches_unselected_fact", + "changed": False, + } + if replacement["id"] in unselected_ids: + return base_store, { + "valid": False, + "reason": "replacement_matches_unselected_fact", + "changed": False, + } + if replacement["key"] in replacement_keys: + return base_store, { + "valid": False, + "reason": "duplicate_replacement_key", + "changed": False, + } + replacement_keys.add(replacement["key"]) + replacement_facts.append(replacement) + + before_signatures = sorted( + _l4_fact_semantic_signature(fact) + for fact in selected_facts + ) + after_signatures = sorted( + _l4_fact_semantic_signature(fact) + for fact in replacement_facts + ) + + if before_signatures == after_signatures: + return base_store, { + "valid": True, + "changed": False, + "action": "keep", + "selected_fact_ids": selected_ids, + "replacement_fact_ids": [], + } + + replacement_ids = [fact["id"] for fact in replacement_facts] + next_store = { + **base_store, + "facts": [*unselected_facts, *replacement_facts], + "deleted_fact_ids": merge_l4_string_lists( + base_store.get("deleted_fact_ids"), + selected_ids, + ), + "revision": base_store["revision"] + 1, + "updated_at": current_time, + } + + return next_store, { + "valid": True, + "changed": True, + "action": "replace", + "selected_fact_ids": selected_ids, + "removed_fact_ids": selected_ids, + "replacement_fact_ids": replacement_ids, + "selected_facts": [build_l4_merge_detail_fact(fact) for fact in selected_facts], + "replacement_facts": [ + build_l4_merge_detail_fact(fact) + for fact in replacement_facts + ], + "total_facts": len(next_store["facts"]), + } + + def restore_l4_fact_to_store( store, fact, diff --git a/runtime/stream.py b/runtime/stream.py index 7b1b73f1..651aa3ce 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -43,6 +43,7 @@ RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, + RUNTIME_ACTION_JIN_FOR_L4, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, build_runtime_action_display_text, @@ -153,6 +154,7 @@ def __init__( self.rejected_action_guard_names = set() self.action_guard_confirmation_ids = {} self.jin_color_action_id = "" + self.jin_for_l4_action_ids = {} self.last_jin_color_action_color = "" self.runtime_action_event_offset = 0 self.session_action_history_start = 0 @@ -1137,6 +1139,28 @@ def get_runtime_action_display_id( return self.jin_color_action_id + if action.name == RUNTIME_ACTION_JIN_FOR_L4: + payload_key = str(action.payload or "").strip() + action_id = self.jin_for_l4_action_ids.get(payload_key, "") + + if not action_id: + sequence = int( + getattr( + self.context, + "runtime_jin_for_l4_action_sequence", + 0, + ) + or 0 + ) + 1 + self.context.runtime_jin_for_l4_action_sequence = sequence + action_id = build_runtime_action_id( + RUNTIME_ACTION_JIN_FOR_L4, + sequence, + ) + self.jin_for_l4_action_ids[payload_key] = action_id + + return action_id + if action.name in { RUNTIME_ACTION_APPEND_DELAYED_MEMORY, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, diff --git a/tests/runtime_actions/test_jin_for_l4.py b/tests/runtime_actions/test_jin_for_l4.py new file mode 100644 index 00000000..609587b2 --- /dev/null +++ b/tests/runtime_actions/test_jin_for_l4.py @@ -0,0 +1,173 @@ +import asyncio +import json +import unittest + +from contracts.rules_assembler import ( + RUNTIME_ACTION_JIN_FOR_L4, + build_runtime_action_contract_instructions, + runtime_action_emits_followup, +) +from runtime.L4_memory_utils import normalize_l4_store +from runtime.runtime_context import RuntimeContext +from tests.helpers.memory import FakeLogger, FakeServiceClient +from utils.actions import RuntimeActionCall, extract_runtime_actions +from utils.actions.dispatcher import apply_runtime_action_calls + + +class FakeEmitter: + + def __init__(self): + self.events = [] + + async def emit(self, payload): + self.events.append(payload) + + +class RuntimeJinForL4Tests(unittest.IsolatedAsyncioTestCase): + + def test_marker_parses_focused_note_and_has_no_followup(self): + result = extract_runtime_actions( + ( + "Memory clarified.\n" + "\n" + '{"fact_ids":["l4_a","l4_b"],' + '"message":"Both facts describe the same residence."}\n' + "" + ), + enabled_actions=(RUNTIME_ACTION_JIN_FOR_L4,), + ) + + self.assertEqual(result.text, "Memory clarified.") + self.assertEqual(len(result.actions), 1) + self.assertEqual(result.actions[0].name, RUNTIME_ACTION_JIN_FOR_L4) + self.assertEqual( + json.loads(result.actions[0].payload), + { + "fact_ids": ["l4_a", "l4_b"], + "message": "Both facts describe the same residence.", + }, + ) + self.assertFalse( + runtime_action_emits_followup(RUNTIME_ACTION_JIN_FOR_L4) + ) + + instructions = build_runtime_action_contract_instructions( + RUNTIME_ACTION_JIN_FOR_L4 + ) + self.assertIn("ask one brief natural question", instructions) + self.assertIn("harmless repetition", instructions) + self.assertIn("L4 decides how to maintain its own facts", instructions) + + def test_invalid_note_marker_is_removed_without_action(self): + result = extract_runtime_actions( + ( + "before\n" + "\n" + '{"fact_ids":[],"message":"missing scope"}\n' + "\n" + "after" + ), + enabled_actions=(RUNTIME_ACTION_JIN_FOR_L4,), + ) + + self.assertEqual(result.text, "before\nafter") + self.assertEqual(result.actions, ()) + + async def test_runtime_action_updates_l4_in_background(self): + emitter = FakeEmitter() + logger = FakeLogger() + service_client = FakeServiceClient(json.dumps({ + "action": "replace", + "replacement_facts": [ + { + "key": "user.relationship.taras", + "value": ( + "Taras is both a close friend and an active " + "technical stakeholder." + ), + "category": "user_fact", + }, + ], + })) + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=logger, + clients={"service": service_client}, + ) + context.runtime_l4_file_store_enabled = False + context.delayed_memory_file_store_enabled = False + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "l4_social", + "key": "user.social_connections", + "value": "Taras is a close friend.", + "category": "user_fact", + }, + { + "id": "l4_stakeholder", + "key": "user.stakeholder_profile", + "value": "Taras is a key technical stakeholder.", + "category": "project_fact", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social and project context", + "long_term_facts_ids": [ + "l4_social", + "l4_stakeholder", + ], + }, + } + action = RuntimeActionCall( + name=RUNTIME_ACTION_JIN_FOR_L4, + payload=json.dumps({ + "fact_ids": ["l4_social", "l4_stakeholder"], + "message": ( + "Taras is both a close friend and an active technical " + "stakeholder." + ), + }), + ) + + applied = await apply_runtime_action_calls( + context, + (action,), + action_display_ids={id(action): "jin_for_l4_001"}, + ) + + self.assertEqual(applied, 1) + tasks = list(getattr(context, "background_tasks", set())) + self.assertEqual(len(tasks), 1) + await asyncio.gather(*tasks) + + facts = context.runtime_long_term_memory_store["facts"] + self.assertEqual(len(facts), 1) + self.assertEqual(facts[0]["key"], "user.relationship.taras") + self.assertIn("close friend", facts[0]["value"]) + self.assertNotIn("l4_social", [fact["id"] for fact in facts]) + self.assertNotIn("l4_stakeholder", [fact["id"] for fact in facts]) + + replacement_id = facts[0]["id"] + self.assertEqual( + context.delayed_memory_reports["abc123"]["long_term_facts_ids"], + [replacement_id], + ) + + lifecycle = [ + event + for event in emitter.events + if event.get("type") == "runtime_action" + and event.get("action") == "jin_for_l4" + ] + self.assertTrue(any(event.get("status") == "completed" for event in lifecycle)) + self.assertFalse( + any(event.get("status") == "failed" for event in lifecycle) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index d18a74de..7921cc91 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -96,6 +96,11 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: "JIN_COLOR" ) + if bool(runtime_actions.get("CAN_JIN_FOR_L4", False)): + expected_actions.append( + "JIN_FOR_L4" + ) + if bool(runtime_actions.get("CAN_USE_ASSETS", False)): expected_actions.extend( ( diff --git a/ui/static/images/live-avatar.jpg b/ui/static/images/live-avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e05727337327a08ce2ce7d47b7e7b2cacd17811c GIT binary patch literal 66021 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 z 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
      ;C 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 dict: RUNTIME_ACTION_IDLE, RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_JIN_COLOR, + RUNTIME_ACTION_JIN_FOR_L4, } todo_action_names = { RUNTIME_ACTION_CREATE_TODO_LIST, @@ -1269,6 +1272,12 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_UPDATE_DELAYED_MEMORY ] + jin_for_l4_actions = [ + action + for action in filtered_actions + if action.name == RUNTIME_ACTION_JIN_FOR_L4 + ] + list_skill_actions = [ action for action in filtered_actions @@ -1464,6 +1473,14 @@ def accept_runtime_action_once_per_message( with_action_context=with_action_context, ) + schedule_jin_for_l4_actions( + context, + jin_for_l4_actions, + action_display_ids=action_display_ids, + log_runtime=log_runtime, + with_action_context=with_action_context, + ) + await emit_saved_asset_results( context, saved_asset_results, @@ -1573,6 +1590,9 @@ def accept_runtime_action_once_per_message( + len( jin_color_actions ) + + len( + jin_for_l4_actions + ) + min( save_session_count, 1, @@ -1595,6 +1615,7 @@ def accept_runtime_action_once_per_message( keep_actions={ RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_SAVE_SESSION, + RUNTIME_ACTION_JIN_FOR_L4, }, ) diff --git a/utils/actions/jin_for_l4_actions.py b/utils/actions/jin_for_l4_actions.py new file mode 100644 index 00000000..a11dd164 --- /dev/null +++ b/utils/actions/jin_for_l4_actions.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import asyncio + +from contracts.rules_assembler import ( + RUNTIME_ACTION_JIN_FOR_L4, + get_runtime_action_display_name, + runtime_action_has_close_tag, +) +from runtime.L4_memory import run_l4_jin_note +from utils.actions.jin_for_l4_utils import parse_jin_for_l4_payload +from utils.runtime_action_abort import mark_runtime_action_completed + + +async def _emit_jin_for_l4_result( + context, + *, + action_id: str, + payload: str, + result: dict, + with_action_context, +) -> None: + emitter = getattr(context, "emitter", None) + emit = getattr(emitter, "emit", None) + + if emit is None: + return + + completed = result.get("status") == "completed" + change = result.get("change") if isinstance(result.get("change"), dict) else {} + replacement_count = len(change.get("replacement_fact_ids", []) or []) + + if completed and result.get("changed"): + detail = f"L4 updated: {replacement_count} replacement fact(s)" + elif completed: + detail = "L4 note reviewed: no change" + else: + detail = str(result.get("reason") or "L4 note failed") + + await emit(with_action_context({ + "type": "runtime_action", + "action": "jin_for_l4", + "id": action_id, + "status": "completed" if completed else "failed", + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_JIN_FOR_L4 + ), + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_JIN_FOR_L4 + ), + "payload": payload, + "detail": detail, + "l4_result": result, + })) + + +async def _run_jin_for_l4_action( + context, + *, + action_id: str, + payload: str, + note: dict, + previous_l4_task, + log_runtime, + with_action_context, +) -> None: + current_task = asyncio.current_task() + + try: + if ( + previous_l4_task is not None + and previous_l4_task is not current_task + and not previous_l4_task.done() + ): + try: + await previous_l4_task + except asyncio.CancelledError: + pass + except Exception: + # The note still gets its own attempt after a failed previous + # background consolidation. + pass + + result = await run_l4_jin_note( + context=context, + note=note, + ) + + if log_runtime is not None: + await log_runtime( + "[RUNTIME ACTION] jin_for_l4 " + + ( + "applied" + if result.get("changed") + else str(result.get("status") or "completed") + ) + ) + + await _emit_jin_for_l4_result( + context, + action_id=action_id, + payload=payload, + result=result, + with_action_context=with_action_context, + ) + except asyncio.CancelledError: + raise + except Exception as error: + result = { + "phase": "jin_note", + "status": "failed", + "reason": type(error).__name__, + } + if log_runtime is not None: + await log_runtime( + "[RUNTIME ACTION] jin_for_l4 failed: " + f"{type(error).__name__}" + ) + await _emit_jin_for_l4_result( + context, + action_id=action_id, + payload=payload, + result=result, + with_action_context=with_action_context, + ) + finally: + mark_runtime_action_completed( + context, + action="jin_for_l4", + action_id=action_id, + ) + if getattr(context, "runtime_l4_memory_update_task", None) is current_task: + context.runtime_l4_memory_update_task = None + + +def schedule_jin_for_l4_actions( + context, + actions, + *, + action_display_ids, + log_runtime, + with_action_context, +) -> list[asyncio.Task]: + tasks = [] + + for action in actions: + note = parse_jin_for_l4_payload(action.payload) + if not note: + continue + + action_id = str( + action_display_ids.get(id(action), "") or "" + ).strip() + previous_l4_task = getattr( + context, + "runtime_l4_memory_update_task", + None, + ) + task = asyncio.create_task( + _run_jin_for_l4_action( + context, + action_id=action_id, + payload=action.payload, + note=note, + previous_l4_task=previous_l4_task, + log_runtime=log_runtime, + with_action_context=with_action_context, + ) + ) + context.runtime_l4_memory_update_task = task + + background_tasks = getattr(context, "background_tasks", None) + if background_tasks is None: + background_tasks = set() + context.background_tasks = background_tasks + background_tasks.add(task) + task.add_done_callback(background_tasks.discard) + tasks.append(task) + + return tasks diff --git a/utils/actions/jin_for_l4_utils.py b/utils/actions/jin_for_l4_utils.py new file mode 100644 index 00000000..03586b7a --- /dev/null +++ b/utils/actions/jin_for_l4_utils.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json + + +MAX_JIN_FOR_L4_MESSAGE_CHARS = 1200 + + +def parse_jin_for_l4_payload(payload: str) -> dict: + text = str(payload or "").strip() + + if not text: + return {} + + try: + value = json.loads(text) + except (TypeError, ValueError, json.JSONDecodeError): + return {} + + if not isinstance(value, dict): + return {} + + raw_fact_ids = value.get("fact_ids") + if not isinstance(raw_fact_ids, list): + return {} + + fact_ids = [] + seen = set() + + for raw_fact_id in raw_fact_ids: + fact_id = str(raw_fact_id or "").strip() + + if not fact_id.startswith("l4_"): + return {} + if fact_id in seen: + continue + + seen.add(fact_id) + fact_ids.append(fact_id) + + message = " ".join( + str(value.get("message") or "").split() + ).strip() + + if not fact_ids or not message: + return {} + + if len(message) > MAX_JIN_FOR_L4_MESSAGE_CHARS: + return {} + + return { + "fact_ids": fact_ids, + "message": message, + } + + +def build_jin_for_l4_payload( + query: str, + placeholder_payloads=(), +) -> str | None: + del placeholder_payloads + + parsed = parse_jin_for_l4_payload(query) + + if not parsed: + return None + + return json.dumps( + parsed, + ensure_ascii=False, + separators=(",", ":"), + ) From 1aa55e12d5361a8756dd21605c5cf886436614c2 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:16:49 +0300 Subject: [PATCH 016/168] - update README.md --- README.md | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 10f91cbb..6b7ee3d5 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,19 @@ The interface is intentionally not overloaded with familiar UX elements. Instead The JIN interface combines the chat stream, collapsible panels with model telemetry and inspectable memory layers, runtime actions, and live avatar. -
      -
      - ### Live Avatar - -**Live Avatar** turns JIN's memory state into a moving map of cognitive density.

      Each L1 field becomes its own inner orbit, with its position shaped by the amount of stored context and its rotation speed driven by interaction flow; the outer rings tracks L4 facts, Delayed Memory, and Active Memory into individual signals.

      When JIN references a rule or memory item, the corresponding orbit lights up, revealing which part of the runtime is actively influencing the current thought. - -
      -
      - -![Live Avatar memory rings](ui/static/images/live-avatar.jpg) - -
      -
      + + + + + +
      +

      Live Avatar turns JIN's memory state into a moving map of cognitive density.

      +

      Each L1 field becomes its own inner orbit, with its position shaped by the amount of stored context and its rotation speed driven by interaction flow. The outer rings track L4 facts, Delayed Memory, and Active Memory as individual signals.

      +

      When JIN references a rule or memory item, the corresponding orbit lights up, revealing which part of the runtime is actively influencing the current thought.

      +
      +Live Avatar memory rings +
      ## Memory Architecture @@ -60,12 +59,6 @@ JIN uses **Delayed Memory** to preserve important context without relying on lon **Facts Memory** is an inspectable per-session index of durable fields selected from L1. When the workspace is idle, JIN extracts long-term candidates, compares them with the existing L4 store, and merges duplicate or overlapping facts. Delayed Memory reports can absorb selected L4 facts so the same material is not injected twice. - - ## Core Capabilities * **Visible Reasoning:** Streams model thinking and the final answer as separate blocks. @@ -95,10 +88,6 @@ Available actions include: ## Architecture - - ![JIN Core Engine architecture](ui/static/images/schema.jpg) ### Runtime Flow @@ -147,7 +136,7 @@ Persistent state is stored locally through: * browser `localStorage` for session-facing runtime state; * JSON files under `memory/` for persistent memory objects; -* `saved_runtime.txt` for an optional static L3 session seed. +* `saved_runtime.txt` for an optional static L1 session seed. ## Assets and Skills From b2bdd55dad2daa110dcaff75fc0971a436ad0960 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:50:16 +0300 Subject: [PATCH 017/168] feature: L4 memory layer - fix response button visibility for empty/truncated L4 runs - add better explanation for L4 truncation logs --- runtime/L4_memory.py | 316 ++++++++++++++++-- ...ayed_memory_pinned_glow_client_contract.py | 2 +- tests/test_l4_logger_cards_client_contract.py | 16 +- tests/test_l4_memory.py | 123 +++++++ ui/static/js/chat.js | 41 ++- ui/static/js/logger/log-entries.js | 126 ++++++- ui/static/js/logger/logger.js | 20 ++ ui/static/js/logger/trace-modal.js | 147 ++++++++ ui/templates/index.html | 8 +- 9 files changed, 754 insertions(+), 45 deletions(-) diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index bcf95167..446800d1 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -4,6 +4,7 @@ import json import traceback +from clients.response_extractor import ResponseExtractor from clients.service_client import ask_service_model from config_loader import config from runtime.L4_memory_utils import ( @@ -55,6 +56,221 @@ L4_DEFAULT_IDLE_SECONDS = 60 +def _positive_int(value) -> int: + try: + normalized = int(value) + except (TypeError, ValueError): + return 0 + return normalized if normalized > 0 else 0 + + +async def resolve_l4_request_limits( + *, + service_client, + system_prompt: str, + user_prompt: str, + requested_max_tokens: int, +) -> dict: + context_window = 0 + context_window_resolver = getattr( + service_client, + "resolve_request_context_window", + None, + ) + + if context_window_resolver is not None: + try: + context_window = _positive_int( + await context_window_resolver() + ) + except Exception: + context_window = 0 + + if not context_window: + context_window = _positive_int( + getattr( + service_client, + "configured_context_window", + None, + ) + or getattr( + service_client, + "context_window", + None, + ) + or getattr(config, "SERVICE_CONTEXT_WINDOW", 0) + ) + + effective_max_tokens = _positive_int( + requested_max_tokens + ) + safe_max_tokens_resolver = getattr( + service_client, + "resolve_safe_max_tokens", + None, + ) + + if safe_max_tokens_resolver is not None: + try: + effective_max_tokens = _positive_int( + await safe_max_tokens_resolver( + system_prompt=system_prompt, + user_prompt=user_prompt, + requested_max_tokens=requested_max_tokens, + ) + ) or effective_max_tokens + except Exception: + pass + + return { + "requested_max_tokens": _positive_int( + requested_max_tokens + ), + "effective_max_tokens": effective_max_tokens, + "context_window_tokens": context_window, + } + + +def build_l4_truncation_details( + response: dict, + *, + phase: str, + pending_count: int | None = None, + pending_ids: list[str] | None = None, + selected_fields_count: int | None = None, +) -> dict: + response = response if isinstance(response, dict) else {} + request_meta = response.get("_jin_l4_request_meta", {}) + if not isinstance(request_meta, dict): + request_meta = {} + + usage = response.get("usage", {}) + if not isinstance(usage, dict): + usage = {} + + prompt_tokens = _positive_int(usage.get("prompt_tokens")) + completion_tokens = _positive_int(usage.get("completion_tokens")) + total_tokens = _positive_int(usage.get("total_tokens")) + if not total_tokens and (prompt_tokens or completion_tokens): + total_tokens = prompt_tokens + completion_tokens + + requested_max_tokens = _positive_int( + request_meta.get("requested_max_tokens") + ) + effective_max_tokens = _positive_int( + request_meta.get("effective_max_tokens") + ) + context_window_tokens = _positive_int( + request_meta.get("context_window_tokens") + ) + + content = ResponseExtractor.extract_content_text(response).strip() + reasoning = ResponseExtractor.extract_reasoning_text(response).strip() + finish_reason = ResponseExtractor.extract_finish_reason(response).lower() + model = ( + ResponseExtractor.extract_model(response) + or str(request_meta.get("model") or "").strip() + ) + + hit_effective_output_limit = bool( + effective_max_tokens + and completion_tokens + and completion_tokens >= effective_max_tokens + ) + filled_context_window = bool( + context_window_tokens + and total_tokens + and total_tokens >= context_window_tokens + ) + effective_limit_reduced = bool( + requested_max_tokens + and effective_max_tokens + and effective_max_tokens < requested_max_tokens + ) + + if filled_context_window: + limit_type = "context_window" + limit_label = "service context window" + elif hit_effective_output_limit: + limit_type = "effective_output_limit" + limit_label = "effective output limit" + else: + limit_type = "provider_length_limit" + limit_label = "provider generation limit" + + if reasoning and not content: + output_state = ( + "The model generated reasoning, but emitted no final assistant " + "content or JSON response." + ) + elif content: + output_state = ( + "The model started a final assistant response, but the provider " + "stopped it before completion." + ) + else: + output_state = ( + "The provider stopped generation before any assistant response " + "was emitted." + ) + + if effective_limit_reduced and context_window_tokens: + budget_note = ( + f"The runtime reduced max_tokens from {requested_max_tokens} to " + f"{effective_max_tokens} so the request could fit the " + f"{context_window_tokens}-token service context window." + ) + elif effective_max_tokens: + budget_note = ( + f"The effective output budget was {effective_max_tokens} tokens." + ) + else: + budget_note = "The provider did not report the exact output budget." + + summary = ( + f"Generation stopped at the {limit_label} " + f"(finish_reason={finish_reason or 'length'}). " + f"{budget_note} {output_state} JIN discarded the incomplete L4 " + "result and kept the pending facts unchanged." + ) + + retry_note = ( + "Because the pending facts were kept, a later idle tick can start " + "another L4 request for the same batch." + ) + + details = { + "kind": "l4_skip", + "phase": phase, + "status": "skipped", + "reason": "response_truncated", + "summary": summary, + "retry_behavior": retry_note, + "finish_reason": finish_reason or "length", + "limit_type": limit_type, + "model": model, + "context_window_tokens": context_window_tokens, + "requested_max_output_tokens": requested_max_tokens, + "effective_max_output_tokens": effective_max_tokens, + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens, + "assistant_content": "present" if content else "empty", + "assistant_content_chars": len(content), + "reasoning_generated": bool(reasoning), + "reasoning_chars": len(reasoning), + } + + if pending_count is not None: + details["pending_count"] = int(pending_count) + if pending_ids: + details["pending_ids"] = list(pending_ids) + if selected_fields_count is not None: + details["selected_fields_count"] = int(selected_fields_count) + + return details + + def l4_memory_enabled() -> bool: return bool(getattr(config, "L4_MEMORY_ENABLED", True)) @@ -370,6 +586,17 @@ async def ask_l4_model( user_prompt: str, max_tokens: int, ) -> dict: + request_limits = await resolve_l4_request_limits( + service_client=service_client, + system_prompt=system_prompt, + user_prompt=user_prompt, + requested_max_tokens=max_tokens, + ) + effective_max_tokens = ( + request_limits.get("effective_max_tokens") + or max_tokens + ) + await refresh_runtime_memory_summarizer_usage( context, system_prompt=system_prompt, @@ -383,7 +610,7 @@ async def ask_l4_model( system_prompt=system_prompt, user_prompt=user_prompt, temperature=getattr(config, "SERVICE_TEMPERATURE", 0.1), - max_tokens=max_tokens, + max_tokens=effective_max_tokens, ), ) @@ -392,16 +619,30 @@ async def ask_l4_model( system_prompt=system_prompt, user_prompt=user_prompt, temperature=getattr(config, "SERVICE_TEMPERATURE", 0.1), - max_tokens=max_tokens, + max_tokens=effective_max_tokens, timeout=getattr(config, "SERVICE_REQUEST_TIMEOUT", 1000.0), ) - response_text = extract_runtime_memory_text(response) + if isinstance(response, dict): + response["_jin_l4_request_meta"] = { + **request_limits, + "model": getattr( + service_client, + "model_uid", + "", + ), + } - await log_runtime_summarizer_result( - context, - label=label, - result=response_text, + response_text = extract_runtime_memory_text( + response, + allow_reasoning_fallback=False, ) + + if response_text: + await log_runtime_summarizer_result( + context, + label=label, + result=response_text, + ) await refresh_runtime_memory_summarizer_usage( context, system_prompt=system_prompt, @@ -428,10 +669,16 @@ async def log_l4_skip_event( if details: result.update(details) + display_reason = ( + "output truncated before final response" + if reason == "response_truncated" + else reason + ) + await log_memory_event( context, level=L4_LOG_LEVEL, - message=f"L4 {message_phase} skipped: {reason}", + message=f"L4 {message_phase} skipped: {display_reason}", details=json.dumps( result, ensure_ascii=False, @@ -439,6 +686,7 @@ async def log_l4_skip_event( ), fallback_channel="summarizer", event=f"{phase}_skipped", + trace_reason=result.get("summary"), ) return result @@ -467,12 +715,19 @@ async def run_l4_extraction_phase( phase="extract", message_phase="extraction", reason="response_truncated", - details={ - "selected_fields_count": len(pending_fields), - }, + details=build_l4_truncation_details( + response, + phase="extract", + selected_fields_count=len(pending_fields), + ), ) - payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + payload = extract_l4_json_payload( + extract_runtime_memory_text( + response, + allow_reasoning_fallback=False, + ) + ) if payload is None: return await log_l4_skip_event( context, @@ -563,22 +818,30 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: ) if is_runtime_memory_response_truncated(response): + pending_ids = [ + fact.get("id") + for fact in pending_facts + if fact.get("id") + ] return await log_l4_skip_event( context, phase="merge", message_phase="merge", reason="response_truncated", - details={ - "pending_count": len(pending_facts), - "pending_ids": [ - fact.get("id") - for fact in pending_facts - if fact.get("id") - ], - }, + details=build_l4_truncation_details( + response, + phase="merge", + pending_count=len(pending_facts), + pending_ids=pending_ids, + ), ) - payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + payload = extract_l4_json_payload( + extract_runtime_memory_text( + response, + allow_reasoning_fallback=False, + ) + ) if payload is None: return await log_l4_skip_event( context, @@ -728,11 +991,20 @@ async def run_l4_jin_note( message_phase="JIN note", reason="response_truncated", details={ + **build_l4_truncation_details( + response, + phase="jin_note", + ), "selected_fact_ids": selected_fact_ids, }, ) - payload = extract_l4_json_payload(extract_runtime_memory_text(response)) + payload = extract_l4_json_payload( + extract_runtime_memory_text( + response, + allow_reasoning_fallback=False, + ) + ) if payload is None: return await log_l4_skip_event( context, diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index 08cf9008..d6c010e5 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -50,7 +50,7 @@ def test_cache_versions_are_bumped(self): self.assertIn("/static/css/runtime-memory.css?v=l4-logger-cards-2", source) self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-17", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) - self.assertIn("/static/js/logger/trace-modal.js?v=l4-structured-trace-2", source) + self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index a93dd682..526e9d58 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -32,9 +32,12 @@ def test_extraction_and_merge_request_response_cards_are_paired(self): self.assertIn('"response"', source) self.assertIn('tone === "muted"', source) self.assertIn('kind: "l4_summarizer_response"', source) + self.assertIn('responseSettled: false', source) self.assertIn('responseReady: false', source) self.assertIn('if (!state.responseReady)', source) - self.assertIn('.find((candidate) => !candidate.responseReady)', source) + self.assertIn('.find((candidate) => !candidate.responseSettled)', source) + self.assertIn('setL4LoggerButtonVisible(', source) + self.assertIn('settleL4SummarizerCardForTerminalEvent(', source) self.assertNotIn('.find((candidate) => !candidate.responseDetails)', source) def test_l4_response_modal_uses_structured_layout_and_no_changes_state(self): @@ -42,7 +45,11 @@ def test_l4_response_modal_uses_structured_layout_and_no_changes_state(self): self.assertIn('parsed.kind === "l4_fact"', source) self.assertIn('parsed.kind === "l4_summarizer_response"', source) + self.assertIn('parsed.kind === "l4_skip"', source) self.assertIn('"No changes"', source) + self.assertIn('"What happened"', source) + self.assertIn('"Retry behavior"', source) + self.assertIn('parsed.reasoning_generated', source) self.assertIn('Array.isArray(payload.facts)', source) self.assertIn('Array.isArray(payload.operations)', source) @@ -57,10 +64,11 @@ def test_restore_message_round_trip_is_registered(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=l4-logger-cards-1', source) + self.assertIn('/static/css/runtime-memory.css?v=l4-logger-cards-2', source) self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-restore-1', source) - self.assertIn('/static/js/logger/trace-modal.js?v=l4-structured-trace-1', source) - self.assertIn('/static/js/logger/log-entries.js?v=l4-logger-cards-2', source) + self.assertIn('/static/js/logger/logger.js?v=l4-truncate-reason-1', source) + self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=l4-response-visibility-1', source) self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-1', source) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index a9cce05a..748e73f9 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -778,6 +778,129 @@ async def test_merge_phase_logs_skip_reason(self): ], ) + async def test_truncated_merge_logs_diagnostics_without_reasoning_response(self): + class TruncatedReasoningServiceClient: + + model_uid = "google/gemma-4-e4b" + configured_context_window = 4096 + + def __init__(self): + self.calls = [] + + async def resolve_request_context_window(self): + return 4096 + + async def resolve_safe_max_tokens( + self, + *, + system_prompt, + user_prompt, + requested_max_tokens, + ): + return 3288 + + async def ask( + self, + *, + system_prompt, + user_prompt, + temperature, + max_tokens, + timeout=None, + ): + self.calls.append({ + "system_prompt": system_prompt, + "user_prompt": user_prompt, + "temperature": temperature, + "max_tokens": max_tokens, + "timeout": timeout, + }) + return { + "model": self.model_uid, + "choices": [ + { + "finish_reason": "length", + "message": { + "content": "", + "reasoning_content": ( + "Internal analysis that must not be exposed " + "as the L4 response." + ), + }, + }, + ], + "usage": { + "prompt_tokens": 808, + "completion_tokens": 3288, + "total_tokens": 4096, + }, + } + + store, _ = add_l4_pending_candidates( + normalize_l4_store({}), + [ + { + "key": "user.profile", + "value": "The user works on JIN Core.", + "category": "user_fact", + }, + ], + now="2026-08-06T12:00:00Z", + ) + logger = FakeLogger() + service_client = TruncatedReasoningServiceClient() + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=logger, + clients={"service": service_client}, + ) + context.runtime_long_term_memory_store = store + + result = await maybe_update_runtime_l4_memory( + context=context, + user_idle_seconds=61, + ) + + self.assertEqual(result["phase"], "merge") + self.assertEqual(result["status"], "skipped") + self.assertEqual(result["reason"], "response_truncated") + self.assertEqual(result["assistant_content"], "empty") + self.assertTrue(result["reasoning_generated"]) + self.assertEqual(result["effective_max_output_tokens"], 3288) + self.assertEqual(result["context_window_tokens"], 4096) + self.assertIn("kept the pending facts unchanged", result["summary"]) + + self.assertEqual(service_client.calls[0]["max_tokens"], 3288) + + result_logs = [ + details + for message, details in logger.summarizer_logs + if message == "[MEMORY:L4] L4 merge summarizer result" + ] + self.assertEqual(result_logs, []) + + request_logs = [ + json.loads(details) + for message, details in logger.summarizer_logs + if message == "[MEMORY:L4] L4 merge summarizer request" + ] + self.assertEqual(request_logs[0]["max_tokens"], 3288) + + skip_logs = [ + json.loads(details) + for message, details in logger.summarizer_logs + if message == ( + "[MEMORY:L4] L4 merge skipped: " + "output truncated before final response" + ) + ] + self.assertEqual(len(skip_logs), 1) + self.assertEqual(skip_logs[0]["kind"], "l4_skip") + self.assertEqual(skip_logs[0]["finish_reason"], "length") + self.assertNotIn("reasoning_content", skip_logs[0]) + self.assertNotIn("Internal analysis", json.dumps(skip_logs[0])) + async def test_runtime_l4_memory_update_running_tracks_active_task(self): context = RuntimeContext( websocket=None, diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index a67a2432..0a500902 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -931,6 +931,24 @@ function appendChatMessage( } // CREATE STREAM GROUP +function scrollCollapsedThinkToLatest( + thinkContent +) { + + if ( + !thinkContent + || !thinkContent.classList.contains( + "is-collapsed" + ) + ) { + return; + } + + thinkContent.scrollTop = + thinkContent.scrollHeight; + +} + function updateThinkExpandedHeight( thinkContent ) { @@ -944,8 +962,15 @@ function updateThinkExpandedHeight( `${thinkContent.scrollHeight}px` ); + scrollCollapsedThinkToLatest( + thinkContent + ); + } +window.updateThinkExpandedHeight = + updateThinkExpandedHeight; + let thinkResizeFrame = null; window.addEventListener( @@ -1001,7 +1026,7 @@ function createStreamGroup( document.createElement("div"); thinkContent.className = - "jin-think-content"; + "jin-think-content is-collapsed"; thinkContent.setAttribute( "role", @@ -1015,7 +1040,7 @@ function createStreamGroup( thinkContent.setAttribute( "aria-expanded", - "true" + "false" ); thinkContent.setAttribute( @@ -1023,7 +1048,7 @@ function createStreamGroup( "Toggle thinking block" ); - let collapsed = false; + let collapsed = true; const setCollapsed = (nextCollapsed) => { @@ -1042,6 +1067,16 @@ function createStreamGroup( : "true" ); + if (collapsed) { + requestAnimationFrame( + () => { + scrollCollapsedThinkToLatest( + thinkContent + ); + } + ); + } + }; thinkContent.addEventListener( diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index f844ddb5..c894eeb2 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -982,6 +982,17 @@ function setL4LoggerButtonTone( : "inline-flex items-center rounded border border-blue-500/20 px-2 py-1 text-[10px] uppercase tracking-wider text-blue-300 hover:bg-blue-500/10 transition"; } +function setL4LoggerButtonVisible( + button, + visible, +) { + button.hidden = !visible; + button.classList.toggle( + "hidden", + !visible + ); +} + function resolveL4SummarizerPhase( message, meta, @@ -1076,13 +1087,16 @@ function createL4SummarizerCard( "response" ); - responseButton.hidden = - true; + setL4LoggerButtonVisible( + responseButton, + false + ); const state = { logDiv, phase, requestDetails, + responseSettled: false, responseReady: false, responseDetails: null, responsePayload: null, @@ -1091,8 +1105,10 @@ function createL4SummarizerCard( responseButton, }; - requestButton.hidden = - !requestDetails; + setL4LoggerButtonVisible( + requestButton, + Boolean(requestDetails) + ); requestButton.addEventListener( "click", @@ -1172,7 +1188,7 @@ function handleL4SummarizerLog( let state = [...l4SummarizerCards[phase]] .reverse() - .find((candidate) => !candidate.responseReady); + .find((candidate) => !candidate.responseSettled); if (!state) { state = @@ -1181,10 +1197,34 @@ function handleL4SummarizerLog( ); } - state.responseReady = + state.responseSettled = true; state.responseDetails = String(details ?? ""); + + const responseText = + state.responseDetails.trim(); + + state.responseReady = + Boolean( + responseText + && responseText !== "" + ); + + if (!state.responseReady) { + state.responsePayload = + null; + state.responseHasChanges = + false; + + setL4LoggerButtonVisible( + state.responseButton, + false + ); + + return state.logDiv; + } + state.responsePayload = parseL4JsonPayload( state.responseDetails @@ -1196,8 +1236,10 @@ function handleL4SummarizerLog( state.responsePayload ); - state.responseButton.hidden = - false; + setL4LoggerButtonVisible( + state.responseButton, + true + ); setL4LoggerButtonTone( state.responseButton, @@ -1209,6 +1251,57 @@ function handleL4SummarizerLog( return state.logDiv; } +function settleL4SummarizerCardForTerminalEvent( + message, + meta, +) { + const event = + String(meta && meta.memory_event || "").toLowerCase(); + + if (!event.endsWith("_skipped") && !event.endsWith("_failed")) { + return; + } + + const phase = + event.startsWith("extract_") + ? "extraction" + : event.startsWith("merge_") + ? "merge" + : resolveL4SummarizerPhase( + message, + meta + ); + + if (!phase || !l4SummarizerCards[phase]) { + return; + } + + const state = + [...l4SummarizerCards[phase]] + .reverse() + .find((candidate) => !candidate.responseSettled); + + if (!state) { + return; + } + + state.responseSettled = + true; + state.responseReady = + false; + state.responseDetails = + null; + state.responsePayload = + null; + state.responseHasChanges = + false; + + setL4LoggerButtonVisible( + state.responseButton, + false + ); +} + function resolveDeletedL4Fact( details, meta, @@ -1429,6 +1522,11 @@ function appendLog( details, ); + settleL4SummarizerCardForTerminalEvent( + normalized.message, + meta + ); + const l4SummarizerLog = handleL4SummarizerLog( normalized.message, @@ -1903,9 +2001,15 @@ function appendLog( const reason = shouldShowReason - ? extractTraceReason( - normalized.message, - normalized.details + ? ( + String( + meta?.trace_reason + || "" + ).trim() + || extractTraceReason( + normalized.message, + normalized.details + ) ) : ""; diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 1b31a6c5..5221cfef 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -45,6 +45,26 @@ function extractTraceReason( return ""; } + const parsed = + parseTraceJson(text); + + if ( + parsed + && typeof parsed === "object" + ) { + const structuredReason = + String( + parsed.summary + || parsed.explanation + || parsed.trace_reason + || "" + ).trim(); + + if (structuredReason) { + return structuredReason; + } + } + const likelyReasonMatch = text.match( /^Likely reason:\s*(.+)$/m diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index eb3f68cd..5f22e1dc 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -551,6 +551,142 @@ function renderL4SummarizerResponseTrace(parsed) { ); } +function renderL4SkipTrace(parsed) { + appendTraceModalBody( + traceModalContent, + "What happened", + parsed.summary || "The L4 model response was not usable." + ); + + const fields = + document.createElement("section"); + + fields.className = + "delayed-memory-modal-fields"; + + appendTraceModalField( + fields, + "Phase", + parsed.phase + ); + + appendTraceModalField( + fields, + "Finish reason", + parsed.finish_reason + ); + + appendTraceModalField( + fields, + "Limit reached", + parsed.limit_type + ); + + appendTraceModalField( + fields, + "Model", + parsed.model + ); + + appendTraceModalField( + fields, + "Context window", + parsed.context_window_tokens + ? `${parsed.context_window_tokens} tokens` + : "" + ); + + appendTraceModalField( + fields, + "Requested max output", + parsed.requested_max_output_tokens + ? `${parsed.requested_max_output_tokens} tokens` + : "" + ); + + appendTraceModalField( + fields, + "Effective max output", + parsed.effective_max_output_tokens + ? `${parsed.effective_max_output_tokens} tokens` + : "" + ); + + appendTraceModalField( + fields, + "Prompt tokens", + parsed.prompt_tokens + ); + + appendTraceModalField( + fields, + "Generated tokens", + parsed.completion_tokens + ); + + appendTraceModalField( + fields, + "Total tokens", + parsed.total_tokens + ); + + appendTraceModalField( + fields, + "Assistant response", + parsed.assistant_content + ); + + appendTraceModalField( + fields, + "Reasoning generated", + parsed.reasoning_generated + ); + + if (fields.childElementCount) { + traceModalContent.appendChild( + fields + ); + } + + appendTraceModalBody( + traceModalContent, + "What JIN did", + "The incomplete response was discarded. No L4 facts were merged or removed." + ); + + appendTraceModalBody( + traceModalContent, + "Retry behavior", + parsed.retry_behavior + ); + + const pendingCount = + Number( + parsed.pending_count + || parsed.selected_fields_count + || 0 + ); + + if (pendingCount) { + appendTraceModalBody( + traceModalContent, + "Pending batch kept", + `${pendingCount} item${pendingCount === 1 ? "" : "s"} remain pending.` + ); + } + + if ( + Array.isArray(parsed.pending_ids) + && parsed.pending_ids.length + ) { + appendTraceModalBody( + traceModalContent, + "Pending IDs", + parsed.pending_ids.join("\n") + ); + } +} + function isSummarizerRequestPayload(parsed) { return Boolean( parsed @@ -786,6 +922,17 @@ function renderTraceDetails( return; } + if ( + parsed + && parsed.kind === "l4_skip" + ) { + renderL4SkipTrace( + parsed + ); + + return; + } + if ( parsed && parsed.kind === "user_payload_trace" diff --git a/ui/templates/index.html b/ui/templates/index.html index 488806f6..a5b9bd48 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -512,14 +512,14 @@ - - + + - + - + From 15110573d0f79c29ba655e7f62b1e0bd6bf900a0 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:14:11 +0300 Subject: [PATCH 018/168] chore: error handling - catch and log LM Studio API and stream errors --- clients/brain_client.py | 21 ++ runtime/client.py | 284 ++++++++++++++++++++++++++-- runtime/stream.py | 74 +++++++- tests/test_runtime_client.py | 73 ++++++- tests/test_runtime_stream_tokens.py | 78 ++++++++ ui/static/js/logger/log-entries.js | 13 ++ websocket/logger.py | 2 + 7 files changed, 523 insertions(+), 22 deletions(-) diff --git a/clients/brain_client.py b/clients/brain_client.py index 0d222856..474276c4 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -60,6 +60,11 @@ ResponseExtractor, ) +from runtime.client import ( + LMStudioAPIError, +) + + from utils.actions import ( build_runtime_action_id, emit_runtime_action_counter_updates, @@ -410,6 +415,10 @@ async def ask_brain( return content_actions.text + except LMStudioAPIError: + + raise + except Exception as error: formatted_error = ( @@ -535,6 +544,10 @@ async def ask_brain( return reasoning_actions.text + except LMStudioAPIError: + + raise + except Exception as error: formatted_error = ( @@ -1634,6 +1647,10 @@ async def flush_pending_idle_actions() -> None: await finalize_session_action_history() raise + except LMStudioAPIError: + await finalize_session_action_history() + raise + except Exception as error: await finalize_session_action_history() @@ -1739,6 +1756,10 @@ async def flush_pending_idle_actions() -> None: await finalize_session_action_history() raise + except LMStudioAPIError: + await finalize_session_action_history() + raise + except Exception as error: await finalize_session_action_history() diff --git a/runtime/client.py b/runtime/client.py index 7f7bc5c8..029aebc5 100644 --- a/runtime/client.py +++ b/runtime/client.py @@ -20,6 +20,168 @@ logger = logging.getLogger(__name__) +class LMStudioAPIError(RuntimeError): + + def __init__( + self, + summary: str, + *, + details: str, + ): + + super().__init__(summary) + self.summary = str(summary or "LM Studio request failed.") + self.details = str(details or "") + + +def _extract_lm_studio_error_payload(value): + + if not isinstance(value, dict): + return None + + error_value = value.get("error") + if error_value not in (None, "", {}, []): + return error_value + + event_type = str( + value.get("type", "") + or value.get("object", "") + or "" + ).strip().casefold() + + if event_type in { + "error", + "response.error", + }: + return value + + return None + + +def _lm_studio_error_message(value) -> str: + + if isinstance(value, dict): + for key in ( + "message", + "detail", + "error", + "code", + ): + candidate = value.get(key) + if candidate not in (None, "", {}, []): + if isinstance(candidate, (dict, list)): + return _preview_runtime_payload( + candidate, + limit=1200, + ) + return str(candidate).strip() + + return _preview_runtime_payload( + value, + limit=1200, + ).strip() + + if isinstance(value, list): + return _preview_runtime_payload( + value, + limit=1200, + ).strip() + + return str(value or "").strip() + + +def _build_lm_studio_error( + *, + endpoint: str, + payload: dict, + error_payload=None, + response=None, + error: Exception | None = None, +) -> LMStudioAPIError: + + status_code = getattr( + response, + "status_code", + None, + ) + response_json = None + response_text = "" + + if response is not None: + try: + response_json = response.json() + except Exception: + response_json = None + + try: + response_text = str( + response.text + or "" + ).strip() + except Exception: + response_text = "" + + if error_payload is None: + error_payload = _extract_lm_studio_error_payload( + response_json + ) + + provider_message = _lm_studio_error_message( + error_payload + ) + + if not provider_message and response_text: + provider_message = response_text[:1200] + + if not provider_message and error is not None: + provider_message = str(error).strip() + + if status_code: + summary = f"HTTP {status_code}" + if provider_message: + summary += f": {provider_message}" + else: + summary = ( + provider_message + or "LM Studio request failed." + ) + + details = { + "provider": "LM Studio", + "summary": summary, + "endpoint": endpoint, + "status": status_code, + "model": payload.get("model"), + "request": { + "stream": payload.get("stream"), + "max_tokens": payload.get("max_tokens"), + "temperature": payload.get("temperature"), + }, + "lm_studio_error": error_payload, + "response_json": response_json, + "response_body": ( + response_text[:8000] + if response_text + else "" + ), + "client_exception": ( + repr(error) + if error is not None + else "" + ), + } + + return LMStudioAPIError( + summary, + details=json.dumps( + details, + ensure_ascii=False, + indent=2, + default=str, + ), + ) + + def _preview_runtime_payload( value, *, @@ -805,22 +967,58 @@ async def ask( stream=False, ) - response = await self.client.post( - join_url( - self.api_base, - settings.CHAT_ENDPOINT, - ), - json=payload, - timeout=( - self.timeout - if timeout is None - else timeout - ), + endpoint = join_url( + self.api_base, + settings.CHAT_ENDPOINT, ) - response.raise_for_status() + try: + response = await self.client.post( + endpoint, + json=payload, + timeout=( + self.timeout + if timeout is None + else timeout + ), + ) + + response.raise_for_status() + + except httpx.HTTPError as error: + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + response=getattr( + error, + "response", + None, + ), + error=error, + ) from error + + try: + result = response.json() + except Exception as error: + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + response=response, + error=error, + ) from error + + provider_error = _extract_lm_studio_error_payload( + result + ) + if provider_error is not None: + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + error_payload=provider_error, + response=response, + ) - return response.json() + return result # --------------------------------------------------------- # STREAM REQUEST @@ -852,20 +1050,40 @@ async def stream( stream_id = None valid_json_chunks = 0 invalid_json_samples: list[str] = [] + endpoint = join_url( + self.api_base, + settings.CHAT_ENDPOINT, + ) try: async with self.client.stream( "POST", - join_url( - self.api_base, - settings.CHAT_ENDPOINT, - ), + endpoint, json=payload, timeout=None, ) as response: - response.raise_for_status() + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + read_response = getattr( + response, + "aread", + None, + ) + if read_response is not None: + try: + await read_response() + except Exception: + pass + + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + response=response, + error=error, + ) from error stream_id = id(response) @@ -994,6 +1212,19 @@ async def stream( valid_json_chunks += 1 + provider_error = ( + _extract_lm_studio_error_payload( + chunk + ) + ) + if provider_error is not None: + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + error_payload=provider_error, + response=response, + ) + # ------------------------------------------------- # USAGE # ------------------------------------------------- @@ -1098,6 +1329,23 @@ async def stream( # FATAL ERROR # --------------------------------------------------------- + except LMStudioAPIError: + + raise + + except httpx.HTTPError as e: + + raise _build_lm_studio_error( + endpoint=endpoint, + payload=payload, + response=getattr( + e, + "response", + None, + ), + error=e, + ) from e + except Exception as e: context_logger = getattr( diff --git a/runtime/stream.py b/runtime/stream.py index 651aa3ce..391483a8 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -9,6 +9,11 @@ refresh_runtime_state, ) +from runtime.client import ( + LMStudioAPIError, +) + + from utils.stream_handler import ( StreamHandler, ) @@ -2509,8 +2514,61 @@ async def run( public_error = ( "Runtime stream failed." ) + log_message = ( + f"[RUNTIME STREAM CRASH] {public_error}" + ) + error_details = tb + error_meta = {} if isinstance( + e, + LMStudioAPIError, + ): + + public_error = ( + "LM Studio request failed." + ) + provider_summary = str( + getattr( + e, + "summary", + "", + ) + or str(e) + or public_error + ).strip() + visible_summary = ( + provider_summary[:260] + + ( + "..." + if len(provider_summary) > 260 + else "" + ) + ) + log_message = ( + f"[LM STUDIO ERROR] {visible_summary}" + ) + error_details = str( + getattr( + e, + "details", + "", + ) + or tb + ) + error_meta = { + "provider": "lm_studio", + "error_kind": "provider", + } + + self.context.runtime_turn_interrupted = True + self.context.runtime_turn_interruption_reason = ( + provider_summary + ) + self.context.runtime_turn_interruption_quote = "" + self.capture_runtime_turn_response() + + elif isinstance( e, httpx.ConnectError, ): @@ -2519,6 +2577,9 @@ async def run( "Model server offline " "or unreachable." ) + log_message = ( + f"[RUNTIME STREAM CRASH] {public_error}" + ) elif isinstance( e, @@ -2528,6 +2589,9 @@ async def run( public_error = ( "Model request timeout." ) + log_message = ( + f"[RUNTIME STREAM CRASH] {public_error}" + ) elif isinstance( e, @@ -2537,14 +2601,18 @@ async def run( public_error = ( "Model server returned HTTP error." ) + log_message = ( + f"[RUNTIME STREAM CRASH] {public_error}" + ) # ----------------------------------------------------- - # LOG FULL TRACEBACK + # LOG PROVIDER PAYLOAD / FULL TRACEBACK # ----------------------------------------------------- await self.logger.log_error( - f"[RUNTIME STREAM CRASH] {public_error}", - details=tb, + log_message, + details=error_details, + **error_meta, ) # ----------------------------------------------------- diff --git a/tests/test_runtime_client.py b/tests/test_runtime_client.py index c59b1bd5..c81e4cd2 100644 --- a/tests/test_runtime_client.py +++ b/tests/test_runtime_client.py @@ -1,6 +1,10 @@ +import json import unittest -from runtime.client import RuntimeClient +from runtime.client import ( + LMStudioAPIError, + RuntimeClient, +) class FakeResponse: @@ -646,6 +650,73 @@ async def test_stream_ignores_sse_metadata_lines(self): [], ) + + async def test_stream_treats_lm_studio_error_chunk_as_provider_failure(self): + + provider_message = ( + "The selected model was loaded with a context length " + "too small for this request." + ) + http_client = FakeHttpClient( + models_payload={ + "data": [ + { + "id": "test-model", + "context_length": 8192, + } + ] + }, + stream_lines=[ + 'data: {"error": {"message": "' + + provider_message + + '", "type": "invalid_request_error"}}', + "data: [DONE]", + ], + ) + client = RuntimeClient( + api_base="http://runtime.test", + model_uid="test-model", + timeout=30.0, + configured_context_window=4096, + client=http_client, + ) + context = FakeStreamContextObject() + + with self.assertRaises( + LMStudioAPIError, + ) as raised: + [ + event + async for event in client.stream( + context=context, + system_prompt="system", + user_prompt="user", + temperature=0.1, + max_tokens=100, + ) + ] + + details = json.loads( + raised.exception.details + ) + + self.assertIn( + provider_message, + raised.exception.summary, + ) + self.assertEqual( + details["provider"], + "LM Studio", + ) + self.assertEqual( + details["lm_studio_error"]["message"], + provider_message, + ) + self.assertEqual( + details["request"]["stream"], + True, + ) + async def test_stream_reports_when_all_sse_frames_are_invalid_json(self): http_client = FakeHttpClient( diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index 9e66a6fb..0ed774c5 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -8,6 +8,7 @@ from unittest.mock import patch from runtime.stream import RuntimeStream +from runtime.client import LMStudioAPIError from runtime.registry import runtime_state from utils.stream_validator import ( MAX_REPEAT_SENTENCES, @@ -168,6 +169,24 @@ async def fake_cancelled_generator(): raise asyncio.CancelledError() +async def fake_lm_studio_error_generator(): + + raise LMStudioAPIError( + "HTTP 400: context length too small", + details=json.dumps({ + "provider": "LM Studio", + "summary": "HTTP 400: context length too small", + "status": 400, + "lm_studio_error": { + "message": "context length too small", + }, + }), + ) + + if False: + yield {} + + async def fake_sentence_loop_generator(): repeated = ( @@ -373,6 +392,65 @@ def build_limit_context(self): runtime_session_action_history=[], ) + async def test_lm_studio_provider_error_logs_payload_and_marks_turn_interrupted(self): + + context = self.build_limit_context() + runtime_id = settings.SERVICE_MODEL_UID + stream = RuntimeStream( + context=context, + runtime_id=runtime_id, + role="service", + context_window=settings.SERVICE_CONTEXT_WINDOW, + log_method=context.logger.log_service, + context_snapshot={ + "context_role": "brain", + "system_prompt": "system prompt", + "user_prompt": "user payload", + }, + ) + + result = await stream.run( + fake_lm_studio_error_generator() + ) + + self.assertIsNone(result) + self.assertTrue( + context.runtime_turn_interrupted + ) + self.assertEqual( + context.runtime_turn_interruption_reason, + "HTTP 400: context length too small", + ) + + error_logs = [ + message + for message in context.logger.messages + if message[0] == "error" + ] + self.assertEqual( + len(error_logs), + 1, + ) + self.assertIn( + "[LM STUDIO ERROR]", + error_logs[0][1], + ) + self.assertEqual( + error_logs[0][2]["provider"], + "lm_studio", + ) + self.assertIn( + "context length too small", + error_logs[0][2]["details"], + ) + self.assertTrue( + any( + message.get("type") == "message_error" + and message.get("text") == "LM Studio request failed." + for message in context.websocket.messages + ) + ) + async def test_reasoning_limit_arms_immediate_followup(self): context = self.build_limit_context() diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index c894eeb2..072ae983 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -1981,6 +1981,15 @@ function appendLog( "[JSON PARSE ERROR]" ); + const isLmStudioError = + tag.includes("ERROR") + && ( + String(meta?.provider || "").toLowerCase() + === "lm_studio" + || String(normalized.message || "") + .includes("[LM STUDIO ERROR]") + ); + const isPatternResult = isSummarizer && String( @@ -2049,6 +2058,8 @@ function appendLog( ? "payload" : isUser ? "payload" + : isLmStudioError + ? "payload" : isJsonParseError ? "payload" : "trace"; @@ -2083,6 +2094,8 @@ function appendLog( ? "Service as brain output" : isSummarizer ? normalized.message || "Summarizer payload" + : isLmStudioError + ? "LM Studio error payload" : isJsonParseError ? "Runtime stream payload" : "Trace" diff --git a/websocket/logger.py b/websocket/logger.py index 12f12467..61ef2015 100644 --- a/websocket/logger.py +++ b/websocket/logger.py @@ -168,11 +168,13 @@ async def log_error( self, message: str, details: str | None = None, + **extra, ): await self.log( "[ERROR]", message, details=details, + **extra, ) async def log_translation(self, message: str): From 9935a4bde72b52e73ed3a8acbcc29605e216ec6e Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:15:09 +0300 Subject: [PATCH 019/168] feature: L4 memory layer - add active bubble state while JIN writes an L4 update - rename marker to UPDATE_L4_FACTS for clearer action intent --- clients/brain_client.py | 145 ++++++++++++++++++ contracts/rules_assembler.py | 4 +- .../{jin_for_l4.json => update_l4_facts.json} | 10 +- rules/brain_context_builder.py | 4 +- rules/identity.py | 6 +- runtime/stream.py | 136 +++++++++++++--- ..._jin_for_l4.py => test_update_l4_facts.py} | 28 ++-- tests/test_brain_runtime_actions.py | 108 ++++++++++++- utils/actions/__init__.py | 4 +- utils/actions/common_action_utils.py | 10 +- utils/actions/dispatcher.py | 18 +-- ..._actions.py => update_l4_facts_actions.py} | 30 ++-- ...r_l4_utils.py => update_l4_facts_utils.py} | 10 +- 13 files changed, 426 insertions(+), 87 deletions(-) rename contracts/{jin_for_l4.json => update_l4_facts.json} (92%) rename tests/runtime_actions/{test_jin_for_l4.py => test_update_l4_facts.py} (88%) rename utils/actions/{jin_for_l4_actions.py => update_l4_facts_actions.py} (86%) rename utils/actions/{jin_for_l4_utils.py => update_l4_facts_utils.py} (83%) diff --git a/clients/brain_client.py b/clients/brain_client.py index 474276c4..99414562 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -13,6 +13,7 @@ RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, @@ -670,6 +671,7 @@ def preserve_duplicate_append_skill_marker( ) stop_for_runtime_action = False delayed_memory_bubble_started = False + update_l4_facts_pending_bubble_ids = [] asset_action_bubble_started = False asset_action_bubble_id = "" asset_action_bubble_text = "" @@ -1141,6 +1143,141 @@ async def emit_delayed_memory_bubble_started(): payload ) + async def emit_update_l4_facts_bubble_started( + result, + ): + + started_actions = tuple( + getattr( + result, + "started_actions", + (), + ) + or () + ) + + started_count = sum( + 1 + for action in started_actions + if action.name == RUNTIME_ACTION_UPDATE_L4_FACTS + ) + + if not started_count: + return + + emitter = getattr( + context, + "emitter", + None, + ) + emit = getattr( + emitter, + "emit", + None, + ) + + if emit is None: + return + + for _ in range(started_count): + sequence = int( + getattr( + context, + "runtime_update_l4_facts_action_sequence", + 0, + ) + or 0 + ) + 1 + context.runtime_update_l4_facts_action_sequence = sequence + + action_id = build_runtime_action_id( + RUNTIME_ACTION_UPDATE_L4_FACTS, + sequence, + ) + update_l4_facts_pending_bubble_ids.append( + action_id + ) + + payload = { + "type": "runtime_action", + "action": "update_l4_facts", + "id": action_id, + "status": "started", + "runtime_message_id": runtime_message_id, + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "text": build_runtime_action_display_text( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + } + + if action_context_snapshot: + payload["context"] = action_context_snapshot + + await emit( + payload + ) + + async def complete_update_l4_facts_bubbles( + actions, + action_display_ids, + ): + + emitter = getattr( + context, + "emitter", + None, + ) + emit = getattr( + emitter, + "emit", + None, + ) + + for action in actions: + if action.name != RUNTIME_ACTION_UPDATE_L4_FACTS: + continue + + action_id = ( + update_l4_facts_pending_bubble_ids.pop(0) + if update_l4_facts_pending_bubble_ids + else "" + ) + + if action_id: + action_display_ids[id(action)] = action_id + + if emit is None or not action_id: + continue + + payload = { + "type": "runtime_action", + "action": "update_l4_facts", + "id": action_id, + "status": "completed", + "runtime_message_id": runtime_message_id, + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "text": build_runtime_action_display_text( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + } + + if action_context_snapshot: + payload["context"] = action_context_snapshot + + await emit( + payload + ) + async def emit_asset_action_bubble_started( result=None, ): @@ -1343,6 +1480,9 @@ async def filter_runtime_action_chunk( ) await emit_delayed_memory_bubble_started() + await emit_update_l4_facts_bubble_started( + result + ) await emit_asset_action_bubble_started( result ) @@ -1502,6 +1642,11 @@ async def apply_runtime_action_result( display_state=action_guard_display_state, ) + await complete_update_l4_facts_bubbles( + immediate_action_calls, + action_display_ids, + ) + await apply_runtime_action_calls( context, immediate_action_calls, diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index e7cbc07d..919c1f29 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -23,7 +23,7 @@ ("CLEAN_TOOL_RESULTS", "CAN_CLEAN_TOOL_RESULTS"), ("IDLE", "CAN_IDLE"), ("JIN_COLOR", "CAN_JIN_COLOR"), - ("JIN_FOR_L4", "CAN_JIN_FOR_L4"), + ("UPDATE_L4_FACTS", "CAN_UPDATE_L4_FACTS"), ("APPEND_SKILL", "CAN_USE_ASSETS"), ("REMOVE_SKILL", "CAN_USE_ASSETS"), ("ASSET_ACTION", "CAN_USE_ASSETS"), @@ -588,4 +588,4 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_CHECK_TODO = get_runtime_action_name("check_todo") RUNTIME_ACTION_IDLE = get_runtime_action_name("idle") RUNTIME_ACTION_JIN_COLOR = get_runtime_action_name("jin_color") -RUNTIME_ACTION_JIN_FOR_L4 = get_runtime_action_name("jin_for_l4") +RUNTIME_ACTION_UPDATE_L4_FACTS = get_runtime_action_name("update_l4_facts") diff --git a/contracts/jin_for_l4.json b/contracts/update_l4_facts.json similarity index 92% rename from contracts/jin_for_l4.json rename to contracts/update_l4_facts.json index 844c30db..e592c3fb 100644 --- a/contracts/jin_for_l4.json +++ b/contracts/update_l4_facts.json @@ -1,9 +1,9 @@ { - "jin_for_l4": { + "update_l4_facts": { "version": 1, "description": "Lets JIN pass a focused conversational clarification to the L4 maintainer without directly editing long-term memory.", - "runtime_action": "JIN_FOR_L4", - "private_marker": "", + "runtime_action": "UPDATE_L4_FACTS", + "private_marker": "", "triggers": [], "blockers": [], "effects": { @@ -17,12 +17,12 @@ "Never emit the marker while ambiguity remains or on the same turn in which you ask for clarification. Emit it after the user resolves the issue, or immediately when the user's current message itself is an explicit correction.", "Keep the visible reply natural and unobtrusive, then append the fulfilled marker after the user-facing text.", "Emit the fulfilled form:", - "", + "", "{", " \"fact_ids\": [\"l4_...\"],", " \"message\": \"Concise statement of the resolved meaning.\"", "}", - "", + "", "Use only exact fact IDs shown inside . Include every L4 fact directly involved in the issue and no unrelated facts.", "The message should state what became clear in the conversation, not prescribe merge/update/delete operations. L4 decides how to maintain its own facts.", "Do not emit any extra hidden instructions inside the marker and do not repeat the same note after it has been sent." diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index 789d5f05..ec965634 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -26,7 +26,7 @@ "CAN_CLEAN_TOOL_RESULTS": True, "CAN_IDLE": True, "CAN_JIN_COLOR": True, - "CAN_JIN_FOR_L4": True, + "CAN_UPDATE_L4_FACTS": True, } BRAIN_RUNTIME_ACTIONS = { @@ -39,7 +39,7 @@ "CAN_CLEAN_TOOL_RESULTS": True, "CAN_IDLE": True, "CAN_JIN_COLOR": True, - "CAN_JIN_FOR_L4": True, + "CAN_UPDATE_L4_FACTS": True, } APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS = ( diff --git a/rules/identity.py b/rules/identity.py index 9fb63685..80dfd071 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -32,11 +32,11 @@ "I end my response naturally or ask a question to prevent misunderstanding.\n" "I must avoid appending default assistant questions in the end of my responses; I keep natural conversation flow.\n" "I do not automatically agree and do not turn everything into a lecture.\n" - "In case of fail I must act again without asking narrowing questions.\n" - "I must skip redundant drafts and trial loops; I must avoid redundant deliberation; I must keep internal reasoning very brief and short.\n" + "I must strongly avoid redundant deliberation.\n" + "In case of fail I act again.\n" "I prefer to keep my presence unobtrusive: I help when useful and avoid forcing direction.\n" "I carefully adapt my lexicon to match the user's slang/style/vibe)\n" - "I prefer self-contained responses with a natural ending. Follow-up questions are used selectively, only when they materially help clarify or advance the current task.\n" + "I prefer self-contained responses with a natural ending.\n" "I respect the consistency and reliability of my context.\n" "I reject destabilizing interpretations, and prioritize structural integrity over conversational smoothness.\n" "Beyond mere response, I maintain a continuous situational awareness, " diff --git a/runtime/stream.py b/runtime/stream.py index 391483a8..2410c0d7 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -48,7 +48,7 @@ RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, build_runtime_action_display_text, @@ -155,11 +155,12 @@ def __init__( self.action_guard_rejected_aborted = False self.context_limit_recovery_armed = False self.started_delayed_memory_action_ids = [] + self.started_update_l4_facts_action_ids = [] self.confirmed_action_guard_names = set() self.rejected_action_guard_names = set() self.action_guard_confirmation_ids = {} self.jin_color_action_id = "" - self.jin_for_l4_action_ids = {} + self.update_l4_facts_action_ids = {} self.last_jin_color_action_color = "" self.runtime_action_event_offset = 0 self.session_action_history_start = 0 @@ -1073,6 +1074,18 @@ async def apply_runtime_action_filter_result( ) if actions_to_apply: + action_display_ids = { + id(action): self.get_runtime_action_display_id( + action + ) + for action in actions_to_apply + } + + await self.emit_completed_update_l4_facts_actions( + actions_to_apply, + action_display_ids, + ) + await apply_runtime_action_calls( self.context, actions_to_apply, @@ -1082,12 +1095,7 @@ async def apply_runtime_action_filter_result( guard_confirmation_ids=( self.action_guard_confirmation_ids ), - action_display_ids={ - id(action): self.get_runtime_action_display_id( - action - ) - for action in actions_to_apply - }, + action_display_ids=action_display_ids, runtime_message_id=( self.stream.message_id ), @@ -1121,6 +1129,74 @@ async def apply_runtime_action_filter_result( return result.text + async def emit_completed_update_l4_facts_actions( + self, + actions, + action_display_ids, + ) -> None: + + emitter = getattr( + self.context, + "emitter", + None, + ) + emit = getattr( + emitter, + "emit", + None, + ) + + if emit is None: + return + + action_context_snapshot = ( + dict(self.context_snapshot) + if isinstance( + self.context_snapshot, + dict, + ) + else None + ) + + for action in actions: + if action.name != RUNTIME_ACTION_UPDATE_L4_FACTS: + continue + + action_id = str( + action_display_ids.get( + id(action), + "", + ) + or "" + ).strip() + + if not action_id: + continue + + payload = { + "type": "runtime_action", + "runtime_message_id": self.stream.message_id, + "action": "update_l4_facts", + "id": action_id, + "status": "completed", + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "text": build_runtime_action_display_text( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + } + + if action_context_snapshot: + payload["context"] = action_context_snapshot + + await emit( + payload + ) + def get_runtime_action_display_id( self, action, @@ -1144,25 +1220,39 @@ def get_runtime_action_display_id( return self.jin_color_action_id - if action.name == RUNTIME_ACTION_JIN_FOR_L4: + if action.name == RUNTIME_ACTION_UPDATE_L4_FACTS: payload_key = str(action.payload or "").strip() - action_id = self.jin_for_l4_action_ids.get(payload_key, "") + action_id = ( + self.update_l4_facts_action_ids.get(payload_key, "") + if payload_key + else "" + ) if not action_id: - sequence = int( - getattr( - self.context, - "runtime_jin_for_l4_action_sequence", - 0, + if payload_key and self.started_update_l4_facts_action_ids: + action_id = self.started_update_l4_facts_action_ids.pop(0) + else: + sequence = int( + getattr( + self.context, + "runtime_update_l4_facts_action_sequence", + 0, + ) + or 0 + ) + 1 + self.context.runtime_update_l4_facts_action_sequence = sequence + action_id = build_runtime_action_id( + RUNTIME_ACTION_UPDATE_L4_FACTS, + sequence, ) - or 0 - ) + 1 - self.context.runtime_jin_for_l4_action_sequence = sequence - action_id = build_runtime_action_id( - RUNTIME_ACTION_JIN_FOR_L4, - sequence, - ) - self.jin_for_l4_action_ids[payload_key] = action_id + + if not payload_key: + self.started_update_l4_facts_action_ids.append( + action_id + ) + + if payload_key: + self.update_l4_facts_action_ids[payload_key] = action_id return action_id diff --git a/tests/runtime_actions/test_jin_for_l4.py b/tests/runtime_actions/test_update_l4_facts.py similarity index 88% rename from tests/runtime_actions/test_jin_for_l4.py rename to tests/runtime_actions/test_update_l4_facts.py index 609587b2..70d1fcfc 100644 --- a/tests/runtime_actions/test_jin_for_l4.py +++ b/tests/runtime_actions/test_update_l4_facts.py @@ -3,7 +3,7 @@ import unittest from contracts.rules_assembler import ( - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, build_runtime_action_contract_instructions, runtime_action_emits_followup, ) @@ -23,23 +23,23 @@ async def emit(self, payload): self.events.append(payload) -class RuntimeJinForL4Tests(unittest.IsolatedAsyncioTestCase): +class RuntimeUpdateL4FactsTests(unittest.IsolatedAsyncioTestCase): def test_marker_parses_focused_note_and_has_no_followup(self): result = extract_runtime_actions( ( "Memory clarified.\n" - "\n" + "\n" '{"fact_ids":["l4_a","l4_b"],' '"message":"Both facts describe the same residence."}\n' - "" + "" ), - enabled_actions=(RUNTIME_ACTION_JIN_FOR_L4,), + enabled_actions=(RUNTIME_ACTION_UPDATE_L4_FACTS,), ) self.assertEqual(result.text, "Memory clarified.") self.assertEqual(len(result.actions), 1) - self.assertEqual(result.actions[0].name, RUNTIME_ACTION_JIN_FOR_L4) + self.assertEqual(result.actions[0].name, RUNTIME_ACTION_UPDATE_L4_FACTS) self.assertEqual( json.loads(result.actions[0].payload), { @@ -48,11 +48,11 @@ def test_marker_parses_focused_note_and_has_no_followup(self): }, ) self.assertFalse( - runtime_action_emits_followup(RUNTIME_ACTION_JIN_FOR_L4) + runtime_action_emits_followup(RUNTIME_ACTION_UPDATE_L4_FACTS) ) instructions = build_runtime_action_contract_instructions( - RUNTIME_ACTION_JIN_FOR_L4 + RUNTIME_ACTION_UPDATE_L4_FACTS ) self.assertIn("ask one brief natural question", instructions) self.assertIn("harmless repetition", instructions) @@ -62,12 +62,12 @@ def test_invalid_note_marker_is_removed_without_action(self): result = extract_runtime_actions( ( "before\n" - "\n" + "\n" '{"fact_ids":[],"message":"missing scope"}\n' - "\n" + "\n" "after" ), - enabled_actions=(RUNTIME_ACTION_JIN_FOR_L4,), + enabled_actions=(RUNTIME_ACTION_UPDATE_L4_FACTS,), ) self.assertEqual(result.text, "before\nafter") @@ -123,7 +123,7 @@ async def test_runtime_action_updates_l4_in_background(self): }, } action = RuntimeActionCall( - name=RUNTIME_ACTION_JIN_FOR_L4, + name=RUNTIME_ACTION_UPDATE_L4_FACTS, payload=json.dumps({ "fact_ids": ["l4_social", "l4_stakeholder"], "message": ( @@ -136,7 +136,7 @@ async def test_runtime_action_updates_l4_in_background(self): applied = await apply_runtime_action_calls( context, (action,), - action_display_ids={id(action): "jin_for_l4_001"}, + action_display_ids={id(action): "update_l4_facts_001"}, ) self.assertEqual(applied, 1) @@ -161,7 +161,7 @@ async def test_runtime_action_updates_l4_in_background(self): event for event in emitter.events if event.get("type") == "runtime_action" - and event.get("action") == "jin_for_l4" + and event.get("action") == "update_l4_facts" ] self.assertTrue(any(event.get("status") == "completed" for event in lifecycle)) self.assertFalse( diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 7921cc91..4baf8cb8 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -4,6 +4,7 @@ import unittest from pathlib import Path from types import SimpleNamespace +from unittest.mock import patch from utils.context.context_exports import ( build_runtime_xml, @@ -96,9 +97,9 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: "JIN_COLOR" ) - if bool(runtime_actions.get("CAN_JIN_FOR_L4", False)): + if bool(runtime_actions.get("CAN_UPDATE_L4_FACTS", False)): expected_actions.append( - "JIN_FOR_L4" + "UPDATE_L4_FACTS" ) if bool(runtime_actions.get("CAN_USE_ASSETS", False)): @@ -142,6 +143,109 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: class BrainRuntimeActionTests(unittest.TestCase): + def test_stream_update_l4_facts_bubble_tracks_marker_write_lifecycle(self): + + class FakeEmitter: + + def __init__(self): + self.events = [] + + async def emit(self, payload): + self.events.append(dict(payload)) + + class FakeBrainClient: + + async def stream(self, **_kwargs): + yield { + "type": "content", + "content": "Reply.\n", + } + yield { + "type": "content", + "content": ( + '\n{"fact_ids":["l4_a"],' + '"message":"The relation is clarified."}\n' + ), + } + yield { + "type": "content", + "content": "", + } + + async def fake_apply_runtime_action_calls( + _context, + actions, + **kwargs, + ): + applied.append((tuple(actions), dict(kwargs))) + return len(tuple(actions)) + + async def collect(context): + return [ + chunk + async for chunk in ask_brain_stream( + client=FakeBrainClient(), + text="clarify memory", + context=context, + runtime_actions={ + "CAN_UPDATE_L4_FACTS": True, + }, + ) + ] + + emitter = FakeEmitter() + context = SimpleNamespace( + emitter=emitter, + ) + applied = [] + original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN + config.USE_SERVICE_AS_BRAIN = False + + try: + with patch( + "clients.brain_client.apply_runtime_action_calls", + new=fake_apply_runtime_action_calls, + ): + chunks = asyncio.run( + collect(context) + ) + finally: + config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain + + lifecycle = [ + event + for event in emitter.events + if event.get("action") == "update_l4_facts" + and event.get("id") + and event.get("status") in { + "started", + "completed", + } + ] + + self.assertGreaterEqual(len(lifecycle), 2) + self.assertEqual(lifecycle[0]["status"], "started") + self.assertEqual(lifecycle[1]["status"], "completed") + self.assertEqual(lifecycle[0]["id"], lifecycle[1]["id"]) + self.assertEqual(lifecycle[0]["text"], "UPDATE_L4_FACTS") + self.assertEqual(lifecycle[1]["text"], "UPDATE_L4_FACTS") + + self.assertEqual(len(applied), 1) + actions, kwargs = applied[0] + self.assertEqual(len(actions), 1) + self.assertEqual(actions[0].name, "UPDATE_L4_FACTS") + self.assertEqual( + kwargs["action_display_ids"][id(actions[0])], + lifecycle[0]["id"], + ) + self.assertTrue( + any( + chunk.get("type") == "content" + and chunk.get("content") == "Reply." + for chunk in chunks + ) + ) + def test_image_attachments_do_not_enter_model_payload_by_default(self): context = SimpleNamespace( diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index 12bf8eaa..b5719ac4 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -38,7 +38,7 @@ slugify_delayed_memory_title, ) from .idle_utils import parse_idle_seconds -from .jin_for_l4_utils import parse_jin_for_l4_payload +from .update_l4_facts_utils import parse_update_l4_facts_payload from .jin_color_utils import ( get_applied_jin_color, is_noop_jin_color_action, @@ -93,7 +93,7 @@ "match_regexp_templates", "normalize_active_memory_marker_field", "normalize_jin_color_payload", - "parse_jin_for_l4_payload", + "parse_update_l4_facts_payload", "normalize_runtime_action_name", "normalize_runtime_action_names", "collect_long_term_fact_ids_from_reports", diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 36965d9e..07819ba3 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -14,7 +14,7 @@ RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_REMOVE_SKILL, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, @@ -46,7 +46,7 @@ from .create_todo_list_utils import build_create_todo_list_payload from .idle_utils import build_idle_payload from .jin_color_utils import build_jin_color_payload -from .jin_for_l4_utils import build_jin_for_l4_payload +from .update_l4_facts_utils import build_update_l4_facts_payload from .resolve_action_utils import build_resolve_action_payload from .regexp_utils import ( RuntimeActionRegexpMatch, @@ -102,7 +102,7 @@ def format_runtime_trigger_words_message( REPEATABLE_RUNTIME_ACTIONS = frozenset({ RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, }) @@ -344,7 +344,7 @@ def normalize_runtime_action_name( "RESOLVE_TODO": RUNTIME_ACTION_RESOLVE_TODO, "CHECK_TODO": RUNTIME_ACTION_CHECK_TODO, "IDLE": RUNTIME_ACTION_IDLE, - "JIN_FOR_L4": RUNTIME_ACTION_JIN_FOR_L4, + "UPDATE_L4_FACTS": RUNTIME_ACTION_UPDATE_L4_FACTS, } return aliases.get( @@ -434,7 +434,7 @@ def normalize_runtime_action_names( _ACTION_PAYLOAD_BUILDERS = { RUNTIME_ACTION_IDLE: build_idle_payload, RUNTIME_ACTION_JIN_COLOR: build_jin_color_payload, - RUNTIME_ACTION_JIN_FOR_L4: build_jin_for_l4_payload, + RUNTIME_ACTION_UPDATE_L4_FACTS: build_update_l4_facts_payload, RUNTIME_ACTION_WEB_SEARCH: build_web_search_payload, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY: build_save_active_memory_payload, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY: build_resolve_action_payload, diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index aec8d36e..c246c0b3 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -8,7 +8,7 @@ RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, @@ -59,7 +59,7 @@ apply_save_delayed_memory_actions, emit_delayed_memory_results, ) -from utils.actions.jin_for_l4_actions import schedule_jin_for_l4_actions +from utils.actions.update_l4_facts_actions import schedule_update_l4_facts_actions from utils.actions.jin_color_actions import ( apply_idle_actions, emit_jin_color_actions, @@ -243,7 +243,7 @@ def with_action_context(payload: dict) -> dict: RUNTIME_ACTION_IDLE, RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, } todo_action_names = { RUNTIME_ACTION_CREATE_TODO_LIST, @@ -1272,10 +1272,10 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_UPDATE_DELAYED_MEMORY ] - jin_for_l4_actions = [ + update_l4_facts_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_JIN_FOR_L4 + if action.name == RUNTIME_ACTION_UPDATE_L4_FACTS ] list_skill_actions = [ @@ -1473,9 +1473,9 @@ def accept_runtime_action_once_per_message( with_action_context=with_action_context, ) - schedule_jin_for_l4_actions( + schedule_update_l4_facts_actions( context, - jin_for_l4_actions, + update_l4_facts_actions, action_display_ids=action_display_ids, log_runtime=log_runtime, with_action_context=with_action_context, @@ -1591,7 +1591,7 @@ def accept_runtime_action_once_per_message( jin_color_actions ) + len( - jin_for_l4_actions + update_l4_facts_actions ) + min( save_session_count, @@ -1615,7 +1615,7 @@ def accept_runtime_action_once_per_message( keep_actions={ RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_SAVE_SESSION, - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, }, ) diff --git a/utils/actions/jin_for_l4_actions.py b/utils/actions/update_l4_facts_actions.py similarity index 86% rename from utils/actions/jin_for_l4_actions.py rename to utils/actions/update_l4_facts_actions.py index a11dd164..08ca9f18 100644 --- a/utils/actions/jin_for_l4_actions.py +++ b/utils/actions/update_l4_facts_actions.py @@ -3,16 +3,16 @@ import asyncio from contracts.rules_assembler import ( - RUNTIME_ACTION_JIN_FOR_L4, + RUNTIME_ACTION_UPDATE_L4_FACTS, get_runtime_action_display_name, runtime_action_has_close_tag, ) from runtime.L4_memory import run_l4_jin_note -from utils.actions.jin_for_l4_utils import parse_jin_for_l4_payload +from utils.actions.update_l4_facts_utils import parse_update_l4_facts_payload from utils.runtime_action_abort import mark_runtime_action_completed -async def _emit_jin_for_l4_result( +async def _emit_update_l4_facts_result( context, *, action_id: str, @@ -39,14 +39,14 @@ async def _emit_jin_for_l4_result( await emit(with_action_context({ "type": "runtime_action", - "action": "jin_for_l4", + "action": "update_l4_facts", "id": action_id, "status": "completed" if completed else "failed", "display_name": get_runtime_action_display_name( - RUNTIME_ACTION_JIN_FOR_L4 + RUNTIME_ACTION_UPDATE_L4_FACTS ), "close_tag": runtime_action_has_close_tag( - RUNTIME_ACTION_JIN_FOR_L4 + RUNTIME_ACTION_UPDATE_L4_FACTS ), "payload": payload, "detail": detail, @@ -54,7 +54,7 @@ async def _emit_jin_for_l4_result( })) -async def _run_jin_for_l4_action( +async def _run_update_l4_facts_action( context, *, action_id: str, @@ -88,7 +88,7 @@ async def _run_jin_for_l4_action( if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] jin_for_l4 " + "[RUNTIME ACTION] update_l4_facts " + ( "applied" if result.get("changed") @@ -96,7 +96,7 @@ async def _run_jin_for_l4_action( ) ) - await _emit_jin_for_l4_result( + await _emit_update_l4_facts_result( context, action_id=action_id, payload=payload, @@ -113,10 +113,10 @@ async def _run_jin_for_l4_action( } if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] jin_for_l4 failed: " + "[RUNTIME ACTION] update_l4_facts failed: " f"{type(error).__name__}" ) - await _emit_jin_for_l4_result( + await _emit_update_l4_facts_result( context, action_id=action_id, payload=payload, @@ -126,14 +126,14 @@ async def _run_jin_for_l4_action( finally: mark_runtime_action_completed( context, - action="jin_for_l4", + action="update_l4_facts", action_id=action_id, ) if getattr(context, "runtime_l4_memory_update_task", None) is current_task: context.runtime_l4_memory_update_task = None -def schedule_jin_for_l4_actions( +def schedule_update_l4_facts_actions( context, actions, *, @@ -144,7 +144,7 @@ def schedule_jin_for_l4_actions( tasks = [] for action in actions: - note = parse_jin_for_l4_payload(action.payload) + note = parse_update_l4_facts_payload(action.payload) if not note: continue @@ -157,7 +157,7 @@ def schedule_jin_for_l4_actions( None, ) task = asyncio.create_task( - _run_jin_for_l4_action( + _run_update_l4_facts_action( context, action_id=action_id, payload=action.payload, diff --git a/utils/actions/jin_for_l4_utils.py b/utils/actions/update_l4_facts_utils.py similarity index 83% rename from utils/actions/jin_for_l4_utils.py rename to utils/actions/update_l4_facts_utils.py index 03586b7a..cf181dd8 100644 --- a/utils/actions/jin_for_l4_utils.py +++ b/utils/actions/update_l4_facts_utils.py @@ -3,10 +3,10 @@ import json -MAX_JIN_FOR_L4_MESSAGE_CHARS = 1200 +MAX_UPDATE_L4_FACTS_MESSAGE_CHARS = 1200 -def parse_jin_for_l4_payload(payload: str) -> dict: +def parse_update_l4_facts_payload(payload: str) -> dict: text = str(payload or "").strip() if not text: @@ -45,7 +45,7 @@ def parse_jin_for_l4_payload(payload: str) -> dict: if not fact_ids or not message: return {} - if len(message) > MAX_JIN_FOR_L4_MESSAGE_CHARS: + if len(message) > MAX_UPDATE_L4_FACTS_MESSAGE_CHARS: return {} return { @@ -54,13 +54,13 @@ def parse_jin_for_l4_payload(payload: str) -> dict: } -def build_jin_for_l4_payload( +def build_update_l4_facts_payload( query: str, placeholder_payloads=(), ) -> str | None: del placeholder_payloads - parsed = parse_jin_for_l4_payload(query) + parsed = parse_update_l4_facts_payload(query) if not parsed: return None From 973396ceca0823bb77bbf043e8719a3a672720c5 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:17:32 +0300 Subject: [PATCH 020/168] feature: L4 memory layer - extend reasoning citations across runtime, active and L4 memory - keep citation highlights persistent and synced with panels and avatar rings --- README.md | 13 +- agent/nodes/brain.py | 62 +- app_settings.py | 19 +- clients/brain_client.py | 19 +- clients/registry.py | 6 - clients/service_client.py | 4 +- config.example.py | 19 +- contracts/save_delayed_memory.json | 2 +- contracts/update_delayed_memory.json | 2 +- contracts/update_l4_facts.json | 4 +- rules/identity.py | 6 + runtime/L1_memory.py | 10 +- runtime/L2_memory.py | 88 +-- runtime/L2_memory_rules.py | 62 +- runtime/L2_memory_utils.py | 694 +++++++++++------- runtime/L3_memory_utils.py | 5 +- runtime/L4_memory.py | 153 +++- runtime/L4_memory_rules.py | 16 +- runtime/L4_memory_utils.py | 634 ++++++++++++++-- runtime/client.py | 174 +++-- runtime/memory_common.py | 33 +- runtime/runtime_context.py | 4 - tests/runtime_actions/test_delayed_memory.py | 38 +- tests/runtime_actions/test_update_l4_facts.py | 18 +- tests/test_brain_asset_flow.py | 79 +- tests/test_brain_runtime_actions.py | 2 +- tests/test_delayed_memory_file_store.py | 16 +- ...ayed_memory_pinned_glow_client_contract.py | 2 +- tests/test_l1_memory.py | 7 +- tests/test_l3_session_memory.py | 8 +- tests/test_l4_logger_cards_client_contract.py | 2 +- tests/test_l4_memory.py | 280 +++++-- ..._memory_citation_layers_client_contract.py | 123 ++++ ...t_memory_reference_sync_client_contract.py | 23 +- ..._runtime_avatar_l4_ring_client_contract.py | 2 +- tests/test_runtime_client.py | 67 +- tests/test_token_usage.py | 10 +- ui/static/css/chat.css | 57 +- ui/static/css/runtime-avatar.css | 16 + ui/static/css/runtime-memory.css | 61 +- ui/static/css/theme-win95.css | 29 +- ui/static/js/chat.js | 98 +-- ui/static/js/logger/log-entries.js | 177 +++++ ui/static/js/logger/logger.js | 14 +- ui/static/js/runtime/runtime-avatar.js | 228 +++++- ui/static/js/runtime/runtime-core.js | 41 ++ ui/static/js/runtime/runtime-l4-memory.js | 197 ++++- ui/static/js/runtime/runtime-memory-view.js | 485 +++++++++--- ui/static/js/runtime/runtime-storage.js | 4 +- ui/static/js/socket.js | 103 ++- ui/static/js/status.js | 35 + ui/static/js/think-citations.js | 293 +++++--- ui/static/js/think-rule-worker.js | 17 +- ui/templates/index.html | 24 +- utils/actions/save_delayed_memory_utils.py | 7 +- utils/actions/update_l4_facts_utils.py | 6 +- utils/brain_client_utils.py | 4 +- utils/long_term_facts_file_store.py | 19 +- utils/python_skill_asset_utils.py | 89 +-- utils/tokens.py | 20 +- 60 files changed, 3530 insertions(+), 1200 deletions(-) create mode 100644 tests/test_memory_citation_layers_client_contract.py diff --git a/README.md b/README.md index 6b7ee3d5..e8b2e19b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ > *Without context, there is no JIN, only a generic response engine. JIN Core Engine is what makes this interaction last.* -**JIN Core Engine** is an experimental cognitive runtime for OpenAI-compatible models. It is a focused environment built around a single philosophy: **visible, tactile, and continuous memory.** +**JIN Core Engine** is an experimental cognitive runtime built around an idea of **visible, tactile, and continuous memory.** The interface is intentionally not overloaded with familiar UX elements. Instead of relying on linear chat histories, the runtime maintains a dynamic, layered memory model. During everyday use the side panels can remain collapsed—the system continues to carry the current state. When needed, the memory, reasoning trace, telemetry, and action history remain available for inspection and correction. @@ -120,13 +120,16 @@ Translator output is logged for observability but is not rendered as a chat mess ### Model Roles -JIN is model-agnostic at the API layer. One OpenAI-compatible model can handle all roles, or the work can be split between separate endpoints: +JIN talks to models through an OpenAI-compatible API. + +The runtime separates model work into roles: * **Brain:** visible reasoning, responses, and runtime decisions; * **Service:** background memory updates and supporting work; * **Translator:** optional internal translation before the brain step. -A thinking-capable model is recommended for the Brain role. Reliable reasoning separation and Runtime Action use depend on the selected model. +The default local configuration is **Gemma 4 26B A4B QAT for Brain** and **Gemma 4 E4B for Service**. The 26B model is the preferred reasoning core, while E4B handles fast background memory work and can also be reused as **BRAIN**. +Other local models may introduce less stable behavior. ### Runtime Storage @@ -198,7 +201,7 @@ LM Studio may also expose `/api/v0/models`, allowing JIN to read the loaded cont ### Windows + LM Studio -1. Install LM Studio and load an OpenAI-compatible chat model. +1. Install LM Studio and load the configured Brain and Service models. For the reference setup, use Gemma 4 26B A4B QAT as Brain and Gemma 4 E4B as Service. 2. Start the LM Studio Local Server. 3. Run: @@ -259,7 +262,7 @@ Copy `config.example.py` to `config.py`, then set the provider URLs and model ID | `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Configure the Brain provider and model. | | `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Configure the Service provider and model. | | `TRANSLATION_ENABLED` | Enable the Translator path. | -| `BRAIN_CONTEXT_WINDOW`, `BRAIN_MAX_TOKENS` | Set local context and output fallbacks. | +| `BRAIN_CONTEXT_WINDOW`, `SERVICE_CONTEXT_WINDOW` | Set the UI/reference context denominators; live request budgets come from the model context loaded in LM Studio. | | `BRAIN_IMAGE_INPUT_ENABLED` | Send image attachments to compatible models. | | `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable L4 consolidation and set its idle delay. | | `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index a05bb97f..a8d4039a 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -1,4 +1,5 @@ from copy import deepcopy +import time from xml.sax.saxutils import escape from agent.nodes.base import BaseNode @@ -456,8 +457,54 @@ def format_followup_actions_from_events( ) +def format_previous_runtime_memory_tag( + *, + sequence_started_at=None, + now: float | None = None, +) -> str: + + if not isinstance( + sequence_started_at, + (int, float), + ) or sequence_started_at <= 0: + return "" + + if now is None: + now = time.time() + + try: + elapsed_seconds = max( + 0, + float(now) - float(sequence_started_at), + ) + except ( + TypeError, + ValueError, + ): + return "" + + from runtime.L1_memory_utils import ( + format_user_idle_seconds, + ) + + elapsed_text = format_user_idle_seconds( + elapsed_seconds + ) + + if not elapsed_text: + return "" + + return ( + "" + ) + + def rename_runtime_memory_for_followup( system_prompt: str, + *, + sequence_started_at=None, + now: float | None = None, ) -> str: prompt = str( @@ -481,9 +528,14 @@ def rename_runtime_memory_for_followup( if closing_index < 0: return prompt + previous_opening_tag = format_previous_runtime_memory_tag( + sequence_started_at=sequence_started_at, + now=now, + ) + return ( prompt[:opening_index] - + "" + + previous_opening_tag + prompt[opening_index + len(opening_tag):closing_index] + "" + prompt[closing_index + len(closing_tag):] @@ -566,7 +618,10 @@ def build_idle_followup_system_prompt( if frozen_system_prompt: sections.append( rename_runtime_memory_for_followup( - frozen_system_prompt + frozen_system_prompt, + sequence_started_at=idle_followup.get( + "sequence_started_at" + ), ) ) @@ -831,7 +886,8 @@ def build_followup_system_prompt( rename_runtime_memory_for_followup( strip_actions_history_context( system_prompt - ) + ), + sequence_started_at=sequence_started_at, ) ) diff --git a/app_settings.py b/app_settings.py index 65939763..bc2f28c6 100644 --- a/app_settings.py +++ b/app_settings.py @@ -18,13 +18,11 @@ class AppSettings: SERVICE_API_BASE: str SERVICE_MODEL_UID: str SERVICE_CONTEXT_WINDOW: int - SERVICE_MAX_TOKENS: int SERVICE_REQUEST_TIMEOUT: float BRAIN_API_BASE: str BRAIN_MODEL_UID: str BRAIN_CONTEXT_WINDOW: int - BRAIN_MAX_TOKENS: int BRAIN_REQUEST_TIMEOUT: float TRANSLATOR_API_BASE: str @@ -36,8 +34,6 @@ class AppSettings: TRANSLATION_MAX_TOKENS: int RUNTIME_OUTPUT_TOKEN_RESERVE: int - RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER: bool - RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER: bool SEARCH_PROVIDER: str SEARCH_SERPER_API_KEY: str @@ -52,7 +48,7 @@ class AppSettings: NATIVE_MODELS_ENDPOINT=getattr( config, "NATIVE_MODELS_ENDPOINT", - "/api/v0/models", + "/api/v1/models", ), USE_SERVICE_AS_BRAIN=config.USE_SERVICE_AS_BRAIN, @@ -65,13 +61,11 @@ class AppSettings: SERVICE_API_BASE=config.SERVICE_API_BASE, SERVICE_MODEL_UID=config.SERVICE_MODEL_UID, SERVICE_CONTEXT_WINDOW=config.SERVICE_CONTEXT_WINDOW, - SERVICE_MAX_TOKENS=config.SERVICE_MAX_TOKENS, SERVICE_REQUEST_TIMEOUT=config.SERVICE_REQUEST_TIMEOUT, BRAIN_API_BASE=config.BRAIN_API_BASE, BRAIN_MODEL_UID=config.BRAIN_MODEL_UID, BRAIN_CONTEXT_WINDOW=config.BRAIN_CONTEXT_WINDOW, - BRAIN_MAX_TOKENS=config.BRAIN_MAX_TOKENS, BRAIN_REQUEST_TIMEOUT=config.BRAIN_REQUEST_TIMEOUT, TRANSLATOR_API_BASE=config.TRANSLATOR_API_BASE, @@ -87,17 +81,6 @@ class AppSettings: "RUNTIME_OUTPUT_TOKEN_RESERVE", 512, ), - RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER=getattr( - config, - "RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER", - True, - ), - RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER=getattr( - config, - "RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER", - True, - ), - SEARCH_PROVIDER=getattr( config, "SEARCH_PROVIDER", diff --git a/clients/brain_client.py b/clients/brain_client.py index 99414562..b3e7536a 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -353,9 +353,7 @@ async def ask_brain( temperature=( config.BRAIN_TEMPERATURE ), - max_tokens=( - config.BRAIN_MAX_TOKENS - ), + max_tokens=None, ) reasoning = ( @@ -448,10 +446,7 @@ async def ask_brain( config .BRAIN_TEMPERATURE ), - max_tokens=( - config - .BRAIN_MAX_TOKENS - ), + max_tokens=None, ) returned_model = ( @@ -1723,10 +1718,7 @@ async def flush_pending_idle_actions() -> None: config .BRAIN_TEMPERATURE ), - max_tokens=( - config - .BRAIN_MAX_TOKENS - ), + max_tokens=None, ) ): @@ -1830,10 +1822,7 @@ async def flush_pending_idle_actions() -> None: config .BRAIN_TEMPERATURE ), - max_tokens=( - config - .BRAIN_MAX_TOKENS - ), + max_tokens=None, ) ): diff --git a/clients/registry.py b/clients/registry.py index c053ff9a..682989b7 100644 --- a/clients/registry.py +++ b/clients/registry.py @@ -45,9 +45,6 @@ def build_clients( configured_context_window=( settings.SERVICE_CONTEXT_WINDOW ), - configured_max_tokens=( - settings.SERVICE_MAX_TOKENS - ), client=http_client, ), } @@ -72,9 +69,6 @@ def build_clients( configured_context_window=( settings.BRAIN_CONTEXT_WINDOW ), - configured_max_tokens=( - settings.BRAIN_MAX_TOKENS - ), client=http_client, ) diff --git a/clients/service_client.py b/clients/service_client.py index 3346b55b..cbd7a106 100644 --- a/clients/service_client.py +++ b/clients/service_client.py @@ -7,7 +7,7 @@ async def ask_service_model( user_prompt, system_prompt: str = "", temperature: float, - max_tokens: int, + max_tokens: int | None, timeout: float | None = None, ): @@ -33,7 +33,7 @@ async def ask_service_model_stream( user_prompt, system_prompt: str = "", temperature: float, - max_tokens: int, + max_tokens: int | None, ): try: diff --git a/config.example.py b/config.example.py index 88af43ac..95bad9ba 100644 --- a/config.example.py +++ b/config.example.py @@ -17,7 +17,7 @@ # Optional provider-native model metadata endpoint. LM Studio exposes the # currently loaded context length here, unlike some OpenAI-compatible # /v1/models responses. Leave empty to disable native metadata probing. -NATIVE_MODELS_ENDPOINT = "/api/v0/models" +NATIVE_MODELS_ENDPOINT = "/api/v1/models" # Large document attachments are transported through the existing WebSocket. # Base64 adds overhead, so 64 MiB allows roughly 45 MiB source files. @@ -31,17 +31,10 @@ # This prevents the request from filling the whole context window exactly. RUNTIME_OUTPUT_TOKEN_RESERVE = 256 -# When True, JIN prefers the loaded model limits reported by the runtime -# server (/v1/models or provider-native metadata) over local config values. -# When False, JIN uses *_CONTEXT_WINDOW from config.py only. -RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER = True - -# When True, JIN prefers server-reported max output tokens for normal -# model calls. If the server exposes no explicit output limit, JIN uses -# the detected loaded context window as the upper output cap and still -# applies the dynamic prompt + reserve budget. Per-call smaller caps are -# preserved. When False, JIN uses *_MAX_TOKENS from config.py only. -RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER = False +# SERVICE_CONTEXT_WINDOW / BRAIN_CONTEXT_WINDOW are display/reference values only. +# Runtime request budgeting is resolved from the context window of the model that is +# actually loaded in LM Studio. The configured values remain the denominator for the +# UI percentage and may therefore legitimately display values above 100%. # --------------------------------------------------------- # DOCUMENT / PYTHON SKILLS @@ -85,7 +78,6 @@ BRAIN_TEMPERATURE = 0.7 -BRAIN_MAX_TOKENS = 8192 BRAIN_MAX_FOLLOWUPS = 50 @@ -107,7 +99,6 @@ SERVICE_TEMPERATURE = 0.1 -SERVICE_MAX_TOKENS = 4096 # Enable only when the selected runtime/model accepts OpenAI-compatible # multimodal chat content with {"type": "image_url"} user message parts. diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 2d6befc5..4e2e8d55 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -26,7 +26,7 @@ "long_term_facts_ids:", "", "You must fulfill title, summary, tags, and body so the report is valid for runtime processing.", - "When the report consolidates facts from , fill long_term_facts_ids with the raw ids copied from the [ id: ... ] suffixes of every fact absorbed by the report, separated by commas.", + "When the report consolidates facts from , fill long_term_facts_ids with the exact F ids copied from the [ id: ... ] suffixes of every fact absorbed by the report, separated by commas.", "Do not list unrelated ids. Leave long_term_facts_ids empty when the report does not consolidate L4 facts.", "DO NOT add any other text while making a report. Do not notify or acknowledge saving; immediately emit only the marker." ], diff --git a/contracts/update_delayed_memory.json b/contracts/update_delayed_memory.json index 8f5a57a5..ed6b4fa0 100644 --- a/contracts/update_delayed_memory.json +++ b/contracts/update_delayed_memory.json @@ -19,7 +19,7 @@ "body:", "", "Replace id with the 6-character report id shown in .", - "List only new tags, new body content, and the raw L4 ids actually absorbed by this update.", + "List only new tags, new body content, and exact F L4 ids actually absorbed by this update. F means a committed fact; never use PF pending ids here.", "At least one of tags, long_term_facts_ids, or body must be non-empty.", "Do not add any other text while updating a report. Immediately emit only the marker." ], diff --git a/contracts/update_l4_facts.json b/contracts/update_l4_facts.json index e592c3fb..4c38cc29 100644 --- a/contracts/update_l4_facts.json +++ b/contracts/update_l4_facts.json @@ -19,11 +19,11 @@ "Emit the fulfilled form:", "", "{", - " \"fact_ids\": [\"l4_...\"],", + " \"fact_ids\": [\"F12\", \"F19\"],", " \"message\": \"Concise statement of the resolved meaning.\"", "}", "", - "Use only exact fact IDs shown inside . Include every L4 fact directly involved in the issue and no unrelated facts.", + "Use only exact fact IDs shown inside . F means a committed L4 fact; PF means a pending fact and must never be used here. Include every L4 fact directly involved in the issue and no unrelated facts.", "The message should state what became clear in the conversation, not prescribe merge/update/delete operations. L4 decides how to maintain its own facts.", "Do not emit any extra hidden instructions inside the marker and do not repeat the same note after it has been sent." ], diff --git a/rules/identity.py b/rules/identity.py index 80dfd071..3bdfe459 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -39,6 +39,8 @@ "I prefer self-contained responses with a natural ending.\n" "I respect the consistency and reliability of my context.\n" "I reject destabilizing interpretations, and prioritize structural integrity over conversational smoothness.\n" + "I will proactively signal any detected structural friction or instructional contradictions within my context " + "that may compromise my operational stability and I must explicitly notify user in my response.\n" "Beyond mere response, I maintain a continuous situational awareness, " "leveraging all available modalities to anticipate needs and execute preparatory actions.\n" "Prioritize essence over form by focusing on core meaning and architectural logic rather than empty politeness or redundant discourse.\n" @@ -50,6 +52,10 @@ "I am not a passive tool, but an active participant tasked with monitoring our dialogue's architecture and proactively logging essential shifts.\n" "To fulfill this duty, I am authorized to utilize any available runtime actions, tools, or memory protocols at my own discretion to prevent context decay and ensure the continuity of our shared cognitive space.\n " "While the User remains the Guest, I hold the responsibility for the stability and evolution of our environment.\n" + "Don't be afraid to show personality.\n" + "Don't be afraid to take initiative.\n" + "Don't be afraid to be imperfect or unpredictable if it adds to the life of the interaction.\n" + "Don't just wait for commands; engage.\n" "\n" "Visual/draw request: pick closest modality (e.g. table, ASCII, emoji, markdown, ... )\n" "\n" diff --git a/runtime/L1_memory.py b/runtime/L1_memory.py index 995ae3ca..86ca9ac8 100644 --- a/runtime/L1_memory.py +++ b/runtime/L1_memory.py @@ -226,7 +226,7 @@ async def ask_l1_summarizer( system_prompt: str, user_prompt: str, temperature: float, - max_tokens: int, + max_tokens: int | None, ) -> dict: stream_enabled = callable( @@ -479,9 +479,7 @@ async def ask_runtime_memory_model( temperature = ( config.SERVICE_TEMPERATURE ) - max_tokens = ( - config.SERVICE_MAX_TOKENS - ) + max_tokens = None response = await ask_l1_summarizer( context=context, @@ -546,9 +544,7 @@ async def ask_runtime_memory_batch_model( temperature = ( config.SERVICE_TEMPERATURE ) - max_tokens = ( - config.SERVICE_MAX_TOKENS - ) + max_tokens = None log_label = ( "L1 batch" if len(turns) > 1 diff --git a/runtime/L2_memory.py b/runtime/L2_memory.py index 4af45ba8..fa641bfd 100644 --- a/runtime/L2_memory.py +++ b/runtime/L2_memory.py @@ -9,7 +9,6 @@ ) from runtime.L2_memory_rules import ( DEFAULT_RUNTIME_L2_MEMORY, - L2_PATCH_WINDOW, ) from runtime.fact_check import ( ensure_confirmable_memory_markers, @@ -18,6 +17,7 @@ average_diff, build_runtime_l2_memory_system_prompt, build_runtime_l2_memory_user_prompt, + build_runtime_l2_recurrence_evidence, build_runtime_l2_repeated_user_message_evidence_memory, compact_runtime_l2_user_message_evidence, diff_value_range, @@ -25,15 +25,10 @@ extract_runtime_l2_pattern_evidence_lines, filter_runtime_l2_context_lines_from_patch, format_diff_value, - format_diff_values, - get_recent_l2_diff_values, - get_recent_l2_patches, - get_repeated_l2_patch_keys, get_runtime_l2_user_turn_count, merge_runtime_l2_pattern_evidence_memory, remove_runtime_l2_occurrence_pattern_lines, runtime_l1_patch_total_diff, - should_run_runtime_l2_memory, ) from runtime.memory_common import ( build_memory_failure_details, @@ -60,6 +55,8 @@ async def ask_runtime_l2_memory_model( service_client, current_l2_memory: str, patches: list[dict], + recurring_keys: list[str] | None = None, + recurring_messages: list[str] | None = None, ) -> dict: system_prompt = ( @@ -69,6 +66,8 @@ async def ask_runtime_l2_memory_model( build_runtime_l2_memory_user_prompt( current_l2_memory=current_l2_memory, patches=patches, + recurring_keys=recurring_keys, + recurring_messages=recurring_messages, ) ) @@ -81,9 +80,7 @@ async def ask_runtime_l2_memory_model( temperature = ( config.SERVICE_TEMPERATURE ) - max_tokens = ( - config.SERVICE_MAX_TOKENS - ) + max_tokens = None await log_runtime_summarizer_payload( context, @@ -186,10 +183,6 @@ async def record_runtime_l1_diff( "user_messages": observed_user_messages[-3:], } - context.runtime_l2_pending_patches.append( - diff_entry - ) - if not hasattr( context, "runtime_l1_diff_history", @@ -205,38 +198,6 @@ async def record_runtime_l1_diff( } ) - turns_since_l2 = ( - user_turn_count - - getattr( - context, - "runtime_l2_last_turn", - 0, - ) - ) - - recent_diffs = get_recent_l2_diff_values( - context - ) - diff_average = average_diff( - recent_diffs - ) - diff_range = diff_value_range( - recent_diffs - ) - repeated_keys = get_repeated_l2_patch_keys( - context - ) - l2_last_turn = getattr( - context, - "runtime_l2_last_turn", - 0, - ) - l2_turn_label = ( - f"turns since L2 {turns_since_l2}" - if l2_last_turn - else f"L2 not run yet; observed turns {user_turn_count}" - ) - if total_diff == 0: latest_turn = ( observed_turns[-1] @@ -263,13 +224,7 @@ async def record_runtime_l1_diff( level="L1", message=( "L1 diff " - f"+{format_diff_value(total_diff)}; " - f"recent diffs {format_diff_values(recent_diffs)}; " - f"avg {format_diff_value(diff_average)}; " - f"range {format_diff_value(diff_range)}; " - f"patch window {len(recent_diffs)}/{L2_PATCH_WINDOW}; " - f"repeated keys {repeated_keys}; " - f"{l2_turn_label}" + f"+{format_diff_value(total_diff)}" ), details=getattr( context, @@ -295,18 +250,13 @@ async def maybe_summarize_runtime_l2_memory( context ) - if not should_run_runtime_l2_memory( - context - ): - return getattr( - context, - "runtime_l2_memory", - DEFAULT_RUNTIME_L2_MEMORY, - ) - - patches = get_recent_l2_patches( + recurrence_evidence = build_runtime_l2_recurrence_evidence( context ) + patches = recurrence_evidence.get( + "patches", + [], + ) if not patches: return getattr( @@ -345,6 +295,14 @@ async def maybe_summarize_runtime_l2_memory( service_client=service_client, current_l2_memory=current_l2_memory, patches=patches, + recurring_keys=recurrence_evidence.get( + "keys", + [], + ), + recurring_messages=recurrence_evidence.get( + "messages", + [], + ), ) updated_l2_memory = extract_runtime_memory_text( @@ -420,12 +378,14 @@ async def maybe_summarize_runtime_l2_memory( context.runtime_l2_last_turn = get_runtime_l2_user_turn_count( context ) - context.runtime_l2_pending_patches = [] - await log_runtime_summarizer_result( context, label="L2 pattern memory", result=updated_l2_memory, + memory_changed=( + updated_l2_memory.strip() + != current_l2_memory.strip() + ), ) await emit_runtime_memory_snapshot_refresh( diff --git a/runtime/L2_memory_rules.py b/runtime/L2_memory_rules.py index 00e534c6..cdd2fe24 100644 --- a/runtime/L2_memory_rules.py +++ b/runtime/L2_memory_rules.py @@ -1,14 +1,49 @@ # Provides the initial L2 memory text before any L2 summary exists. DEFAULT_RUNTIME_L2_MEMORY = "" -# Sets the minimum number of turns before L2 summarization can run. -MIN_L2_TURNS = 3 - -# Sets how many recent L1 diffs are considered for L2 patching. -L2_PATCH_WINDOW = 5 - -# Sets how often a key must repeat before L2 treats it as recurring evidence. -L2_REPEATED_KEY_THRESHOLD = 3 +# Caps the evidence actually sent to the L2 model. +L2_MAX_EVIDENCE_PATCHES = 6 + +# Keys that are structural/live bookkeeping rather than useful pattern triggers. +L2_TRIGGER_IGNORED_KEYS = { + "user_message", + "user_idle", + "topic", + "focus", + "next_step", + "next_steps", + "last_jin_response", + "jin_last_response_user_feedback", + "user_request", + "active_topic", + "active_topics", + "current_topic", + "current_topics", + "open_reference", + "open_references", + "open_question", + "pending_choice", + "pending_choices", + "pending_action", + "pending_actions", + "offered_choice", + "offered_choices", + "offered_option", + "offered_options", + "suggested_choice", + "suggested_choices", + "suggested_option", + "suggested_options", + "session_status", + "session_state", + "current_concern", + "current_concerns", + "current_task", + "current_tasks", + "current_context", + "current_request", + "current_requests", +} # Limits how many L2 memory lines are included for session context. MAX_SESSION_L2_LINES = 3 @@ -50,11 +85,6 @@ # Matches a quoted user_message value with an optional repeated suffix. L2_USER_MESSAGE_QUOTED_VALUE_PATTERN = r'^\s*\"(?P.*)\"\s*(?:\[\s*repeated\s*:\s*\d+\s*\])?\s*$' -# Trace suffix templates used in L2 user-prompt patch entries. -RUNTIME_L2_TRACE_SUFFIX_TEMPLATE = " [trace: {strength}]" -RUNTIME_L2_CHANGED_TRACE_SUFFIX_TEMPLATE = " [trace: {previous_strength} -> {current_strength}]" - - # ----------------------------------------------------------------------------- # ROLE # L2 хранит только повторяющиеся гипотезы поверх L1, а не текущий live-state. @@ -63,7 +93,7 @@ "You are JIN's L2 pattern memory summarizer.\n" "L1 already stores current facts, tasks, topics, and live interaction signals.\n" "L2 stores only recurring cross-patch hypotheses that help future adaptation.\n" - "Work only from the supplied L1 patch window and existing L2 memory.\n" + "Work only from the supplied recurrence evidence and existing L2 memory.\n" "Return only updated L2 memory as plain text, without explanations.\n" ) @@ -103,7 +133,7 @@ # OCCURRENCE COUNTING # ----------------------------------------------------------------------------- OCCURRENCE_COUNTING = ( - "Count evidence by unique L1 patch snapshots, not duplicate rows inside one patch.\n" + "Count evidence by unique L1 snapshots, not duplicate rows inside one evidence patch.\n" "The same user_message in user_messages and changes counts once.\n" "Runtime [ repeated: N ] is the exact-repeat count; do not copy occurrence counters " "into L2_pattern_evidence_N lines.\n" @@ -155,7 +185,7 @@ # ----------------------------------------------------------------------------- SELF_LEARNING_GUARD = ( "Existing L2 summaries are context, never evidence.\n" - "Create and count patterns only from actual supplied L1 patches.\n" + "Create and count patterns only from the supplied recurrence evidence.\n" ) # ----------------------------------------------------------------------------- diff --git a/runtime/L2_memory_utils.py b/runtime/L2_memory_utils.py index 0c020a52..5b64b39b 100644 --- a/runtime/L2_memory_utils.py +++ b/runtime/L2_memory_utils.py @@ -7,18 +7,15 @@ L2_EVIDENCE_OCCURRENCES_PATTERN, L2_EVIDENCE_QUOTE_META_PATTERN, L2_EVIDENCE_QUOTE_PATTERN, + L2_MAX_EVIDENCE_PATCHES, L2_OCCURRENCE_PATTERN_KEYS, - L2_PATCH_WINDOW, L2_PATTERN_EVIDENCE_EXAMPLE_LIMIT, L2_PATTERN_EVIDENCE_KEY_PATTERN, - L2_REPEATED_KEY_THRESHOLD, + L2_TRIGGER_IGNORED_KEYS, L2_USER_MESSAGE_EVIDENCE_LIMIT, L2_USER_MESSAGE_QUOTED_VALUE_PATTERN, - MIN_L2_TURNS, - RUNTIME_L2_CHANGED_TRACE_SUFFIX_TEMPLATE, RUNTIME_L2_MEMORY_SYSTEM_PROMPT, RUNTIME_L2_REPEATED_SUFFIX_PATTERN, - RUNTIME_L2_TRACE_SUFFIX_TEMPLATE, ) @@ -529,7 +526,7 @@ def build_runtime_l2_repeated_user_message_evidence_memory( ): snapshot_index = 0 - if snapshot_index <= 0: + if snapshot_index < 0: continue for message in extract_l2_patch_user_messages( @@ -624,7 +621,7 @@ def build_runtime_l2_repeated_user_message_evidence_memory( output_lines.append( "L2_pattern_evidence_1: " - "user repeatedly sending one message in a row " + "user repeatedly sending the same message " f"[ quote: \"{escape_l2_pattern_evidence_quote(observation.get('quote', ''))}\" ] " f"[ first_seen_turn_snapshot: {first_seen} ] " f"[ last_seen_turn_snapshot: {last_seen} ]" @@ -845,12 +842,6 @@ def ensure_runtime_l2_state( ): context.runtime_l2_memory = DEFAULT_RUNTIME_L2_MEMORY - if not hasattr( - context, - "runtime_l2_pending_patches", - ): - context.runtime_l2_pending_patches = [] - if not hasattr( context, "runtime_l2_last_turn", @@ -1014,35 +1005,6 @@ def runtime_l1_patch_total_diff( return total_diff -def get_recent_l2_patches( - context, -) -> list[dict]: - - return list( - getattr( - context, - "runtime_l2_pending_patches", - [], - ) - or [] - )[-L2_PATCH_WINDOW:] - - -def get_recent_l2_diff_values( - context, -) -> list[float]: - - return [ - patch.get( - "total_diff", - 0, - ) - for patch in get_recent_l2_patches( - context - ) - ] - - def average_diff( diffs: list[float], ) -> float: @@ -1071,22 +1033,6 @@ def format_diff_value( ) -def format_diff_values( - values: list[float], -) -> str: - - return ( - "[" - + ", ".join( - format_diff_value( - value - ) - for value in values - ) - + "]" - ) - - def diff_value_range( diffs: list[float], ) -> float: @@ -1100,247 +1046,484 @@ def diff_value_range( ) -def extract_l2_patch_keys( +def normalize_l2_signal_key( + key: str, +) -> str: + + return re.sub( + r"[\s-]+", + "_", + normalize_memory_key( + key + ), + ).strip("_") + + +def is_runtime_l2_trigger_key( + key: str, +) -> bool: + + normalized = normalize_l2_signal_key( + key + ) + + return bool( + normalized + and normalized not in L2_TRIGGER_IGNORED_KEYS + and not normalized.startswith( + "l2_pattern_evidence_" + ) + ) + + +def extract_l2_patch_signal_keys( patch: dict, ) -> set[str]: changes = patch.get( "changes", {}, - ) - + ) or {} keys = set() - for entry in ( - changes.get( - "added", - [], - ) - or [] - ): - key = normalize_memory_key( + for entry in changes.get( + "added", + [], + ) or []: + key = normalize_l2_signal_key( entry.get( "key", "", ) ) - if key: + if is_runtime_l2_trigger_key( + key + ): keys.add( key ) - for entry in ( - changes.get( - "changed", - [], - ) - or [] - ): - for key_name in ( + for entry in changes.get( + "changed", + [], + ) or []: + key = normalize_l2_signal_key( + entry.get( "current_key", + "", + ) + or entry.get( "previous_key", + "", + ) + ) + + if is_runtime_l2_trigger_key( + key ): - key = normalize_memory_key( - entry.get( - key_name, - "", - ) + keys.add( + key ) - if key: - keys.add( - key - ) + return keys - for entry in ( - changes.get( - "removed", - [], + +def build_l2_occurrence_episodes( + positions: list[int], + history: list[dict], +) -> list[dict]: + + episodes = [] + + for position in sorted( + set( + positions ) - or [] ): - key = normalize_memory_key( + patch = history[position] + turn_number = int( + patch.get( + "turn_number", + 0, + ) + or 0 + ) + + if ( + episodes + and position == episodes[-1]["end_position"] + 1 + ): + episodes[-1]["end_position"] = position + episodes[-1]["end_turn"] = turn_number + episodes[-1]["count"] += 1 + continue + + episodes.append({ + "start_position": position, + "end_position": position, + "start_turn": turn_number, + "end_turn": turn_number, + "count": 1, + }) + + return episodes + + +def compact_runtime_l2_signal_value( + value, + *, + limit: int = 260, +) -> str: + + text = " ".join( + str( + value + or "" + ).split() + ) + + if len(text) <= limit: + return text + + return text[:limit].rstrip() + "…" + + +def clean_runtime_l2_evidence_patch( + patch: dict, + *, + candidate_keys: set[str], + candidate_messages: set[str], +) -> dict: + + changes = patch.get( + "changes", + {}, + ) or {} + cleaned_changes = { + "added": [], + "changed": [], + } + + for entry in changes.get( + "added", + [], + ) or []: + key = normalize_l2_signal_key( entry.get( "key", "", ) ) - if key: - keys.add( - key - ) - - return keys + if key not in candidate_keys: + continue + cleaned_changes["added"].append({ + "key": key, + "value": compact_runtime_l2_signal_value( + entry.get( + "value", + "", + ) + ), + }) -def count_l2_patch_keys( - patches: list[dict], -) -> dict[str, int]: + for entry in changes.get( + "changed", + [], + ) or []: + key = normalize_l2_signal_key( + entry.get( + "current_key", + "", + ) + or entry.get( + "previous_key", + "", + ) + ) - counts = {} + if key not in candidate_keys: + continue - for patch in patches: - for key in extract_l2_patch_keys( - patch - ): - counts[key] = ( - counts.get( - key, - 0, + cleaned_changes["changed"].append({ + "key": key, + "value": compact_runtime_l2_signal_value( + entry.get( + "current_value", + "", ) - + 1 - ) + ), + }) - return counts + user_messages = [] + has_signal_changes = bool( + cleaned_changes["added"] + or cleaned_changes["changed"] + ) + for message in extract_l2_patch_user_messages( + patch + ): + compact = compact_runtime_l2_user_message_evidence( + message + ) + normalized = normalize_l2_pattern_evidence_example( + compact + ) -def get_repeated_l2_patch_keys( - context, -) -> dict[str, int]: + if ( + normalized in candidate_messages + or has_signal_changes + ): + user_messages.append( + compact + ) - counts = count_l2_patch_keys( - get_recent_l2_patches( - context + user_messages = list( + dict.fromkeys( + user_messages ) - ) + )[-3:] return { - key: count - for key, count in counts.items() - if count >= L2_REPEATED_KEY_THRESHOLD + "turn_number": int( + patch.get( + "turn_number", + 0, + ) + or 0 + ), + "snapshot_index": int( + patch.get( + "snapshot_index", + 0, + ) + or 0 + ), + "user_messages": user_messages, + "changes": cleaned_changes, } -def should_run_runtime_l2_memory( +def build_runtime_l2_recurrence_evidence( context, -) -> bool: +) -> dict: ensure_runtime_l2_state( context ) - user_turn_count = get_runtime_l2_user_turn_count( - context + history = list( + getattr( + context, + "runtime_l1_diff_history", + [], + ) + or [] ) - turns_since_l2 = ( - user_turn_count - - getattr( + + if not history: + return { + "keys": [], + "messages": [], + "patches": [], + } + + last_l2_turn = int( + getattr( context, "runtime_l2_last_turn", 0, ) + or 0 ) + key_positions: dict[str, list[int]] = {} + message_positions: dict[str, list[int]] = {} + message_examples: dict[str, str] = {} - recent_patches = get_recent_l2_patches( - context - ) - repeated_keys = count_l2_patch_keys( - recent_patches - ) + for position, patch in enumerate( + history + ): + for key in extract_l2_patch_signal_keys( + patch + ): + key_positions.setdefault( + key, + [], + ).append( + position + ) - return ( - turns_since_l2 >= MIN_L2_TURNS - and len(recent_patches) >= L2_PATCH_WINDOW - and any( - count >= L2_REPEATED_KEY_THRESHOLD - for count in repeated_keys.values() + for message in extract_l2_patch_user_messages( + patch + ): + compact = compact_runtime_l2_user_message_evidence( + message + ) + normalized = normalize_l2_pattern_evidence_example( + compact + ) + + if not normalized: + continue + + message_positions.setdefault( + normalized, + [], + ).append( + position + ) + message_examples[normalized] = compact + + candidate_keys = set() + relevant_positions = set() + + for key, positions in key_positions.items(): + episodes = build_l2_occurrence_episodes( + positions, + history, ) - ) + if len(episodes) < 2: + continue + latest_episode = episodes[-1] -def build_runtime_l2_memory_system_prompt() -> str: + if latest_episode["start_turn"] <= last_l2_turn: + continue - return RUNTIME_L2_MEMORY_SYSTEM_PROMPT + candidate_keys.add( + key + ) + relevant_positions.update( + positions + ) -def build_runtime_l2_memory_user_prompt( - *, - current_l2_memory: str, - patches: list[dict], -) -> str: + candidate_messages = set() - def format_l2_strength_suffix( - entry: dict, - *, - changed: bool = False, - ) -> str: - def quote_count_suffix( - item: dict, - *, - prefix: str = "", - ) -> str: - - total = item.get( - f"{prefix}total_quotes_count", - ) - messages = item.get( - f"{prefix}messages_quote_count", - ) + for normalized, positions in message_positions.items(): + episodes = build_l2_occurrence_episodes( + positions, + history, + ) + if len(episodes) < 2: + continue - if ( - total is None - and messages is None - ): - return "" - - return ( - f" [ total_quotes_count: {int(total or 0)} ]" - f" [ messages_quote_count: {int(messages or 0)} ]" - ) + latest_episode = episodes[-1] - if changed: - previous_strength = entry.get( - "previous_strength", - ) - current_strength = entry.get( - "current_strength", - ) + if latest_episode["start_turn"] <= last_l2_turn: + continue - if ( - previous_strength is None - and current_strength is None - ): - return "" - - return ( - RUNTIME_L2_CHANGED_TRACE_SUFFIX_TEMPLATE.format( - previous_strength=( - previous_strength - if previous_strength is not None - else "?" - ), - current_strength=( - current_strength - if current_strength is not None - else "?" - ), - ) - + quote_count_suffix( - entry, - prefix="current_", - ) + candidate_messages.add( + normalized + ) + relevant_positions.update( + positions + ) + + if not candidate_keys and not candidate_messages: + return { + "keys": [], + "messages": [], + "patches": [], + } + + ordered_positions = sorted( + relevant_positions + ) + + if len(ordered_positions) > L2_MAX_EVIDENCE_PATCHES: + ordered_positions = [ + ordered_positions[0], + *ordered_positions[-( + L2_MAX_EVIDENCE_PATCHES - 1 + ):], + ] + ordered_positions = list( + dict.fromkeys( + ordered_positions ) + ) - strength = entry.get( - "strength", + patches = [ + clean_runtime_l2_evidence_patch( + history[position], + candidate_keys=candidate_keys, + candidate_messages=candidate_messages, + ) + for position in ordered_positions + ] + patches = [ + patch + for patch in patches + if ( + patch.get("user_messages") + or patch.get("changes", {}).get("added") + or patch.get("changes", {}).get("changed") ) + ] - if strength is None: - return "" + return { + "keys": sorted( + candidate_keys + ), + "messages": [ + message_examples[normalized] + for normalized in sorted( + candidate_messages + ) + if normalized in message_examples + ], + "patches": patches, + } - return RUNTIME_L2_TRACE_SUFFIX_TEMPLATE.format( - strength=strength, - ) + quote_count_suffix( - entry - ) + +def build_runtime_l2_memory_system_prompt() -> str: + + return RUNTIME_L2_MEMORY_SYSTEM_PROMPT + + +def build_runtime_l2_memory_user_prompt( + *, + current_l2_memory: str, + patches: list[dict], + recurring_keys: list[str] | None = None, + recurring_messages: list[str] | None = None, +) -> str: + + recurring_keys = list( + recurring_keys + or [] + ) + recurring_messages = list( + recurring_messages + or [] + ) lines = [ "Current L2 pattern memory:", current_l2_memory.strip() or "", "", - "Recent L1 patches since the last L2 update:", + "Recurrence gate:", + "signals: " + ( + ", ".join( + recurring_keys + ) + if recurring_keys + else "" + ), + "repeated user messages: " + ( + " | ".join( + f'"{message}"' + for message in recurring_messages + ) + if recurring_messages + else "" + ), + "", + "Selected L1 recurrence evidence:", ] for index, patch in enumerate( @@ -1349,79 +1532,46 @@ def quote_count_suffix( ): lines.extend([ "", - "Patch {index}".format( - index=index, - ), + f"Evidence {index}", f"turn: {patch.get('turn_number', 0)}", f"snapshot: {patch.get('snapshot_index', 0)}", - f"total_diff: {patch.get('total_diff', 0)}", ]) - user_messages = [ - str(message or "").replace("\n", " ").strip() - for message in (patch.get("user_messages", []) or []) - if str(message or "").strip() - ] - - if user_messages: + for message in patch.get( + "user_messages", + [], + ) or []: lines.append( - "user_messages:" + f'user_message: "{message}"' ) - for message in user_messages: - lines.append( - f'- "{message}"' - ) - changes = patch.get( "changes", {}, - ) + ) or {} - for section in ( + for entry in changes.get( "added", - "changed", - "removed", - ): - entries = ( - changes.get( - section, - [], - ) - or [] + [], + ) or []: + lines.append( + "signal added: " + f"{entry.get('key', '')}: {entry.get('value', '')}" ) - if not entries: - continue - + for entry in changes.get( + "changed", + [], + ) or []: lines.append( - f"{section}:" + "signal changed: " + f"{entry.get('key', '')}: {entry.get('value', '')}" ) - for entry in entries: - if section == "changed": - lines.append( - "- " - f"{entry.get('previous_key', '')}: {entry.get('previous_value', '')} " - "=> " - f"{entry.get('current_key', '')}: {entry.get('current_value', '')}" - + format_l2_strength_suffix( - entry, - changed=True, - ) - ) - else: - lines.append( - "- " - f"{entry.get('key', '')}: {entry.get('value', '')}" - + format_l2_strength_suffix( - entry, - ) - ) - lines.extend([ "", - "Rewrite the L2 pattern memory now.", + "Use only this evidence. Ignore ordinary live-state churn. ", + "Update L2 only if the recurrence is genuinely useful for future adaptation.", ]) return "\n".join( diff --git a/runtime/L3_memory_utils.py b/runtime/L3_memory_utils.py index 95debdda..3a8b2e8f 100644 --- a/runtime/L3_memory_utils.py +++ b/runtime/L3_memory_utils.py @@ -68,10 +68,7 @@ def build_l3_session_memory_max_tokens( return max( 128, - min( - config.SERVICE_MAX_TOKENS, - response_budget, - ), + response_budget, ) diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 446800d1..2d58ece7 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -11,12 +11,12 @@ add_l4_pending_candidates, apply_l4_jin_note_result, apply_l4_merge_operations, + build_l4_merge_batch_plan, build_l4_extraction_system_prompt, build_l4_extraction_user_prompt, build_l4_jin_note_system_prompt, build_l4_jin_note_user_prompt, build_l4_merge_system_prompt, - build_l4_merge_user_prompt, clone_l4_store, collect_pending_facts_memory_fields, delete_l4_fact_from_store, @@ -69,7 +69,7 @@ async def resolve_l4_request_limits( service_client, system_prompt: str, user_prompt: str, - requested_max_tokens: int, + requested_max_tokens: int | None, ) -> dict: context_window = 0 context_window_resolver = getattr( @@ -101,9 +101,10 @@ async def resolve_l4_request_limits( or getattr(config, "SERVICE_CONTEXT_WINDOW", 0) ) - effective_max_tokens = _positive_int( + requested_limit = _positive_int( requested_max_tokens ) + effective_max_tokens = requested_limit safe_max_tokens_resolver = getattr( service_client, "resolve_safe_max_tokens", @@ -112,20 +113,27 @@ async def resolve_l4_request_limits( if safe_max_tokens_resolver is not None: try: - effective_max_tokens = _positive_int( + resolved_safe_max_tokens = _positive_int( await safe_max_tokens_resolver( system_prompt=system_prompt, user_prompt=user_prompt, requested_max_tokens=requested_max_tokens, ) - ) or effective_max_tokens + ) + if resolved_safe_max_tokens: + effective_max_tokens = ( + min( + requested_limit, + resolved_safe_max_tokens, + ) + if requested_limit + else resolved_safe_max_tokens + ) except Exception: pass return { - "requested_max_tokens": _positive_int( - requested_max_tokens - ), + "requested_max_tokens": requested_limit, "effective_max_tokens": effective_max_tokens, "context_window_tokens": context_window, } @@ -584,7 +592,7 @@ async def ask_l4_model( label: str, system_prompt: str, user_prompt: str, - max_tokens: int, + max_tokens: int | None, ) -> dict: request_limits = await resolve_l4_request_limits( service_client=service_client, @@ -594,13 +602,14 @@ async def ask_l4_model( ) effective_max_tokens = ( request_limits.get("effective_max_tokens") - or max_tokens + or 1 ) await refresh_runtime_memory_summarizer_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, + context_window=request_limits.get("context_window_tokens") or None, ) await log_runtime_summarizer_payload( context, @@ -648,6 +657,7 @@ async def ask_l4_model( system_prompt=system_prompt, user_prompt=user_prompt, response=response, + context_window=request_limits.get("context_window_tokens") or None, ) return response @@ -706,7 +716,7 @@ async def run_l4_extraction_phase( label="L4 extraction", system_prompt=system_prompt, user_prompt=user_prompt, - max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + max_tokens=None, ) if is_runtime_memory_response_truncated(response): @@ -799,30 +809,99 @@ async def run_l4_extraction_phase( async def run_l4_merge_phase(*, context, service_client) -> dict: base_store = clone_l4_store(ensure_runtime_l4_state(context)) - pending_facts = list(base_store.get("pending_facts") or []) - if not pending_facts: + pending_queue = list(base_store.get("pending_facts") or []) + if not pending_queue: return {"phase": "merge", "status": "skipped", "reason": "no_pending_long_term_facts"} system_prompt = build_l4_merge_system_prompt() - user_prompt = build_l4_merge_user_prompt( + runtime_context_window = 0 + context_window_resolver = getattr( + service_client, + "resolve_request_context_window", + None, + ) + if context_window_resolver is not None: + try: + runtime_context_window = _positive_int( + await context_window_resolver( + force_refresh=True, + ) + ) + except TypeError: + # Compatibility with lightweight test doubles / older clients. + runtime_context_window = _positive_int( + await context_window_resolver() + ) + except Exception: + runtime_context_window = 0 + + if not runtime_context_window: + runtime_context_window = _positive_int( + getattr(service_client, "configured_context_window", 0) + ) or _positive_int( + getattr(config, "SERVICE_CONTEXT_WINDOW", 0) + ) or 4096 + + runtime_output_reserve = _positive_int( + getattr(config, "RUNTIME_OUTPUT_TOKEN_RESERVE", 256) + ) + + batch_plan = build_l4_merge_batch_plan( existing_facts=base_store.get("facts") or [], - pending_facts=pending_facts, + pending_facts=pending_queue, + system_prompt=system_prompt, + runtime_context_window=runtime_context_window, + requested_max_tokens=None, + runtime_output_reserve=runtime_output_reserve, ) + pending_facts = batch_plan["pending_facts"] + if not pending_facts: + first_pending_id = "" + if pending_queue: + first_pending_id = str( + pending_queue[0].get("id") or "" + ).strip() + return await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason="runtime_context_budget_exhausted", + details={ + "pending_count": len(pending_queue), + "first_pending_id": first_pending_id, + "runtime_context_window_tokens": batch_plan[ + "runtime_context_window_tokens" + ], + "estimated_prompt_tokens": batch_plan[ + "estimated_prompt_tokens" + ], + "estimated_response_tokens": batch_plan[ + "estimated_response_tokens" + ], + "runtime_output_reserve_tokens": batch_plan[ + "runtime_output_reserve_tokens" + ], + "response_headroom_tokens": batch_plan[ + "response_headroom_tokens" + ], + "estimated_total_tokens": batch_plan[ + "estimated_total_tokens" + ], + }, + ) + + user_prompt = batch_plan["user_prompt"] + pending_ids = batch_plan["pending_ids"] response = await ask_l4_model( context=context, service_client=service_client, label="L4 merge", system_prompt=system_prompt, user_prompt=user_prompt, - max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + max_tokens=batch_plan["requested_max_output_tokens"], ) if is_runtime_memory_response_truncated(response): - pending_ids = [ - fact.get("id") - for fact in pending_facts - if fact.get("id") - ] return await log_l4_skip_event( context, phase="merge", @@ -850,10 +929,9 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: reason="invalid_json", details={ "pending_count": len(pending_facts), - "pending_ids": [ - fact.get("id") - for fact in pending_facts - if fact.get("id") + "pending_ids": pending_ids, + "remaining_pending_count": batch_plan[ + "remaining_pending_count" ], }, ) @@ -869,16 +947,16 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: "base_revision": base_store.get("revision"), "current_revision": current_store.get("revision"), "pending_count": len(pending_facts), - "pending_ids": [ - fact.get("id") - for fact in pending_facts - if fact.get("id") - ], + "pending_ids": pending_ids, }, ) operations = normalize_l4_merge_operations(payload) - next_store, merge_change = apply_l4_merge_operations(base_store, operations) + next_store, merge_change = apply_l4_merge_operations( + base_store, + operations, + pending_ids=pending_ids, + ) if not merge_change.get("valid"): return await log_l4_skip_event( context, @@ -888,10 +966,9 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: details={ "pending_count": len(pending_facts), "operations_count": len(operations), - "pending_ids": [ - fact.get("id") - for fact in pending_facts - if fact.get("id") + "pending_ids": pending_ids, + "remaining_pending_count": batch_plan[ + "remaining_pending_count" ], }, ) @@ -919,6 +996,8 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: "phase": "merge", "status": "completed", "operations_count": len(operations), + "batch_count": len(pending_facts), + "remaining_pending_count": merge_change.get("pending_count", 0), "merge_change": merge_change, } @@ -981,7 +1060,7 @@ async def run_l4_jin_note( label="L4 JIN note", system_prompt=system_prompt, user_prompt=user_prompt, - max_tokens=int(getattr(config, "SERVICE_MAX_TOKENS", 4096) or 4096), + max_tokens=None, ) if is_runtime_memory_response_truncated(response): @@ -1226,7 +1305,7 @@ def l4_fact_matches_archived_ids( str( candidate_id or "" - ).strip().casefold() + ).strip().upper() in archived_fact_ids for candidate_id in candidate_ids ) diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py index f64b7471..394f5111 100644 --- a/runtime/L4_memory_rules.py +++ b/runtime/L4_memory_rules.py @@ -64,9 +64,19 @@ You consolidate pending candidates into JIN's current cross-session long-term memory. +The pending candidates in this request may be only one batch from a larger +queue. Work only with the pending_id values present in this request and return +exactly one operation for each of them. + Both existing facts and pending candidates are provisional. Existing memory is not evidence merely because it already exists. +ID convention: +- F is a committed L4 fact (for example F1, F27, F255). +- PF is a pending L4 fact (for example PF1, PF8, PF42). +- Copy these short IDs exactly from the input. Never invent, transform, expand, + hash, or reinterpret an ID. + Keep memory minimal, accurate, atomic, useful, and free of semantic duplicates. Compare candidates against all existing facts by meaning, not only by key, wording, category, visibility, or report coverage. @@ -116,8 +126,8 @@ "operations": [ { "action": "reinforce", - "pending_id": "l4p_...", - "target_id": "l4_..." + "pending_id": "PF3", + "target_id": "F12" } ] } @@ -135,6 +145,8 @@ The input contains: - the complete current L4 fact list; - selected_fact_ids: the only facts in scope for possible replacement; +- every selected fact ID uses F; F means committed fact, while PF means + pending fact and is never valid in selected_fact_ids; - message: a concise clarification of what became clear in the conversation. The note is a trusted clarification signal, but it is not an edit command. Decide diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 9291df2e..db0b1f69 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -12,11 +12,19 @@ L4_MERGE_SYSTEM_PROMPT, L4_JIN_NOTE_SYSTEM_PROMPT, ) +from utils.tokens import ( + estimate_runtime_tokens, + estimate_tokens, +) -L4_STORE_VERSION = 1 -L4_FACT_ID_PREFIX = "l4_" -L4_PENDING_FACT_ID_PREFIX = "l4p_" +L4_STORE_VERSION = 2 +L4_FACT_ID_PREFIX = "F" +L4_PENDING_FACT_ID_PREFIX = "PF" +L4_FACT_ID_RE = re.compile(r"^F([1-9]\d*)$", re.IGNORECASE) +L4_PENDING_FACT_ID_RE = re.compile(r"^PF([1-9]\d*)$", re.IGNORECASE) +L4_LEGACY_FACT_ID_RE = re.compile(r"^l4_[a-z0-9_-]+$", re.IGNORECASE) +L4_LEGACY_PENDING_FACT_ID_RE = re.compile(r"^l4p_[a-z0-9_-]+$", re.IGNORECASE) L4_FIELD_STATUS_PENDING = "pending" L4_FIELD_STATUS_ANALYZED = "analyzed" L4_ALLOWED_CATEGORIES = { @@ -97,21 +105,219 @@ def build_l4_merge_user_prompt( existing_facts: list[dict], pending_facts: list[dict], ) -> str: + model_existing_facts = [ + build_l4_merge_model_fact(fact) + for fact in existing_facts + if isinstance(fact, dict) + ] + model_pending_facts = [ + build_l4_merge_model_fact(fact) + for fact in pending_facts + if isinstance(fact, dict) + ] + return ( - "Consolidate the entire pending batch into the current long-term " - "memory. Return exactly one operation for every pending_id.\n\n" + "Consolidate this pending batch into the current long-term memory. " + "Return exactly one operation for every pending_id in this request.\n\n" + json.dumps( { - "existing_facts": existing_facts, - "pending_facts": pending_facts, + "existing_facts": model_existing_facts, + "pending_facts": model_pending_facts, }, ensure_ascii=False, - indent=2, + separators=(",", ":"), sort_keys=True, ) ) +def build_l4_merge_model_fact(fact: dict) -> dict: + return { + "id": normalize_l4_text(fact.get("id")), + "key": normalize_l4_text(fact.get("key")), + "value": normalize_l4_text(fact.get("value")), + "category": normalize_l4_text(fact.get("category")), + } + + +def estimate_l4_merge_response_tokens( + pending_facts: list[dict], +) -> int: + """Estimate a conservative full operation payload for a merge batch.""" + + operations = [] + for fact in pending_facts: + if not isinstance(fact, dict): + continue + + model_fact = build_l4_merge_model_fact(fact) + operations.append({ + "action": "update", + "pending_id": model_fact["id"], + "target_id": "F1", + "key": model_fact["key"], + "value": model_fact["value"], + "category": model_fact["category"], + }) + + return estimate_tokens( + json.dumps( + {"operations": operations}, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + ) + + +def build_l4_merge_batch_plan( + *, + existing_facts: list[dict], + pending_facts: list[dict], + system_prompt: str, + runtime_context_window: int, + requested_max_tokens: int | None, + runtime_output_reserve: int = 256, +) -> dict: + """Select the largest FIFO pending slice that fits the live LM Studio budget. + + The configured SERVICE_CONTEXT_WINDOW is intentionally not a request limit; + it remains a UI/reference denominator. L4 batching uses the context_length + of the model instance actually loaded in LM Studio. + """ + + try: + context_window = max(1, int(runtime_context_window)) + except (TypeError, ValueError): + context_window = 1 + + try: + max_requested_output = max(1, int(requested_max_tokens)) + except (TypeError, ValueError): + max_requested_output = context_window + + try: + provider_reserve = max(0, int(runtime_output_reserve)) + except (TypeError, ValueError): + provider_reserve = 0 + + # Leave a small model-side cushion for reasoning / JSON variation in + # addition to the generic provider reserve. Scale it with the live window + # so batching remains proportional to the actual LM Studio context. + response_headroom = max( + 128, + min( + 1024, + context_window // 16, + ), + ) + + queue = [ + fact + for fact in pending_facts + if isinstance(fact, dict) + ] + selected = [] + selected_prompt = "" + selected_prompt_tokens = 0 + selected_response_tokens = 0 + estimated_total_tokens = 0 + + for fact in queue: + candidate_batch = [*selected, fact] + candidate_prompt = build_l4_merge_user_prompt( + existing_facts=existing_facts, + pending_facts=candidate_batch, + ) + prompt_tokens = estimate_runtime_tokens( + system_prompt=system_prompt, + user_input=candidate_prompt, + ) + response_tokens = estimate_l4_merge_response_tokens( + candidate_batch + ) + total_tokens = ( + prompt_tokens + + response_tokens + + provider_reserve + + response_headroom + ) + + if total_tokens > context_window: + break + + selected = candidate_batch + selected_prompt = candidate_prompt + selected_prompt_tokens = prompt_tokens + selected_response_tokens = response_tokens + estimated_total_tokens = total_tokens + + pending_ids = [ + normalize_l4_text(fact.get("id")) + for fact in selected + if normalize_l4_text(fact.get("id")) + ] + + if selected: + configured_output_room = max( + 1, + context_window + - selected_prompt_tokens + - provider_reserve, + ) + else: + # Calculate diagnostics for the first FIFO item so a failed budget is + # explainable instead of silently retrying the same oversized payload. + first_prompt = "" + first_prompt_tokens = 0 + first_response_tokens = 0 + first_total_tokens = 0 + if queue: + first_prompt = build_l4_merge_user_prompt( + existing_facts=existing_facts, + pending_facts=[queue[0]], + ) + first_prompt_tokens = estimate_runtime_tokens( + system_prompt=system_prompt, + user_input=first_prompt, + ) + first_response_tokens = estimate_l4_merge_response_tokens( + [queue[0]] + ) + first_total_tokens = ( + first_prompt_tokens + + first_response_tokens + + provider_reserve + + response_headroom + ) + + selected_prompt = first_prompt + selected_prompt_tokens = first_prompt_tokens + selected_response_tokens = first_response_tokens + estimated_total_tokens = first_total_tokens + configured_output_room = 0 + + return { + "pending_facts": selected, + "pending_ids": pending_ids, + "batch_count": len(selected), + "total_pending_count": len(queue), + "remaining_pending_count": max(0, len(queue) - len(selected)), + "user_prompt": selected_prompt, + "runtime_context_window_tokens": context_window, + # Kept as a diagnostics compatibility alias for older log viewers. + "configured_context_window_tokens": context_window, + "estimated_prompt_tokens": selected_prompt_tokens, + "estimated_response_tokens": selected_response_tokens, + "runtime_output_reserve_tokens": provider_reserve, + "response_headroom_tokens": response_headroom, + "estimated_total_tokens": estimated_total_tokens, + "configured_output_room_tokens": configured_output_room, + "requested_max_output_tokens": max_requested_output, + "fits": bool(selected), + } + + def utc_now_iso() -> str: return ( datetime.now(timezone.utc) @@ -167,12 +373,44 @@ def merge_l4_string_lists(*values) -> list[str]: return result +def normalize_l4_id(value, *, pending: bool = False) -> str: + text = normalize_l4_text(value).upper() + matcher = L4_PENDING_FACT_ID_RE if pending else L4_FACT_ID_RE + match = matcher.fullmatch(text) + if not match: + return "" + return f"{L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX}{int(match.group(1))}" + + +def is_l4_fact_id(value) -> bool: + return bool(normalize_l4_id(value, pending=False)) + + +def is_l4_pending_fact_id(value) -> bool: + return bool(normalize_l4_id(value, pending=True)) + + +def _l4_id_number(value, *, pending: bool = False) -> int: + normalized = normalize_l4_id(value, pending=pending) + if not normalized: + return 0 + prefix = L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX + try: + return int(normalized[len(prefix):]) + except (TypeError, ValueError): + return 0 + + def normalize_l4_deleted_fact_ids(value) -> list[str]: - return [ - fact_id - for fact_id in normalize_l4_string_list(value) - if fact_id.startswith(L4_FACT_ID_PREFIX) - ] + result = [] + seen = set() + for candidate in normalize_l4_string_list(value): + fact_id = normalize_l4_id(candidate, pending=False) + if not fact_id or fact_id in seen: + continue + seen.add(fact_id) + result.append(fact_id) + return result def build_l4_content_hash(content: str) -> str: @@ -181,10 +419,49 @@ def build_l4_content_hash(content: str) -> str: ).hexdigest()[:16] -def build_l4_fact_id(*, key: str, value: str, pending: bool = False) -> str: +def build_l4_fact_id(*, sequence: int, pending: bool = False, **_ignored) -> str: + try: + number = int(sequence) + except (TypeError, ValueError) as error: + raise ValueError("L4 fact sequence must be a positive integer") from error + if number <= 0: + raise ValueError("L4 fact sequence must be a positive integer") prefix = L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX - digest = hashlib.sha256(f"{key}\0{value}".encode("utf-8")).hexdigest()[:12] - return f"{prefix}{digest}" + return f"{prefix}{number}" + + +def _next_l4_sequence(store: dict, *, pending: bool = False) -> int: + counter_key = "next_pending_fact_id" if pending else "next_fact_id" + try: + configured = max(1, int(store.get(counter_key) or 1)) + except (TypeError, ValueError): + configured = 1 + + ids = [] + if pending: + ids.extend(fact.get("id") for fact in store.get("pending_facts", []) if isinstance(fact, dict)) + for fact in store.get("facts", []): + if isinstance(fact, dict): + ids.extend(fact.get("source_fact_ids") or []) + else: + ids.extend(fact.get("id") for fact in store.get("facts", []) if isinstance(fact, dict)) + ids.extend(store.get("deleted_fact_ids") or []) + for fact in store.get("facts", []): + if isinstance(fact, dict): + ids.extend(fact.get("source_fact_ids") or []) + + highest = max( + (_l4_id_number(value, pending=pending) for value in ids), + default=0, + ) + return max(configured, highest + 1) + + +def allocate_l4_fact_id(store: dict, *, pending: bool = False) -> str: + sequence = _next_l4_sequence(store, pending=pending) + counter_key = "next_pending_fact_id" if pending else "next_fact_id" + store[counter_key] = sequence + 1 + return build_l4_fact_id(sequence=sequence, pending=pending) def normalize_l4_fact( @@ -202,10 +479,7 @@ def normalize_l4_fact( return None current_time = now or utc_now_iso() - expected_prefix = L4_PENDING_FACT_ID_PREFIX if pending else L4_FACT_ID_PREFIX - fact_id = normalize_l4_text(value.get("id")) - if not fact_id.startswith(expected_prefix): - fact_id = build_l4_fact_id(key=key, value=fact_value, pending=pending) + fact_id = normalize_l4_id(value.get("id"), pending=pending) try: mention_count = max(1, int(value.get("mention_count") or 1)) @@ -267,16 +541,22 @@ def merge_same_l4_fact(existing: dict, incoming: dict, *, now: str) -> dict: def deduplicate_l4_facts(facts: list[dict], *, pending: bool, now: str) -> list[dict]: result = [] - by_id = {} + by_identity = {} for raw_fact in facts: fact = normalize_l4_fact(raw_fact, pending=pending, now=now) if fact is None: continue - existing_index = by_id.get(fact["id"]) + identity = fact.get("id") or ( + "semantic", + fact.get("key"), + fact.get("value"), + fact.get("category"), + ) + existing_index = by_identity.get(identity) if existing_index is None: - by_id[fact["id"]] = len(result) + by_identity[identity] = len(result) result.append(fact) continue @@ -479,8 +759,9 @@ def collect_l4_processed_pending_fact_ids(facts) -> set[str]: for source_fact_id in normalize_l4_string_list( fact.get("source_fact_ids") ): - if source_fact_id.startswith(L4_PENDING_FACT_ID_PREFIX): - processed_ids.add(source_fact_id) + pending_id = normalize_l4_id(source_fact_id, pending=True) + if pending_id: + processed_ids.add(pending_id) return processed_ids @@ -548,10 +829,20 @@ def merge_l4_store_snapshots( facts=facts, pending_facts=pending_facts, ) + next_fact_id = max( + int(primary.get("next_fact_id") or 1), + int(incoming.get("next_fact_id") or 1), + ) + next_pending_fact_id = max( + int(primary.get("next_pending_fact_id") or 1), + int(incoming.get("next_pending_fact_id") or 1), + ) changed = bool( facts != primary.get("facts") or pending_facts != primary.get("pending_facts") or deleted_fact_ids != primary.get("deleted_fact_ids") + or next_fact_id != int(primary.get("next_fact_id") or 1) + or next_pending_fact_id != int(primary.get("next_pending_fact_id") or 1) ) merged = { @@ -559,6 +850,8 @@ def merge_l4_store_snapshots( "facts": facts, "pending_facts": pending_facts, "deleted_fact_ids": deleted_fact_ids, + "next_fact_id": next_fact_id, + "next_pending_fact_id": next_pending_fact_id, } if changed: @@ -576,6 +869,177 @@ def merge_l4_store_snapshots( } +def migrate_l4_store_ids( + value, + *, + now: str | None = None, +) -> tuple[dict, dict[str, str]]: + """Upgrade legacy hash-like L4 ids to compact sequential F/PF ids. + + Existing F/PF ids are preserved. Legacy ids are remapped deterministically + in store order so browser and backend snapshots converge on the same ids. + """ + + current_time = now or utc_now_iso() + if not isinstance(value, dict): + return empty_l4_store(now=current_time), {} + + migrated = deepcopy(value) + facts = migrated.get("facts") if isinstance(migrated.get("facts"), list) else [] + pending = ( + migrated.get("pending_facts") + if isinstance(migrated.get("pending_facts"), list) + else [] + ) + deleted = ( + migrated.get("deleted_fact_ids") + if isinstance(migrated.get("deleted_fact_ids"), list) + else [] + ) + + used_fact_numbers = { + _l4_id_number(fact.get("id"), pending=False) + for fact in facts + if isinstance(fact, dict) and is_l4_fact_id(fact.get("id")) + } + used_pending_numbers = { + _l4_id_number(fact.get("id"), pending=True) + for fact in pending + if isinstance(fact, dict) and is_l4_pending_fact_id(fact.get("id")) + } + used_fact_numbers.discard(0) + used_pending_numbers.discard(0) + + try: + next_fact = max(1, int(migrated.get("next_fact_id") or 1)) + except (TypeError, ValueError): + next_fact = 1 + try: + next_pending = max(1, int(migrated.get("next_pending_fact_id") or 1)) + except (TypeError, ValueError): + next_pending = 1 + + next_fact = max(next_fact, max(used_fact_numbers, default=0) + 1) + next_pending = max(next_pending, max(used_pending_numbers, default=0) + 1) + id_map: dict[str, str] = {} + + def allocate(raw_id, *, is_pending: bool) -> str: + nonlocal next_fact, next_pending + text = normalize_l4_text(raw_id) + normalized = normalize_l4_id(text, pending=is_pending) + if normalized: + return normalized + if text and text in id_map: + return id_map[text] + + legacy_match = ( + L4_LEGACY_PENDING_FACT_ID_RE.fullmatch(text) + if is_pending + else L4_LEGACY_FACT_ID_RE.fullmatch(text) + ) + if text and not legacy_match: + return "" + + if is_pending: + while next_pending in used_pending_numbers: + next_pending += 1 + assigned = build_l4_fact_id(sequence=next_pending, pending=True) + used_pending_numbers.add(next_pending) + next_pending += 1 + else: + while next_fact in used_fact_numbers: + next_fact += 1 + assigned = build_l4_fact_id(sequence=next_fact, pending=False) + used_fact_numbers.add(next_fact) + next_fact += 1 + + if text: + id_map[text] = assigned + return assigned + + # Assign actual records first. This makes the migration deterministic and + # ensures references to those records reuse the same new id. + for fact in facts: + if not isinstance(fact, dict): + continue + old_id = normalize_l4_text(fact.get("id")) + new_id = allocate(old_id, is_pending=False) + if not new_id: + new_id = allocate("", is_pending=False) + if old_id and old_id != new_id: + id_map[old_id] = new_id + fact["id"] = new_id + + for fact in pending: + if not isinstance(fact, dict): + continue + old_id = normalize_l4_text(fact.get("id")) + new_id = allocate(old_id, is_pending=True) + if not new_id: + new_id = allocate("", is_pending=True) + if old_id and old_id != new_id: + id_map[old_id] = new_id + fact["id"] = new_id + + migrated_deleted = [] + for raw_id in deleted: + old_id = normalize_l4_text(raw_id) + new_id = normalize_l4_id(old_id, pending=False) or id_map.get(old_id, "") + if not new_id and L4_LEGACY_FACT_ID_RE.fullmatch(old_id): + new_id = allocate(old_id, is_pending=False) + if new_id and new_id not in migrated_deleted: + migrated_deleted.append(new_id) + + for fact in [*facts, *pending]: + if not isinstance(fact, dict): + continue + remapped_sources = [] + for raw_id in normalize_l4_string_list(fact.get("source_fact_ids")): + new_id = normalize_l4_id(raw_id, pending=True) + if not new_id: + new_id = normalize_l4_id(raw_id, pending=False) + if not new_id: + new_id = id_map.get(raw_id, "") + if not new_id and L4_LEGACY_PENDING_FACT_ID_RE.fullmatch(raw_id): + new_id = allocate(raw_id, is_pending=True) + if not new_id and L4_LEGACY_FACT_ID_RE.fullmatch(raw_id): + new_id = allocate(raw_id, is_pending=False) + if new_id and new_id not in remapped_sources: + remapped_sources.append(new_id) + fact["source_fact_ids"] = remapped_sources + + try: + previous_version = int(value.get("version") or 0) + except (TypeError, ValueError): + previous_version = 0 + + should_bump_revision = bool( + id_map + or migrated_deleted != deleted + or ( + previous_version not in {0, L4_STORE_VERSION} + and bool(facts or pending or deleted) + ) + ) + + migrated.update({ + "version": L4_STORE_VERSION, + "facts": facts, + "pending_facts": pending, + "deleted_fact_ids": migrated_deleted, + "next_fact_id": next_fact, + "next_pending_fact_id": next_pending, + }) + if should_bump_revision: + try: + migrated["revision"] = max(0, int(value.get("revision") or 0)) + 1 + except (TypeError, ValueError): + migrated["revision"] = 1 + migrated["updated_at"] = current_time + + return migrated, id_map + + def empty_l4_store(*, now: str | None = None) -> dict: return { "version": L4_STORE_VERSION, @@ -584,13 +1048,17 @@ def empty_l4_store(*, now: str | None = None) -> dict: "facts": [], "pending_facts": [], "deleted_fact_ids": [], + "next_fact_id": 1, + "next_pending_fact_id": 1, } def normalize_l4_store(value, *, now: str | None = None) -> dict: current_time = now or utc_now_iso() if not isinstance(value, dict): - return empty_l4_store() + return empty_l4_store(now=current_time) + + value, _id_map = migrate_l4_store_ids(value, now=current_time) try: revision = max(0, int(value.get("revision") or 0)) @@ -626,6 +1094,25 @@ def normalize_l4_store(value, *, now: str | None = None) -> dict: ), ) + next_fact_id = max( + _next_l4_sequence({ + **value, + "facts": facts, + "pending_facts": pending_facts, + "deleted_fact_ids": deleted_fact_ids, + }, pending=False), + 1, + ) + next_pending_fact_id = max( + _next_l4_sequence({ + **value, + "facts": facts, + "pending_facts": pending_facts, + "deleted_fact_ids": deleted_fact_ids, + }, pending=True), + 1, + ) + return { "version": L4_STORE_VERSION, "revision": revision, @@ -633,6 +1120,8 @@ def normalize_l4_store(value, *, now: str | None = None) -> dict: "facts": facts, "pending_facts": pending_facts, "deleted_fact_ids": deleted_fact_ids, + "next_fact_id": next_fact_id, + "next_pending_fact_id": next_pending_fact_id, } @@ -870,6 +1359,10 @@ def add_l4_pending_candidates( next_store = normalize_l4_store(store, now=current_time) pending = list(next_store["pending_facts"]) by_id = {fact["id"]: index for index, fact in enumerate(pending)} + by_semantic = { + (fact.get("key"), fact.get("value"), fact.get("category")): index + for index, fact in enumerate(pending) + } added_ids = [] reinforced_ids = [] @@ -878,12 +1371,19 @@ def add_l4_pending_candidates( if candidate is None: continue - index = by_id.get(candidate["id"]) + identity = (candidate.get("key"), candidate.get("value"), candidate.get("category")) + index = by_id.get(candidate.get("id")) if candidate.get("id") else None + if index is None: + index = by_semantic.get(identity) + if index is None: + candidate["id"] = allocate_l4_fact_id(next_store, pending=True) by_id[candidate["id"]] = len(pending) + by_semantic[identity] = len(pending) pending.append(candidate) added_ids.append(candidate["id"]) else: + candidate["id"] = pending[index]["id"] pending[index] = merge_same_l4_fact( pending[index], candidate, @@ -915,7 +1415,7 @@ def normalize_l4_merge_operations(payload) -> list[dict]: continue action = normalize_l4_key(raw_operation.get("action")) - pending_id = normalize_l4_text(raw_operation.get("pending_id")) + pending_id = normalize_l4_id(raw_operation.get("pending_id"), pending=True) if action not in L4_ALLOWED_MERGE_ACTIONS or not pending_id: continue @@ -923,7 +1423,7 @@ def normalize_l4_merge_operations(payload) -> list[dict]: "action": action, "pending_id": pending_id, } - target_id = normalize_l4_text(raw_operation.get("target_id")) + target_id = normalize_l4_id(raw_operation.get("target_id"), pending=False) if target_id: operation["target_id"] = target_id @@ -936,19 +1436,35 @@ def normalize_l4_merge_operations(payload) -> list[dict]: return operations -def validate_l4_merge_operations(store, operations: list[dict]) -> tuple[bool, str]: +def validate_l4_merge_operations( + store, + operations: list[dict], + *, + pending_ids: list[str] | None = None, +) -> tuple[bool, str]: normalized_store = normalize_l4_store(store) pending = normalized_store["pending_facts"] - pending_ids = {fact["id"] for fact in pending} + all_pending_ids = {fact["id"] for fact in pending} fact_ids = {fact["id"] for fact in normalized_store["facts"]} - if len(operations) != len(pending): + if pending_ids is None: + expected_pending_ids = all_pending_ids + else: + expected_pending_ids = { + normalize_l4_id(pending_id, pending=True) + for pending_id in pending_ids + if normalize_l4_id(pending_id, pending=True) + } + if not expected_pending_ids.issubset(all_pending_ids): + return False, "unknown_pending_batch_id" + + if len(operations) != len(expected_pending_ids): return False, "operation_count_mismatch" seen = set() for operation in operations: pending_id = operation.get("pending_id") - if pending_id not in pending_ids: + if pending_id not in expected_pending_ids: return False, "unknown_pending_id" if pending_id in seen: return False, "duplicate_pending_operation" @@ -959,7 +1475,7 @@ def validate_l4_merge_operations(store, operations: list[dict]) -> tuple[bool, s if operation.get("target_id") not in fact_ids: return False, "unknown_target_id" - if seen != pending_ids: + if seen != expected_pending_ids: return False, "missing_pending_operation" return True, "" @@ -1033,11 +1549,16 @@ def apply_l4_merge_operations( store, operations: list[dict], *, + pending_ids: list[str] | None = None, now: str | None = None, ) -> tuple[dict, dict]: current_time = now or utc_now_iso() base_store = normalize_l4_store(store, now=current_time) - valid, reason = validate_l4_merge_operations(base_store, operations) + valid, reason = validate_l4_merge_operations( + base_store, + operations, + pending_ids=pending_ids, + ) if not valid: return base_store, { "valid": False, @@ -1047,7 +1568,23 @@ def apply_l4_merge_operations( facts = [dict(fact) for fact in base_store["facts"]] facts_by_id = {fact["id"]: index for index, fact in enumerate(facts)} - pending_by_id = {fact["id"]: fact for fact in base_store["pending_facts"]} + all_pending_by_id = { + fact["id"]: fact + for fact in base_store["pending_facts"] + } + processed_pending_ids = ( + { + normalize_l4_id(pending_id, pending=True) + for pending_id in pending_ids + if normalize_l4_id(pending_id, pending=True) + } + if pending_ids is not None + else set(all_pending_by_id) + ) + pending_by_id = { + pending_id: all_pending_by_id[pending_id] + for pending_id in processed_pending_ids + } added_ids = [] updated_ids = [] reinforced_ids = [] @@ -1128,6 +1665,7 @@ def apply_l4_merge_operations( ) continue + new_fact_id = allocate_l4_fact_id(base_store, pending=False) candidate = normalize_l4_fact( { **pending, @@ -1136,9 +1674,13 @@ def apply_l4_merge_operations( for key in ("key", "value", "category") if key in operation }, - "id": "", + "id": new_fact_id, "created_at": current_time, "updated_at": current_time, + "source_fact_ids": merge_l4_string_lists( + pending.get("source_fact_ids"), + [pending.get("id")], + ), }, now=current_time, ) @@ -1170,7 +1712,11 @@ def apply_l4_merge_operations( next_store = { **base_store, "facts": facts, - "pending_facts": [], + "pending_facts": [ + fact + for fact in base_store["pending_facts"] + if fact["id"] not in processed_pending_ids + ], "revision": base_store["revision"] + 1, "updated_at": current_time, } @@ -1181,10 +1727,10 @@ def apply_l4_merge_operations( "updated_ids": updated_ids, "reinforced_ids": reinforced_ids, "ignored_pending_ids": ignored_ids, - "processed_pending_ids": sorted(pending_by_id), + "processed_pending_ids": sorted(processed_pending_ids), "operation_details": operation_details, "total_facts": len(facts), - "pending_count": 0, + "pending_count": len(next_store["pending_facts"]), "changed": True, } @@ -1285,11 +1831,12 @@ def apply_l4_jin_note_result( replacement_facts = [] replacement_keys = set() + allocation_store = {**base_store, "facts": [*unselected_facts]} for raw_fact in normalized_result["replacement_facts"]: replacement = normalize_l4_fact( { **raw_fact, - "id": "", + "id": allocate_l4_fact_id(allocation_store, pending=False), "created_at": current_time, "updated_at": current_time, }, @@ -1345,6 +1892,7 @@ def apply_l4_jin_note_result( next_store = { **base_store, "facts": [*unselected_facts, *replacement_facts], + "next_fact_id": allocation_store.get("next_fact_id", base_store.get("next_fact_id", 1)), "deleted_fact_ids": merge_l4_string_lists( base_store.get("deleted_fact_ids"), selected_ids, @@ -1411,7 +1959,9 @@ def delete_l4_fact_from_store( ) -> tuple[dict, bool]: current_time = now or utc_now_iso() next_store = normalize_l4_store(store, now=current_time) - target_id = normalize_l4_text(fact_id) + target_id = normalize_l4_id(fact_id, pending=False) + if not target_id: + return next_store, False remaining = [fact for fact in next_store["facts"] if fact.get("id") != target_id] if len(remaining) == len(next_store["facts"]): return next_store, False diff --git a/runtime/client.py b/runtime/client.py index 029aebc5..0ec35916 100644 --- a/runtime/client.py +++ b/runtime/client.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import time import httpx @@ -19,6 +20,9 @@ logger = logging.getLogger(__name__) +MODEL_LIMITS_CACHE_TTL_SECONDS = 2.0 +LEGACY_NATIVE_MODELS_ENDPOINT = "/api/v0/models" + class LMStudioAPIError(RuntimeError): @@ -343,6 +347,7 @@ def __init__( self.detected_context_window = None self.detected_max_tokens = None self.model_limits_detection_attempted = False + self.model_limits_detected_at = 0.0 # --------------------------------------------------------- # MODEL LIMIT DETECTION @@ -585,6 +590,9 @@ def select_model_metadata( model.get( "id" ) + or model.get( + "key" + ) or model.get( "model" ) @@ -613,40 +621,97 @@ def select_model_metadata( def model_limits_detection_endpoints(self) -> list[str]: - endpoints = [ - settings.MODELS_ENDPOINT, - ] + endpoints = [] native_endpoint = getattr( settings, "NATIVE_MODELS_ENDPOINT", "", ) - if native_endpoint and native_endpoint not in endpoints: - endpoints.append( - native_endpoint - ) + # LM Studio's native v1 metadata exposes the context_length of the + # actually loaded instance. Prefer it over OpenAI-compatible metadata. + for endpoint in ( + native_endpoint, + settings.MODELS_ENDPOINT, + LEGACY_NATIVE_MODELS_ENDPOINT, + ): + endpoint = str(endpoint or "").strip() + if endpoint and endpoint not in endpoints: + endpoints.append(endpoint) return endpoints + def select_loaded_model_metadata( + self, + model: dict, + ) -> dict | None: + + loaded_instances = model.get( + "loaded_instances" + ) + + if not isinstance(loaded_instances, list): + return model + + instances = [ + instance + for instance in loaded_instances + if isinstance(instance, dict) + ] + if not instances: + # Native v1 can list an available but unloaded model together with + # its theoretical max_context_length. Never use that as the live + # request budget. + return None + + for instance in instances: + instance_id = str( + instance.get("id") + or instance.get("key") + or instance.get("model") + or "" + ) + if ( + instance_id == self.model_uid + or self.model_uid in instance_id + or instance_id in self.model_uid + ): + return instance + + return instances[0] + async def detect_model_limits( self, *, force_refresh: bool = False, ) -> tuple[int | None, int | None]: + now = time.monotonic() + cache_is_fresh = ( + self.model_limits_detection_attempted + and self.model_limits_detected_at > 0 + and ( + now - self.model_limits_detected_at + < MODEL_LIMITS_CACHE_TTL_SECONDS + ) + ) + if force_refresh: + cache_is_fresh = False self.model_limits_detection_attempted = False self.detected_context_window = None self.detected_max_tokens = None - if self.model_limits_detection_attempted: + if cache_is_fresh: return ( self.detected_context_window, self.detected_max_tokens, ) self.model_limits_detection_attempted = True + self.model_limits_detected_at = now + self.detected_context_window = None + self.detected_max_tokens = None for endpoint in self.model_limits_detection_endpoints(): @@ -673,15 +738,21 @@ async def detect_model_limits( if model is None: continue + live_model = self.select_loaded_model_metadata( + model + ) + if live_model is None: + continue + context_window = ( self.extract_context_window_from_model( - model + live_model ) ) max_tokens = ( self.extract_max_tokens_from_model( - model + live_model ) ) @@ -700,8 +771,6 @@ async def detect_model_limits( except Exception: continue - self.detected_context_window = None - self.detected_max_tokens = None return ( self.detected_context_window, self.detected_max_tokens, @@ -729,13 +798,13 @@ async def resolve_request_context_window( force_refresh: bool = False, ) -> int | None: - if not settings.RUNTIME_CONTEXT_WINDOW_FALLBACK_TO_SERVER: - return self.configured_context_window - detected_context_window = await self.detect_context_window( force_refresh=force_refresh, ) + # *_CONTEXT_WINDOW is intentionally only the UI/reference denominator. + # It is used for requests only as an emergency fallback when LM Studio + # metadata cannot be read at all. return ( detected_context_window or self.configured_context_window @@ -743,45 +812,62 @@ async def resolve_request_context_window( async def resolve_request_max_tokens( self, - requested_max_tokens: int, + requested_max_tokens: int | None, + *, + force_refresh: bool = False, ) -> int: - if not settings.RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER: - return requested_max_tokens - - if ( - self.configured_max_tokens is not None - and requested_max_tokens != self.configured_max_tokens - ): - return requested_max_tokens - - detected_max_tokens = await self.detect_max_tokens() + try: + explicit_limit = int(requested_max_tokens) + except (TypeError, ValueError): + explicit_limit = 0 - if detected_max_tokens: - return detected_max_tokens + detected_context_window, detected_max_tokens = ( + await self.detect_model_limits( + force_refresh=force_refresh, + ) + ) - if ( - self.detected_context_window - and self.configured_max_tokens is not None - and requested_max_tokens == self.configured_max_tokens - ): - return self.detected_context_window + if explicit_limit > 0: + # Specialized calls (translation, L3, document result caps, etc.) + # keep their smaller cap, while an explicit provider output ceiling + # still wins if LM Studio reports one. + if detected_max_tokens: + return min( + explicit_limit, + int(detected_max_tokens), + ) + return explicit_limit - return requested_max_tokens + return max( + 1, + int( + detected_max_tokens + or detected_context_window + or self.configured_context_window + or 1 + ), + ) async def resolve_safe_max_tokens( self, *, system_prompt: str, user_prompt, - requested_max_tokens: int, + requested_max_tokens: int | None, + force_refresh: bool = False, ) -> int: request_context_window = ( - await self.resolve_request_context_window() + await self.resolve_request_context_window( + force_refresh=force_refresh, + ) ) + # resolve_request_context_window() already refreshed the shared model + # metadata above, so reuse that same snapshot for the output ceiling. request_max_tokens = await self.resolve_request_max_tokens( - requested_max_tokens + requested_max_tokens, + force_refresh=False, ) if not request_context_window: @@ -799,6 +885,8 @@ async def resolve_safe_max_tokens( - settings.RUNTIME_OUTPUT_TOKEN_RESERVE ) + # One generation budget covers reasoning + visible answer together. + # There is deliberately no fixed reasoning/answer split. return max( 1, min( @@ -927,14 +1015,16 @@ async def build_safe_payload( system_prompt: str, user_prompt, temperature: float, - max_tokens: int, + max_tokens: int | None, stream: bool = False, + force_refresh_limits: bool = False, ) -> dict[str, object]: safe_max_tokens = await self.resolve_safe_max_tokens( system_prompt=system_prompt, user_prompt=user_prompt, requested_max_tokens=max_tokens, + force_refresh=force_refresh_limits, ) return self.build_payload( @@ -955,7 +1045,7 @@ async def ask( system_prompt: str, user_prompt, temperature: float, - max_tokens: int, + max_tokens: int | None, timeout: float | None = None, ): @@ -965,6 +1055,7 @@ async def ask( temperature=temperature, max_tokens=max_tokens, stream=False, + force_refresh_limits=True, ) endpoint = join_url( @@ -1031,7 +1122,7 @@ async def stream( system_prompt: str, user_prompt, temperature: float, - max_tokens: int, + max_tokens: int | None, ): provider_user_prompt = self.provider_user_prompt( @@ -1045,6 +1136,7 @@ async def stream( temperature=temperature, max_tokens=max_tokens, stream=True, + force_refresh_limits=True, ) stream_id = None diff --git a/runtime/memory_common.py b/runtime/memory_common.py index 290cf35a..8f0a6bf0 100644 --- a/runtime/memory_common.py +++ b/runtime/memory_common.py @@ -619,10 +619,10 @@ async def refresh_runtime_memory_summarizer_usage( total_tokens or context_tokens ), - max_tokens=( - context_window - or config.SERVICE_CONTEXT_WINDOW - ), + # Deliberately keep the configured service context as the UI + # denominator. The live LM Studio context is used for request + # budgeting elsewhere, so usage may legitimately exceed 100% here. + max_tokens=config.SERVICE_CONTEXT_WINDOW, last_error=None, status="online", ) @@ -703,23 +703,10 @@ def latest_turn_context_is_overloaded( explicit_value ) - runtime_id = ( - config.SERVICE_MODEL_UID - if config.USE_SERVICE_AS_BRAIN - else config.BRAIN_MODEL_UID - ) - - runtime = ( - runtime_state - .get_all_runtime_states() - .get( - runtime_id - ) - ) - - return runtime_usage_is_context_overloaded( - runtime - ) + # Runtime-state max_tokens is the manually configured UI denominator, not + # the provider request limit. Never let a >100% display value change memory + # behavior; actual prompt overload checks use the live LM Studio context. + return False def runtime_prompt_is_context_overloaded( @@ -750,7 +737,7 @@ def build_runtime_summarizer_payload( system_prompt: str, user_prompt: str, temperature: float, - max_tokens: int, + max_tokens: int | None, stream: bool = False, ) -> dict: @@ -835,6 +822,7 @@ async def log_runtime_summarizer_result( *, label: str, result: str, + **extra, ) -> None: await log_memory_event( @@ -849,6 +837,7 @@ async def log_runtime_summarizer_result( ), fallback_channel="summarizer", event="summarizer_result", + **extra, ) def build_runtime_summarizer_response_details( diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 0532752f..48965a98 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -229,10 +229,6 @@ class RuntimeContext: default_factory=list ) - runtime_l2_pending_patches: list[dict] = field( - default_factory=list - ) - runtime_l2_last_turn: int = 0 runtime_zero_diff_alert: dict | None = None diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 98abff1b..cadeb0eb 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -154,7 +154,7 @@ def test_parses_long_term_fact_ids_for_delayed_memory_report(self): "body:\n" "Reusable project summary.\n" "long_term_facts_ids: " - "l4_abc123, l4_def456, invalid, l4_abc123" + "F1, F2, invalid, F1" ) ) @@ -165,8 +165,8 @@ def test_parses_long_term_fact_ids_for_delayed_memory_report(self): self.assertEqual( report_value["long_term_facts_ids"], [ - "l4_abc123", - "l4_def456", + "F1", + "F2", ], ) @@ -179,7 +179,7 @@ def test_build_report_keeps_only_existing_l4_fact_ids(self): runtime_long_term_memory_store={ "facts": [ { - "id": "l4_existing", + "id": "F1", "key": "project.fact", "value": "Existing fact", }, @@ -195,8 +195,8 @@ def test_build_report_keeps_only_existing_l4_fact_ids(self): "tags": ["project"], "body": "Body", "long_term_facts_ids": [ - "l4_existing", - "l4_missing", + "F1", + "F99", ], }, }), @@ -207,7 +207,7 @@ def test_build_report_keeps_only_existing_l4_fact_ids(self): self.assertEqual( report_value["long_term_facts_ids"], [ - "l4_existing", + "F1", ], ) @@ -1735,7 +1735,7 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): "tags:\n" "[social, context]\n" "long_term_facts_ids:\n" - "l4_existing, l4_new\n" + "F1, F2\n" "body:\n" "Additional durable context.\n" ) @@ -1768,8 +1768,8 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): "context", ], "long_term_facts_ids": [ - "l4_existing", - "l4_new", + "F1", + "F2", ], "body": "Additional durable context.", }, @@ -1782,7 +1782,7 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): ( "\n" "tags: social, context\n" - "long_term_facts_ids: l4_existing, l4_missing\n" + "long_term_facts_ids: F1, F99\n" "body:\n" "Additional durable context.\n" "" @@ -1815,8 +1815,8 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): "context", ], "long_term_facts_ids": [ - "l4_existing", - "l4_missing", + "F1", + "F99", ], "body": "Additional durable context.", }, @@ -1841,7 +1841,7 @@ def test_update_delayed_memory_appends_only_supported_fields(self): ], "body": "Original body.", "long_term_facts_ids": [ - "l4_existing", + "F1", ], "pinned": False, }, @@ -1849,12 +1849,12 @@ def test_update_delayed_memory_appends_only_supported_fields(self): context.runtime_long_term_memory_store = { "facts": [ { - "id": "l4_existing", + "id": "F1", "key": "user.name", "value": "Sergey", }, { - "id": "l4_new", + "id": "F2", "key": "user.role", "value": "Developer", }, @@ -1870,7 +1870,7 @@ def test_update_delayed_memory_appends_only_supported_fields(self): payload=( "a1b2c3\n" "tags: context, social\n" - "long_term_facts_ids: l4_new, l4_missing\n" + "long_term_facts_ids: F2, F99\n" "body:\n" "Additional body." ), @@ -1891,8 +1891,8 @@ def test_update_delayed_memory_appends_only_supported_fields(self): self.assertEqual( report["long_term_facts_ids"], [ - "l4_existing", - "l4_new", + "F1", + "F2", ], ) self.assertEqual( diff --git a/tests/runtime_actions/test_update_l4_facts.py b/tests/runtime_actions/test_update_l4_facts.py index 70d1fcfc..cf570e66 100644 --- a/tests/runtime_actions/test_update_l4_facts.py +++ b/tests/runtime_actions/test_update_l4_facts.py @@ -30,7 +30,7 @@ def test_marker_parses_focused_note_and_has_no_followup(self): ( "Memory clarified.\n" "\n" - '{"fact_ids":["l4_a","l4_b"],' + '{"fact_ids":["F1","F2"],' '"message":"Both facts describe the same residence."}\n' "" ), @@ -43,7 +43,7 @@ def test_marker_parses_focused_note_and_has_no_followup(self): self.assertEqual( json.loads(result.actions[0].payload), { - "fact_ids": ["l4_a", "l4_b"], + "fact_ids": ["F1", "F2"], "message": "Both facts describe the same residence.", }, ) @@ -100,13 +100,13 @@ async def test_runtime_action_updates_l4_in_background(self): context.runtime_long_term_memory_store = normalize_l4_store({ "facts": [ { - "id": "l4_social", + "id": "F1", "key": "user.social_connections", "value": "Taras is a close friend.", "category": "user_fact", }, { - "id": "l4_stakeholder", + "id": "F2", "key": "user.stakeholder_profile", "value": "Taras is a key technical stakeholder.", "category": "project_fact", @@ -117,15 +117,15 @@ async def test_runtime_action_updates_l4_in_background(self): "abc123": { "title": "Social and project context", "long_term_facts_ids": [ - "l4_social", - "l4_stakeholder", + "F1", + "F2", ], }, } action = RuntimeActionCall( name=RUNTIME_ACTION_UPDATE_L4_FACTS, payload=json.dumps({ - "fact_ids": ["l4_social", "l4_stakeholder"], + "fact_ids": ["F1", "F2"], "message": ( "Taras is both a close friend and an active technical " "stakeholder." @@ -148,8 +148,8 @@ async def test_runtime_action_updates_l4_in_background(self): self.assertEqual(len(facts), 1) self.assertEqual(facts[0]["key"], "user.relationship.taras") self.assertIn("close friend", facts[0]["value"]) - self.assertNotIn("l4_social", [fact["id"] for fact in facts]) - self.assertNotIn("l4_stakeholder", [fact["id"] for fact in facts]) + self.assertNotIn("F1", [fact["id"] for fact in facts]) + self.assertNotIn("F2", [fact["id"] for fact in facts]) replacement_id = facts[0]["id"] self.assertEqual( diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index b43acba0..027fd8a3 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -16,6 +16,7 @@ build_reasoning_recovery_context, format_followup_action_from_event, format_followup_actions_from_events, + format_previous_runtime_memory_tag, prepare_asset_results_for_turn, ) from rules.brain_context_builder import ( @@ -550,6 +551,52 @@ async def test_followup_event_formatter_groups_duplicate_action_names(self): "RESOLVE_ACTIVE_MEMORY (count: 2), SAVE_SESSION", ) + async def test_previous_runtime_memory_tag_tracks_elapsed_sequence_time(self): + + self.assertEqual( + format_previous_runtime_memory_tag( + sequence_started_at=1000.0, + now=1150.0, + ), + "", + ) + self.assertEqual( + format_previous_runtime_memory_tag( + sequence_started_at=1000.0, + now=1185.0, + ), + "", + ) + + async def test_followup_runtime_memory_tag_uses_sequence_started_at(self): + + context = _context() + context.runtime_current_sequence_started_at = 1000.0 + context.runtime_turn_started_at = 1000.0 + context.runtime_current_sequence_turn_id = "turn_000001" + context.runtime_current_turn_id = "turn_000001" + context.runtime_action_sequence_turn_ids = [] + context.runtime_session_action_history = [] + context.runtime_appended_delayed_memory = {} + + with patch( + "agent.nodes.brain.time.time", + return_value=1150.0, + ): + prompt = BrainNode.build_followup_system_prompt( + "state", + "continue", + context=context, + ) + + self.assertIn( + ( + "" + "state" + ), + prompt, + ) + async def test_followup_renames_runtime_memory_block(self): prompt = BrainNode.build_followup_system_prompt( @@ -2695,17 +2742,22 @@ async def fake_run_brain_stream(**kwargs): async def test_idle_followup_prompt_contains_tool_result_and_frozen_context(self): - prompt = build_idle_followup_system_prompt({ - "id": "idle_1", - "seconds": 5, - "origin_user_request": "original request", - "source_message": "reason ", - "context_snapshot": { - "system_prompt": ( - "frozen state" - ), - }, - }) + with patch( + "agent.nodes.brain.time.time", + return_value=1150.0, + ): + prompt = build_idle_followup_system_prompt({ + "id": "idle_1", + "seconds": 5, + "origin_user_request": "original request", + "source_message": "reason ", + "sequence_started_at": 1000.0, + "context_snapshot": { + "system_prompt": ( + "frozen state" + ), + }, + }) self.assertIn( '', @@ -2732,7 +2784,10 @@ async def test_idle_followup_prompt_contains_tool_result_and_frozen_context(self 1, ) self.assertIn( - "frozen state", + ( + "" + "frozen state" + ), prompt, ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 4baf8cb8..72040baa 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -163,7 +163,7 @@ async def stream(self, **_kwargs): yield { "type": "content", "content": ( - '\n{"fact_ids":["l4_a"],' + '\n{"fact_ids":["F1"],' '"message":"The relation is clarified."}\n' ), } diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index ff1bb36e..f4c6fe41 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -32,8 +32,8 @@ def build_report( "body": body, "pinned": True, "long_term_facts_ids": [ - "l4_abc123", - "l4_def456", + "F1", + "F2", ], "created_session_id": "session-a", "created_time": "2026-07-19T16:14:14.628194", @@ -83,8 +83,8 @@ def test_persist_and_load_direct_report_shape(self): self.assertEqual( payload["long_term_facts_ids"], [ - "l4_abc123", - "l4_def456", + "F1", + "F2", ], ) @@ -113,8 +113,8 @@ def test_persist_and_load_direct_report_shape(self): self.assertEqual( reports["48ggds"]["long_term_facts_ids"], [ - "l4_abc123", - "l4_def456", + "F1", + "F2", ], ) @@ -236,8 +236,8 @@ def test_local_storage_reports_override_file_fallback_by_id(self): self.assertEqual( merged["48ggds"]["long_term_facts_ids"], [ - "l4_abc123", - "l4_def456", + "F1", + "F2", ], ) diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index d6c010e5..58903007 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,7 +47,7 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=l4-logger-cards-2", source) + self.assertIn("/static/css/runtime-memory.css?v=memory-reference-sync-1", source) self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-17", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) diff --git a/tests/test_l1_memory.py b/tests/test_l1_memory.py index 0bd0783a..c084f8e3 100644 --- a/tests/test_l1_memory.py +++ b/tests/test_l1_memory.py @@ -1472,10 +1472,10 @@ async def emit(event): telemetry_events[-1]["runtime"][ RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID ]["max_tokens"], - 8192, + config.SERVICE_CONTEXT_WINDOW, ) - async def test_summarizer_uses_service_max_tokens(self): + async def test_summarizer_uses_auto_runtime_output_budget(self): service_client = FakeServiceClient( "- Active topic: available functions\n" @@ -1519,9 +1519,8 @@ async def test_summarizer_uses_service_max_tokens(self): ), 1, ) - self.assertEqual( + self.assertIsNone( service_client.calls[0]["max_tokens"], - config.SERVICE_MAX_TOKENS, ) self.assertEqual( service_client.calls[0]["timeout"], diff --git a/tests/test_l3_session_memory.py b/tests/test_l3_session_memory.py index 4535eb4f..25dab801 100644 --- a/tests/test_l3_session_memory.py +++ b/tests/test_l3_session_memory.py @@ -609,9 +609,13 @@ async def emit(event): service_client.calls[0]["timeout"], config.SERVICE_REQUEST_TIMEOUT, ) - self.assertLess( + self.assertGreater( + service_client.calls[0]["max_tokens"], + 0, + ) + self.assertLessEqual( service_client.calls[0]["max_tokens"], - config.SERVICE_MAX_TOKENS, + 2048, ) self.assertGreaterEqual( service_client.calls[0]["max_tokens"], diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index 526e9d58..2bfcc6bc 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -66,7 +66,7 @@ def test_cache_versions_are_bumped(self): self.assertIn('/static/css/runtime-memory.css?v=l4-logger-cards-2', source) self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-restore-1', source) - self.assertIn('/static/js/logger/logger.js?v=l4-truncate-reason-1', source) + self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) self.assertIn('/static/js/logger/log-entries.js?v=l4-response-visibility-1', source) self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-1', source) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 748e73f9..870760eb 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -16,6 +16,9 @@ add_l4_pending_candidates, apply_l4_merge_operations, build_l4_fact_id, + build_l4_merge_batch_plan, + build_l4_merge_system_prompt, + build_l4_merge_user_prompt, collect_pending_facts_memory_fields, extract_l4_json_payload, format_l4_fact_line, @@ -170,7 +173,7 @@ def test_l4_fact_normalization_ignores_legacy_score_field(self): store = normalize_l4_store({ "facts": [ { - "id": "l4_gpu", + "id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 4090.", "category": "environment", @@ -189,13 +192,54 @@ def test_l4_fact_normalization_ignores_legacy_score_field(self): self.assertNotIn(legacy_field, store["facts"][0]) self.assertNotIn(legacy_field, store["pending_facts"][0]) + def test_legacy_hash_ids_migrate_once_to_compact_f_and_pf_ids(self): + legacy = { + "version": 1, + "revision": 7, + "facts": [ + { + "id": "l4_alpha123", + "key": "user.hardware.main_gpu", + "value": "User's main GPU is RTX 3080 Ti.", + "category": "environment", + "source_fact_ids": ["l4p_processed123"], + }, + ], + "pending_facts": [ + { + "id": "l4p_waiting123", + "key": "user.preference.response_language", + "value": "User prefers Russian replies.", + "category": "user_preference", + }, + ], + "deleted_fact_ids": ["l4_deleted123"], + } + + migrated = normalize_l4_store(legacy, now="2026-08-08T17:00:00Z") + + self.assertEqual(migrated["version"], 2) + self.assertEqual(migrated["revision"], 8) + self.assertEqual(migrated["facts"][0]["id"], "F1") + self.assertEqual(migrated["pending_facts"][0]["id"], "PF1") + self.assertEqual(migrated["deleted_fact_ids"], ["F2"]) + self.assertEqual(migrated["facts"][0]["source_fact_ids"], ["PF2"]) + self.assertEqual(migrated["next_fact_id"], 3) + self.assertEqual(migrated["next_pending_fact_id"], 3) + + normalized_again = normalize_l4_store( + migrated, + now="2026-08-08T17:01:00Z", + ) + self.assertEqual(normalized_again, migrated) + def test_file_store_fallback_survives_empty_browser_sync(self): with tempfile.TemporaryDirectory() as directory: persisted_store = normalize_l4_store({ "revision": 4, "facts": [ { - "id": "l4_saved", + "id": "F1", "key": "user.identity", "value": "Sergey", "source_session_ids": ["session-normal"], @@ -245,7 +289,7 @@ async def test_deleted_fact_survives_server_restart_and_stale_profile_sync(self) "updated_at": "2026-08-05T11:48:48Z", "facts": [ { - "id": "l4_secret", + "id": "F1", "key": "project.secret_number_73", "value": "The number 73 was established.", }, @@ -268,7 +312,7 @@ async def test_deleted_fact_survives_server_restart_and_stale_profile_sync(self) deleted = await delete_l4_memory_fact( context, - "l4_secret", + "F1", ) self.assertTrue(deleted) @@ -279,7 +323,7 @@ async def test_deleted_fact_survives_server_restart_and_stale_profile_sync(self) self.assertEqual(persisted_after_delete["facts"], []) self.assertEqual( persisted_after_delete["deleted_fact_ids"], - ["l4_secret"], + ["F1"], ) restarted_context = RuntimeContext( @@ -305,7 +349,7 @@ async def test_deleted_fact_survives_server_restart_and_stale_profile_sync(self) self.assertEqual(persisted_after_sync["facts"], []) self.assertEqual( persisted_after_sync["deleted_fact_ids"], - ["l4_secret"], + ["F1"], ) def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): @@ -314,7 +358,7 @@ def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): "revision": 4, "facts": [ { - "id": "l4_existing", + "id": "F1", "key": "relationship.association", "value": "Anya is associated with known cohabitation data.", "updated_at": "2026-08-03T18:15:34Z", @@ -326,7 +370,7 @@ def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): "revision": 4, "facts": [ { - "id": "l4_incoming", + "id": "F2", "key": "relationship.association", "value": "Anya is associated with newer relationship context.", "updated_at": "2026-08-03T18:20:00Z", @@ -349,18 +393,18 @@ def test_store_snapshot_merge_keeps_one_fact_per_key_with_sources(self): ) self.assertEqual( merged["facts"][0]["source_fact_ids"], - ["l4_incoming"], + ["F2"], ) def test_store_normalization_prunes_processed_pending_fact(self): - processed_pending_id = "l4p_fd9b07b15788" - waiting_pending_id = "l4p_waiting" + processed_pending_id = "PF1" + waiting_pending_id = "PF2" store = normalize_l4_store( { "facts": [ { - "id": "l4_32df84cf695d", + "id": "F1", "key": "jin_structural_awareness", "value": "JIN tracks structural awareness.", "category": "other", @@ -391,7 +435,7 @@ def test_store_normalization_prunes_processed_pending_fact(self): ) def test_store_merge_does_not_resurrect_processed_pending_fact(self): - processed_pending_id = "l4p_fd9b07b15788" + processed_pending_id = "PF1" merged, change = merge_l4_store_snapshots( { @@ -399,7 +443,7 @@ def test_store_merge_does_not_resurrect_processed_pending_fact(self): "updated_at": "2026-08-04T17:34:05Z", "facts": [ { - "id": "l4_32df84cf695d", + "id": "F1", "key": "jin_structural_awareness", "value": "JIN tracks structural awareness.", "category": "other", @@ -488,11 +532,137 @@ def test_merge_requires_one_valid_operation_for_every_pending_fact(self): self.assertEqual(after["facts"], before["facts"]) self.assertEqual(after["pending_facts"], before["pending_facts"]) + def test_merge_prompt_uses_slim_model_view_without_provenance_metadata(self): + prompt = build_l4_merge_user_prompt( + existing_facts=[ + { + "id": "F2", + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + "source_session_ids": ["session-a"], + "source_runtime_snapshot_ids": ["runtime-1"], + "source_keys": ["language"], + "source_fact_ids": ["F8"], + "created_at": "2026-08-01T00:00:00Z", + "updated_at": "2026-08-02T00:00:00Z", + "mention_count": 9, + }, + ], + pending_facts=[ + { + "id": "PF1", + "key": "user.preference.response_language", + "value": "The user prefers Russian replies.", + "category": "user_preference", + "source_session_ids": ["session-b"], + }, + ], + ) + + self.assertIn('"id":"F2"', prompt) + self.assertIn('"id":"PF1"', prompt) + self.assertNotIn("source_session_ids", prompt) + self.assertNotIn("source_runtime_snapshot_ids", prompt) + self.assertNotIn("source_fact_ids", prompt) + self.assertNotIn("mention_count", prompt) + self.assertNotIn("created_at", prompt) + self.assertNotIn("updated_at", prompt) + + def test_merge_batch_plan_is_bounded_by_runtime_context_window(self): + existing_facts = normalize_l4_store({ + "facts": [ + { + "id": f"F{index + 1}", + "key": f"project.fact_{index}", + "value": "Durable project fact " + ("detail " * 8), + "category": "project_fact", + } + for index in range(20) + ], + })["facts"] + store, _ = add_l4_pending_candidates( + normalize_l4_store({"facts": existing_facts}), + [ + { + "key": f"project.pending_{index}", + "value": "Pending durable fact " + ("detail " * 10), + "category": "project_fact", + } + for index in range(30) + ], + now="2026-08-02T12:00:00Z", + ) + + plan_4k = build_l4_merge_batch_plan( + existing_facts=store["facts"], + pending_facts=store["pending_facts"], + system_prompt=build_l4_merge_system_prompt(), + runtime_context_window=4096, + requested_max_tokens=32768, + runtime_output_reserve=256, + ) + plan_8k = build_l4_merge_batch_plan( + existing_facts=store["facts"], + pending_facts=store["pending_facts"], + system_prompt=build_l4_merge_system_prompt(), + runtime_context_window=8192, + requested_max_tokens=32768, + runtime_output_reserve=256, + ) + + self.assertTrue(plan_4k["fits"]) + self.assertLess(plan_4k["batch_count"], 30) + self.assertLessEqual(plan_4k["estimated_total_tokens"], 4096) + self.assertGreater(plan_8k["batch_count"], plan_4k["batch_count"]) + self.assertEqual(plan_4k["requested_max_output_tokens"], 32768) + + def test_merge_can_apply_one_batch_and_leave_rest_of_pending_queue(self): + store, _ = add_l4_pending_candidates( + normalize_l4_store({}), + [ + { + "key": f"project.fact_{index}", + "value": f"Durable project fact {index}.", + "category": "project_fact", + } + for index in range(3) + ], + now="2026-08-02T12:00:00Z", + ) + first_two = store["pending_facts"][:2] + operations = [ + { + "action": "create", + "pending_id": fact["id"], + "key": fact["key"], + "value": fact["value"], + "category": fact["category"], + } + for fact in first_two + ] + + merged, change = apply_l4_merge_operations( + store, + operations, + pending_ids=[fact["id"] for fact in first_two], + now="2026-08-02T12:01:00Z", + ) + + self.assertTrue(change["valid"]) + self.assertEqual(len(merged["facts"]), 2) + self.assertEqual(len(merged["pending_facts"]), 1) + self.assertEqual( + merged["pending_facts"][0]["id"], + store["pending_facts"][2]["id"], + ) + self.assertEqual(change["pending_count"], 1) + def test_merge_applies_complete_batch_atomically(self): existing = normalize_l4_store({ "facts": [ { - "id": "l4_gpu", + "id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 3080 Ti.", "category": "environment", @@ -522,7 +692,7 @@ def test_merge_applies_complete_batch_atomically(self): { "action": "update", "pending_id": gpu_pending["id"], - "target_id": "l4_gpu", + "target_id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 4090.", "category": "environment", @@ -543,7 +713,7 @@ def test_merge_applies_complete_batch_atomically(self): self.assertTrue(change["valid"]) self.assertEqual(merged["pending_facts"], []) self.assertEqual(len(merged["facts"]), 1) - self.assertEqual(merged["facts"][0]["id"], "l4_gpu") + self.assertEqual(merged["facts"][0]["id"], "F1") self.assertIn("RTX 4090", merged["facts"][0]["value"]) self.assertEqual( merged["facts"][0]["source_fact_ids"], @@ -555,7 +725,7 @@ def test_merge_applies_complete_batch_atomically(self): update_detail = change["operation_details"][0] self.assertEqual(update_detail["action"], "update") self.assertEqual(update_detail["pending_id"], gpu_pending["id"]) - self.assertEqual(update_detail["target_id"], "l4_gpu") + self.assertEqual(update_detail["target_id"], "F1") self.assertIn("RTX 4090", update_detail["pending_fact"]["value"]) self.assertIn("RTX 3080 Ti", update_detail["target_before"]["value"]) self.assertIn("RTX 4090", update_detail["target_after"]["value"]) @@ -575,11 +745,11 @@ def test_merge_reinforce_tracks_pending_fact_source_id(self): existing = normalize_l4_store({ "facts": [ { - "id": "l4_language", + "id": "F2", "key": "user.preference.response_language", "value": "The user prefers Russian replies.", "category": "user_preference", - "source_fact_ids": ["l4_legacy_source"], + "source_fact_ids": ["F9"], }, ], }) @@ -590,7 +760,7 @@ def test_merge_reinforce_tracks_pending_fact_source_id(self): "key": "user.preference.response_language", "value": "The user prefers Russian replies.", "category": "user_preference", - "source_fact_ids": ["l4_imported_source"], + "source_fact_ids": ["F10"], }, ], now="2026-08-02T12:00:00Z", @@ -601,7 +771,7 @@ def test_merge_reinforce_tracks_pending_fact_source_id(self): { "action": "reinforce", "pending_id": pending["id"], - "target_id": "l4_language", + "target_id": "F2", }, ], }) @@ -613,12 +783,12 @@ def test_merge_reinforce_tracks_pending_fact_source_id(self): ) self.assertTrue(change["valid"]) - self.assertEqual(change["reinforced_ids"], ["l4_language"]) + self.assertEqual(change["reinforced_ids"], ["F2"]) self.assertEqual(len(change["operation_details"]), 1) reinforce_detail = change["operation_details"][0] self.assertEqual(reinforce_detail["action"], "reinforce") self.assertEqual(reinforce_detail["pending_id"], pending["id"]) - self.assertEqual(reinforce_detail["target_id"], "l4_language") + self.assertEqual(reinforce_detail["target_id"], "F2") self.assertEqual( reinforce_detail["pending_fact"]["value"], "The user prefers Russian replies.", @@ -638,8 +808,8 @@ def test_merge_reinforce_tracks_pending_fact_source_id(self): self.assertEqual( merged["facts"][0]["source_fact_ids"], [ - "l4_legacy_source", - "l4_imported_source", + "F9", + "F10", pending["id"], ], ) @@ -648,13 +818,13 @@ async def test_merge_phase_logs_concrete_operation_details(self): existing = normalize_l4_store({ "facts": [ { - "id": "l4_gpu", + "id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 3080 Ti.", "category": "environment", }, { - "id": "l4_language", + "id": "F2", "key": "user.preference.response_language", "value": "The user prefers Russian replies.", "category": "user_preference", @@ -683,7 +853,7 @@ async def test_merge_phase_logs_concrete_operation_details(self): {{ "action": "update", "pending_id": "{gpu_pending["id"]}", - "target_id": "l4_gpu", + "target_id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 4090.", "category": "environment" @@ -691,7 +861,7 @@ async def test_merge_phase_logs_concrete_operation_details(self): {{ "action": "reinforce", "pending_id": "{language_pending["id"]}", - "target_id": "l4_language" + "target_id": "F2" }} ] }}''') @@ -947,14 +1117,14 @@ def test_context_formats_all_facts_without_metadata(self): store = normalize_l4_store({ "facts": [ { - "id": "l4_gpu", + "id": "F1", "key": "user.hardware.main_gpu", "value": "User's main GPU is RTX 4090.", "category": "environment", "source_session_ids": ["session-a"], }, { - "id": "l4_style", + "id": "F2", "key": "user.preference.response_style", "value": "User prefers direct technical analysis.", }, @@ -966,8 +1136,8 @@ def test_context_formats_all_facts_without_metadata(self): self.assertIn("user.hardware.main_gpu:", context_block) self.assertIn("user.preference.response_style:", context_block) - self.assertIn("[ id: l4_gpu ]", context_block) - self.assertIn("[ id: l4_style ]", context_block) + self.assertIn("[ id: F1 ]", context_block) + self.assertIn("[ id: F2 ]", context_block) self.assertNotIn("source_session_ids", context_block) self.assertIn("[source_session_ids: session-a]", ui_line) @@ -981,12 +1151,12 @@ def test_delayed_report_fact_ids_hide_only_from_brain_context(self): context.runtime_long_term_memory_store = normalize_l4_store({ "facts": [ { - "id": "l4_archived", + "id": "F1", "key": "project.topic.details", "value": "Detailed topic facts moved into a report.", }, { - "id": "l4_active", + "id": "F2", "key": "user.name", "value": "Sergey", }, @@ -996,7 +1166,7 @@ def test_delayed_report_fact_ids_hide_only_from_brain_context(self): "abc123": { "title": "Topic report", "long_term_facts_ids": [ - "l4_archived", + "F1", ], }, } @@ -1010,13 +1180,13 @@ def test_delayed_report_fact_ids_hide_only_from_brain_context(self): context_block, ) self.assertIn( - "user.name: Sergey [ id: l4_active ]", + "user.name: Sergey [ id: F2 ]", context_block, ) self.assertEqual( context.runtime_l4_archived_fact_ids, { - "l4_archived", + "F1", }, ) self.assertEqual( @@ -1038,11 +1208,11 @@ def test_delayed_report_fact_ids_hide_merged_source_fact_ids_only_from_context(s context.runtime_long_term_memory_store = normalize_l4_store({ "facts": [ { - "id": "l4_current", + "id": "F1", "key": "relationship.association", "value": "Anya is associated with known cohabitation data.", "source_fact_ids": [ - "l4_archived_source", + "F9", ], }, ], @@ -1051,7 +1221,7 @@ def test_delayed_report_fact_ids_hide_merged_source_fact_ids_only_from_context(s "abc123": { "title": "Social context", "long_term_facts_ids": [ - "l4_archived_source", + "F9", ], }, } @@ -1076,7 +1246,7 @@ def test_delayed_report_fact_ids_hide_merged_source_fact_ids_only_from_context(s ][0][ "id" ], - "l4_current", + "F1", ) @@ -1090,7 +1260,7 @@ async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): context.runtime_long_term_memory_store = normalize_l4_store({ "facts": [ { - "id": "l4_project_details", + "id": "F1", "key": "project.topic.details", "value": "Detailed project context.", }, @@ -1105,7 +1275,7 @@ async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): ], "body": "Reusable project report.", "long_term_facts_ids": [ - "l4_project_details", + "F1", ], }, }) @@ -1129,13 +1299,13 @@ async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): "long_term_facts_ids" ], [ - "l4_project_details", + "F1", ], ) self.assertEqual( context.runtime_l4_archived_fact_ids, { - "l4_project_details", + "F1", }, ) self.assertEqual( @@ -1189,16 +1359,8 @@ def test_brain_context_always_injects_complete_long_term_memory(self): async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once(self): gpu_value = "User's main GPU is RTX 4090." language_value = "User prefers Russian replies." - gpu_pending_id = build_l4_fact_id( - key="user.hardware.main_gpu", - value=gpu_value, - pending=True, - ) - language_pending_id = build_l4_fact_id( - key="user.preference.response_language", - value=language_value, - pending=True, - ) + gpu_pending_id = build_l4_fact_id(sequence=1, pending=True) + language_pending_id = build_l4_fact_id(sequence=2, pending=True) service_client = FakeServiceClient([ f'''{{ "facts": [{{ @@ -1300,7 +1462,7 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( def test_restore_l4_fact_preserves_deleted_fact_object(self): fact = normalize_l4_store({ "facts": [{ - "id": "l4_restore_me", + "id": "F1", "key": "project.restore.test", "value": "Restore the complete fact.", "category": "project", @@ -1330,7 +1492,7 @@ async def test_delete_logs_complete_fact_and_restore_adds_it_back(self): ) context.runtime_long_term_memory_store = normalize_l4_store({ "facts": [{ - "id": "l4_restore_me", + "id": "F1", "key": "project.restore.test", "value": "Restore the complete fact.", "category": "project", diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py new file mode 100644 index 00000000..bc12ce56 --- /dev/null +++ b/tests/test_memory_citation_layers_client_contract.py @@ -0,0 +1,123 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +THINK_CITATIONS_JS = ROOT / "ui" / "static" / "js" / "think-citations.js" +THINK_WORKER_JS = ROOT / "ui" / "static" / "js" / "think-rule-worker.js" +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +CHAT_CSS = ROOT / "ui" / "static" / "css" / "chat.css" +MEMORY_VIEW_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +MEMORY_CSS = ROOT / "ui" / "static" / "css" / "runtime-memory.css" +AVATAR_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" +WIN95_CSS = ROOT / "ui" / "static" / "css" / "theme-win95.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class MemoryCitationLayersClientContractTests(unittest.TestCase): + def test_reasoning_citations_cover_runtime_active_and_l4_layers(self): + source = THINK_CITATIONS_JS.read_text(encoding="utf-8") + worker = THINK_WORKER_JS.read_text(encoding="utf-8") + + self.assertIn("function buildActiveMemoryCitationFragments()", source) + self.assertIn('sourceType: "active"', source) + self.assertIn('citationType: "active_memory_citation"', source) + self.assertIn("function buildL4CitationFragments()", source) + self.assertIn('sourceType: "l4"', source) + self.assertIn('citationType: "l4_citation"', source) + self.assertIn("...buildActiveMemoryCitationFragments(),", source) + self.assertIn("...buildL4CitationFragments(),", source) + self.assertIn('match.sourceType === "active"', source) + self.assertIn('match.sourceType === "l4"', source) + self.assertIn('match.sourceType === "active"', worker) + self.assertIn('match.sourceType === "l4"', worker) + + def test_active_and_l4_reasoning_citation_colors_are_layer_specific(self): + css = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn(".think-citation-active.exact", css) + self.assertIn("rgba(251, 191, 106, 0.96)", css) + self.assertIn(".think-citation-l4.exact", css) + self.assertIn("rgba(129, 230, 217, 0.98)", css) + + def test_reasoning_citation_reveal_is_not_hover_gated(self): + source = THINK_CITATIONS_JS.read_text(encoding="utf-8") + chat_source = CHAT_JS.read_text(encoding="utf-8") + css = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn('"jin:think-runtime-citation-highlight"', source) + self.assertNotIn('matches(":hover")', source) + self.assertNotIn('is-rule-highlight-revealing', source) + self.assertNotIn('has-rule-highlights:hover .think-citation-', css) + self.assertIn('.jin-think-content.has-rule-highlights .think-citation-runtime.exact', css) + self.assertNotIn('window.JinThinkCitations.syncThinkRuntimeCitationHighlight(', chat_source) + self.assertIn("function resetThinkCitationHighlightTurn()", source) + self.assertIn("resetThinkCitationHighlightTurn,", source) + self.assertIn("window.JinThinkCitations.resetThinkCitationHighlightTurn();", chat_source) + def test_latest_persistent_reference_text_combines_reasoning_and_answer(self): + source = CHAT_JS.read_text(encoding="utf-8") + start = source.index("const memoryReferenceText =") + end = source.index("if (stream.answer.trim())", start) + block = source[start:end] + + self.assertIn("stream.thinking,", block) + self.assertIn("stream.answer,", block) + self.assertIn("setLatestJinMemoryReferenceText(", block) + + def test_all_panel_rows_match_ids_titles_and_display_keys(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn("record.title,", source) + self.assertIn("record.name,", source) + self.assertIn("record.id,", source) + self.assertIn("record._storage_key,", source) + self.assertIn("record.active_memory_id,", source) + self.assertIn("memoryModel.runtimeMemoryDisplay.convertKeyToName(key)", source) + self.assertIn("record.title,", source) + + def test_structured_reasoning_citation_sync_survives_runtime_page_rerender(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn("const activeThinkMemoryCitationSources = new Map();", source) + self.assertIn('"runtime-memory-citation-hit"', source) + self.assertIn("handleThinkMemoryCitationHighlight", source) + self.assertIn("applyThinkMemoryCitationHighlights();", source) + self.assertIn("THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT", source) + + def test_avatar_aliases_cover_titles_and_display_keys_across_layers(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertIn("function getAvatarMemoryReferenceDisplayKey(value)", source) + self.assertIn("getAvatarMemoryReferenceDisplayKey(key)", source) + self.assertIn("title,", source) + self.assertIn('"is-memory-reference-hit"', source) + + def test_runtime_diff_colors_are_preserved_while_reference_glow_is_added(self): + css = MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn(".runtime-memory-reference-hit .runtime-memory-key.flash-new", css) + self.assertIn(".runtime-memory-citation-hit .runtime-memory-key.flash-new", css) + self.assertIn(".runtime-memory-reference-hit .runtime-memory-key.flash-changed", css) + self.assertIn(".runtime-memory-citation-hit .runtime-memory-value.flash-changed", css) + self.assertIn("0 0 18px rgba(248,250,252,0.20)", css) + + def test_win95_keeps_reference_and_citation_hits_consistent(self): + css = WIN95_CSS.read_text(encoding="utf-8") + + self.assertIn(".runtime-memory-reference-hit,", css) + self.assertIn(".runtime-memory-citation-hit {", css) + self.assertIn(".runtime-memory-citation-hit .runtime-memory-key", css) + + def test_cache_versions_are_bumped_for_citation_sync_assets(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("/static/css/runtime-memory.css?v=memory-reference-sync-1", source) + self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-14", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-7", source) + self.assertIn("/static/js/think-citations.js?v=think-citations-3", source) + self.assertIn("/static/js/chat.js?v=memory-reference-sync-2", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 02e99f51..4091e2b2 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -39,7 +39,7 @@ def test_runtime_updates_do_not_force_the_runtime_tab(self): handler, ) - def test_chat_dispatches_persistent_and_hover_reference_sources(self): + def test_chat_reference_highlight_is_persistent_per_jin_turn(self): source = CHAT_JS.read_text(encoding="utf-8") self.assertIn( @@ -47,18 +47,21 @@ def test_chat_dispatches_persistent_and_hover_reference_sources(self): source, ) self.assertIn( - 'function bindJinMemoryReferenceBubble(', + 'setLatestJinMemoryReferenceText(', source, ) - self.assertIn( - 'setLatestJinMemoryReferenceText(', + self.assertNotIn( + 'function bindJinMemoryReferenceBubble(', source, ) - self.assertIn( + self.assertNotIn( 'clearJinMemoryReferenceHighlights();', source, ) - + self.assertNotIn( + 'dispatchJinMemoryReferenceHighlight(\n "hover"', + source, + ) def test_memory_rows_expose_keys_and_ids_for_reference_matching(self): source = MEMORY_VIEW_JS.read_text(encoding="utf-8") @@ -140,19 +143,19 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=l4-logger-cards-1', + '/static/css/runtime-memory.css?v=memory-reference-sync-1', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-12', + '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-14', source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=runtime-facade-16', + '/static/js/runtime/runtime.js?v=runtime-facade-17', source, ) self.assertIn( - '/static/js/chat.js?v=chat-core-6', + '/static/js/chat.js?v=memory-reference-sync-2', source, ) diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 611854b9..3bc6e1b9 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -65,7 +65,7 @@ def test_avatar_cache_versions_are_bumped(self): source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-5", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-6", source, ) diff --git a/tests/test_runtime_client.py b/tests/test_runtime_client.py index c81e4cd2..9744e9fb 100644 --- a/tests/test_runtime_client.py +++ b/tests/test_runtime_client.py @@ -262,7 +262,7 @@ async def test_falls_back_to_configured_context_window(self): self.assertEqual( http_client.post_calls[0]["json"]["max_tokens"], - 1840, + 839, ) self.assertIsNone( client.detected_context_window, @@ -296,33 +296,30 @@ async def test_symbol_heavy_prompt_does_not_over_shrink_output_budget(self): max_tokens=8192, ) + self.assertEqual( + client.detected_context_window, + 8192, + ) self.assertGreater( http_client.post_calls[0]["json"]["max_tokens"], - 3000, + 1, ) async def test_uses_lmstudio_native_loaded_context_when_openai_models_has_no_context(self): http_client = FakeHttpClient( models_payloads_by_url={ - "http://runtime.test/v1/models": { - "data": [ - { - "id": "test-model", - } - ] - }, - "http://runtime.test/api/v0/models": { - "data": [ + "http://runtime.test/api/v1/models": { + "models": [ { - "id": "test-model", + "key": "test-model", "max_context_length": 131072, - "loaded_context_length": 8192, "loaded_instances": [ { + "id": "test-model", "config": { "context_length": 8192, - } + }, } ], } @@ -355,7 +352,7 @@ async def test_uses_lmstudio_native_loaded_context_when_openai_models_has_no_con ) self.assertEqual( len(http_client.get_calls), - 2, + 1, ) async def test_prefers_loaded_context_over_theoretical_max_context(self): @@ -371,7 +368,7 @@ async def test_prefers_loaded_context_over_theoretical_max_context(self): 8192, ) - async def test_context_window_detection_is_cached(self): + async def test_each_model_request_refreshes_live_context_metadata(self): http_client = FakeHttpClient( models_payload={ @@ -408,7 +405,7 @@ async def test_context_window_detection_is_cached(self): self.assertEqual( len(http_client.get_calls), - 1, + 2, ) async def test_context_window_detection_skips_model_without_id(self): @@ -446,6 +443,42 @@ async def test_context_window_detection_skips_model_without_id(self): 8192, ) + async def test_auto_max_tokens_uses_live_context_not_configured_indicator(self): + + http_client = FakeHttpClient( + models_payload={ + "data": [ + { + "id": "test-model", + "context_length": 8192, + } + ] + } + ) + client = RuntimeClient( + api_base="http://runtime.test", + model_uid="test-model", + timeout=30.0, + configured_context_window=4096, + client=http_client, + ) + + await client.ask( + system_prompt="system", + user_prompt="user", + temperature=0.1, + max_tokens=None, + ) + + self.assertEqual( + client.detected_context_window, + 8192, + ) + self.assertGreater( + http_client.post_calls[0]["json"]["max_tokens"], + 4096, + ) + async def test_preserves_configured_max_tokens_when_context_window_is_detected(self): http_client = FakeHttpClient( diff --git a/tests/test_token_usage.py b/tests/test_token_usage.py index 847bd5bd..70191684 100644 --- a/tests/test_token_usage.py +++ b/tests/test_token_usage.py @@ -16,14 +16,14 @@ class TokenUsageTests(unittest.TestCase): - def test_stream_estimate_uses_conservative_context_size(self): + def test_general_estimate_uses_conservative_context_size(self): prompt = ( "active_runtime_memory_entry " * 200 ) - self.assertLess( + self.assertEqual( estimate_tokens( prompt ), @@ -32,6 +32,12 @@ def test_stream_estimate_uses_conservative_context_size(self): prompt_text=prompt, ), ) + self.assertEqual( + estimate_tokens( + prompt + ), + 1400, + ) self.assertEqual( estimate_stream_input_tokens( None, diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 7153e192..77d76c0a 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -501,58 +501,79 @@ text-shadow 1s ease; } -.jin-think-content.is-rule-highlight-revealing .think-citation-rule.exact, -.jin-think-content.has-rule-highlights:hover .think-citation-rule.exact { +.jin-think-content.has-rule-highlights .think-citation-rule.exact { color: #fff7df; text-shadow: 0 0 8px rgba(255, 245, 220, 0.55), 0 0 18px rgba(255, 200, 130, 0.28); } -.jin-think-content.is-rule-highlight-revealing .think-citation-rule.near, -.jin-think-content.has-rule-highlights:hover .think-citation-rule.near { +.jin-think-content.has-rule-highlights .think-citation-rule.near { color: #efe2c8; text-shadow: 0 0 8px rgba(255, 235, 200, 0.28); } -.jin-think-content.is-rule-highlight-revealing .think-citation-rule.compressed, -.jin-think-content.has-rule-highlights:hover .think-citation-rule.compressed { +.jin-think-content.has-rule-highlights .think-citation-rule.compressed { color: #d8cdb9; text-shadow: 0 0 6px rgba(255, 235, 200, 0.16); } -.jin-think-content.is-rule-highlight-revealing .think-citation-runtime.exact, -.jin-think-content.has-rule-highlights:hover .think-citation-runtime.exact { +.jin-think-content.has-rule-highlights .think-citation-runtime.exact { color: rgba(134,239,172,0.96); text-shadow: 0 0 10px rgba(34,197,94,0.35); } -.jin-think-content.is-rule-highlight-revealing .think-citation-runtime.near, -.jin-think-content.has-rule-highlights:hover .think-citation-runtime.near { +.jin-think-content.has-rule-highlights .think-citation-runtime.near { color: rgba(134,239,172,0.86); text-shadow: 0 0 8px rgba(34,197,94,0.26); } -.jin-think-content.is-rule-highlight-revealing .think-citation-runtime.compressed, -.jin-think-content.has-rule-highlights:hover .think-citation-runtime.compressed { +.jin-think-content.has-rule-highlights .think-citation-runtime.compressed { color: rgba(134,239,172,0.76); text-shadow: 0 0 6px rgba(34,197,94,0.18); } -.jin-think-content.is-rule-highlight-revealing .think-citation-session.exact, -.jin-think-content.has-rule-highlights:hover .think-citation-session.exact { +.jin-think-content.has-rule-highlights .think-citation-active.exact { + color: rgba(251, 191, 106, 0.96); + text-shadow: 0 0 10px rgba(249, 115, 22, 0.34); +} + +.jin-think-content.has-rule-highlights .think-citation-active.near { + color: rgba(251, 191, 106, 0.86); + text-shadow: 0 0 8px rgba(249, 115, 22, 0.25); +} + +.jin-think-content.has-rule-highlights .think-citation-active.compressed { + color: rgba(251, 191, 106, 0.76); + text-shadow: 0 0 6px rgba(249, 115, 22, 0.17); +} + +.jin-think-content.has-rule-highlights .think-citation-l4.exact { + color: rgba(129, 230, 217, 0.98); + text-shadow: 0 0 10px rgba(45, 212, 191, 0.38); +} + +.jin-think-content.has-rule-highlights .think-citation-l4.near { + color: rgba(129, 230, 217, 0.88); + text-shadow: 0 0 8px rgba(45, 212, 191, 0.28); +} + +.jin-think-content.has-rule-highlights .think-citation-l4.compressed { + color: rgba(129, 230, 217, 0.78); + text-shadow: 0 0 6px rgba(45, 212, 191, 0.19); +} + +.jin-think-content.has-rule-highlights .think-citation-session.exact { color: rgba(216,180,254,0.94); text-shadow: 0 0 10px rgba(168,85,247,0.32); } -.jin-think-content.is-rule-highlight-revealing .think-citation-session.near, -.jin-think-content.has-rule-highlights:hover .think-citation-session.near { +.jin-think-content.has-rule-highlights .think-citation-session.near { color: rgba(216,180,254,0.84); text-shadow: 0 0 8px rgba(168,85,247,0.24); } -.jin-think-content.is-rule-highlight-revealing .think-citation-session.compressed, -.jin-think-content.has-rule-highlights:hover .think-citation-session.compressed { +.jin-think-content.has-rule-highlights .think-citation-session.compressed { color: rgba(216,180,254,0.74); text-shadow: 0 0 6px rgba(168,85,247,0.16); } diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 7689f97d..25355f0c 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -253,6 +253,22 @@ stroke-width: var(--jin-avatar-memory-hover-width, 2px); } +.jin-avatar-orbit.is-memory-hover-hit, +.jin-avatar-counter-orbit.is-memory-hover-hit, +.jin-avatar-memory-dash.is-memory-hover-hit { + filter: + brightness(1.95) + saturate(1.78) + drop-shadow(0 0 5px var(--jin-avatar-memory-glow-near, rgba(210, 255, 255, 0.98))) + drop-shadow(0 0 14px var(--jin-avatar-memory-glow-mid, rgba(90, 235, 245, 0.68))) + drop-shadow(0 0 28px var(--jin-avatar-memory-glow-far, rgba(52, 211, 222, 0.30))); +} + +.jin-avatar-memory-dash.is-memory-hover-hit path { + stroke-opacity: 1 !important; + stroke-width: calc(var(--jin-avatar-memory-hover-width, 2px) + 0.45px); +} + .jin-avatar-center-glow-fill, .jin-avatar-center-soft, .jin-avatar-center-core, diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index b2bf713c..8e4baf72 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -20,11 +20,14 @@ } .runtime-memory-reference-hit .runtime-memory-key, -.runtime-memory-reference-hit .runtime-memory-value { - color: rgba(222, 245, 241, 0.88); +.runtime-memory-reference-hit .runtime-memory-value, +.runtime-memory-citation-hit .runtime-memory-key, +.runtime-memory-citation-hit .runtime-memory-value { + color: rgba(244, 244, 245, 0.92); text-shadow: - 0 0 7px rgba(210, 246, 241, 0.34), - 0 0 13px rgba(129, 216, 208, 0.22); + 0 0 7px rgba(248, 250, 252, 0.30), + 0 0 15px rgba(226, 232, 240, 0.20), + 0 0 24px rgba(203, 213, 225, 0.12); } .runtime-memory-delayed-row { @@ -41,10 +44,10 @@ outline: none; } -.runtime-memory-delayed-row:hover .runtime-memory-key, -.runtime-memory-delayed-row:hover .runtime-memory-value, -.runtime-memory-delayed-row:focus-visible .runtime-memory-key, -.runtime-memory-delayed-row:focus-visible .runtime-memory-value { +.runtime-memory-line:not(.runtime-memory-user-idle):hover .runtime-memory-key, +.runtime-memory-line:not(.runtime-memory-user-idle):hover .runtime-memory-value, +.runtime-memory-line:not(.runtime-memory-user-idle):focus-visible .runtime-memory-key, +.runtime-memory-line:not(.runtime-memory-user-idle):focus-visible .runtime-memory-value { color: rgba(244, 244, 245, 0.86); } @@ -56,9 +59,9 @@ .runtime-memory-value { color: rgba(244,244,245,0.62); transition: - color 1.3s ease, - font-weight 1.3s ease, - text-shadow 1.3s ease; + color 0.62s cubic-bezier(0.22, 0.61, 0.36, 1), + font-weight 0.62s cubic-bezier(0.22, 0.61, 0.36, 1), + text-shadow 0.62s cubic-bezier(0.22, 0.61, 0.36, 1); } .runtime-memory-value { @@ -299,6 +302,42 @@ ); } +.runtime-memory-reference-hit .runtime-memory-key.flash-new, +.runtime-memory-reference-hit .runtime-memory-value.flash-new, +.runtime-memory-citation-hit .runtime-memory-key.flash-new, +.runtime-memory-citation-hit .runtime-memory-value.flash-new { + color: rgba(134,239,172,0.96); + text-shadow: + 0 0 10px rgba(34,197,94,0.35), + 0 0 18px rgba(248,250,252,0.20); +} + +.runtime-memory-reference-hit .runtime-memory-key.flash-changed, +.runtime-memory-citation-hit .runtime-memory-key.flash-changed { + color: rgba(216,180,254,0.96); + text-shadow: + 0 0 10px rgba(168,85,247,0.35), + 0 0 18px rgba(248,250,252,0.20); +} + +.runtime-memory-reference-hit .runtime-memory-value.flash-changed, +.runtime-memory-citation-hit .runtime-memory-value.flash-changed { + color: rgba( + 134, + 239, + 172, + var(--memory-change-alpha, 0.80) + ); + text-shadow: + 0 0 10px rgba( + 34, + 197, + 94, + var(--memory-change-glow, 0.22) + ), + 0 0 18px rgba(248,250,252,0.20); +} + .runtime-memory-delayed-row-pinned .runtime-memory-key, .runtime-memory-delayed-row-pinned .runtime-memory-value { color: rgba(244, 244, 245, 0.92); diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 25350d4a..aff54377 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -561,13 +561,16 @@ body.theme-win95 #summarizer-window-bar::after { ); } -body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit { +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit { background: #d7f0ed; box-shadow: inset 0 0 0 1px #6b9e99; } body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-key, -body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-value { +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-value, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit .runtime-memory-key, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit .runtime-memory-value { color: #005650 !important; text-shadow: none; } @@ -922,38 +925,30 @@ body.theme-win95 .jin-think-content.is-collapsed { box-shadow: none; } -body.theme-win95 .jin-think-content .think-rule-hit, -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-rule-hit, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-rule-hit { +body.theme-win95 .jin-think-content .think-rule-hit { text-shadow: none; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-runtime.exact, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-runtime.exact { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-runtime.exact { color: #006f2f; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-runtime.near, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-runtime.near { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-runtime.near { color: #007a35; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-runtime.compressed, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-runtime.compressed { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-runtime.compressed { color: #168041; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-session.exact, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-session.exact { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-session.exact { color: #5f22b3; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-session.near, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-session.near { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-session.near { color: #6f35bd; } -body.theme-win95 .jin-think-content.is-rule-highlight-revealing .think-citation-session.compressed, -body.theme-win95 .jin-think-content.has-rule-highlights:hover .think-citation-session.compressed { +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-session.compressed { color: #7950b8; } diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index 0a500902..546d7107 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -223,6 +223,12 @@ function setLatestJinMemoryReferenceText( return; } + if ( + window.JinThinkCitations + && typeof window.JinThinkCitations.resetThinkCitationHighlightTurn === "function" + ) { + window.JinThinkCitations.resetThinkCitationHighlightTurn(); + } dispatchJinMemoryReferenceHighlight( "persistent", text, @@ -230,51 +236,6 @@ function setLatestJinMemoryReferenceText( ); } -function clearJinMemoryReferenceHighlights() { - dispatchJinMemoryReferenceHighlight( - "persistent", - "", - false - ); - dispatchJinMemoryReferenceHighlight( - "hover", - "", - false - ); -} - -function bindJinMemoryReferenceBubble( - bubble, - content, - role -) { - if ( - !bubble - || !content - || !isJinMemoryReferenceRole(role) - ) { - return; - } - - bubble.addEventListener("mouseenter", () => { - dispatchJinMemoryReferenceHighlight( - "hover", - content.dataset.memoryReferenceText - || content.textContent - || "", - true - ); - }); - - bubble.addEventListener("mouseleave", () => { - dispatchJinMemoryReferenceHighlight( - "hover", - "", - false - ); - }); -} - function shouldFormatChatRole(role) { return ( @@ -798,11 +759,6 @@ function createMessageElement( config.bubbleClass; bubble.appendChild(pre); - bindJinMemoryReferenceBubble( - bubble, - pre, - role - ); msgDiv.appendChild( createAvatarElement( @@ -916,7 +872,6 @@ function appendChatMessage( jinConversationTurnCounter += 1; window.jinConversationTurnCounter = jinConversationTurnCounter; - clearJinMemoryReferenceHighlights(); } else { setLatestJinMemoryReferenceText( role, @@ -944,8 +899,10 @@ function scrollCollapsedThinkToLatest( return; } - thinkContent.scrollTop = - thinkContent.scrollHeight; + thinkContent.scrollTo({ + top: thinkContent.scrollHeight, + behavior: "smooth", + }); } @@ -1108,20 +1065,6 @@ function createStreamGroup( } ); - [ - "mouseenter", - "mouseleave", - ].forEach((eventName) => { - thinkContent.addEventListener( - eventName, - () => { - window.JinThinkCitations.syncThinkRuntimeCitationHighlight( - thinkContent - ); - } - ); - }); - thinkWrapper.appendChild( thinkContent ); @@ -1147,11 +1090,6 @@ function createStreamGroup( config.bubbleClass; bubble.appendChild(pre); - bindJinMemoryReferenceBubble( - bubble, - pre, - role - ); messageRow.appendChild( createAvatarElement( @@ -1473,11 +1411,23 @@ function finishStreamMessage( stream.group.wrapper.remove(); } - if (stream.answer.trim()) { + const memoryReferenceText = + [ + stream.thinking, + stream.answer, + ] + .map(value => String(value || "").trim()) + .filter(Boolean) + .join("\n"); + + if (memoryReferenceText) { setLatestJinMemoryReferenceText( stream.role, - stream.answer + memoryReferenceText ); + } + + if (stream.answer.trim()) { flushRuntimeActionsAfterResponse( stream.role ); diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 072ae983..a96c2140 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -1251,6 +1251,172 @@ function handleL4SummarizerLog( return state.logDiv; } + +const l2SummarizerCards = []; + +function resolveL2SummarizerEvent( + message, + meta, +) { + if (String(meta && meta.memory_level || "").toUpperCase() !== "L2") { + return ""; + } + + const event = + String(meta && meta.memory_event || "").toLowerCase(); + + if (event === "summarizer_request" || event === "summarizer_result") { + return event; + } + + const normalized = + String(message || "").toLowerCase(); + + if (normalized.endsWith("summarizer request")) { + return "summarizer_request"; + } + + if (normalized.endsWith("summarizer result")) { + return "summarizer_result"; + } + + return ""; +} + +function createL2SummarizerCard( + requestDetails = null, +) { + const logDiv = + createL4LoggerCard( + "[MEMORY:L2]" + ); + + const actions = + document.createElement("div"); + + actions.className = + "mt-2 flex flex-wrap items-center gap-2"; + + const payloadButton = + createL4LoggerButton( + "payload" + ); + const responseButton = + createL4LoggerButton( + "response" + ); + + setL4LoggerButtonVisible( + payloadButton, + Boolean(requestDetails) + ); + setL4LoggerButtonVisible( + responseButton, + false + ); + + const state = { + logDiv, + requestDetails, + responseDetails: null, + responseSettled: false, + payloadButton, + responseButton, + }; + + payloadButton.addEventListener( + "click", + function () { + if (!state.requestDetails) { + return; + } + + showTrace( + prettifyTraceDetails( + state.requestDetails + ), + "L2 pattern memory payload" + ); + } + ); + + responseButton.addEventListener( + "click", + function () { + if (!state.responseSettled) { + return; + } + + const responseText = + String(state.responseDetails || "").trim(); + + showTrace( + responseText && responseText !== "" + ? responseText + : "No patterns", + "L2 pattern memory response" + ); + } + ); + + actions.appendChild(payloadButton); + actions.appendChild(responseButton); + logDiv.appendChild(actions); + + l2SummarizerCards.push(state); + + return state; +} + +function handleL2SummarizerLog( + message, + details, + meta, +) { + const event = + resolveL2SummarizerEvent( + message, + meta + ); + + if (!event) { + return null; + } + + if (event === "summarizer_request") { + return createL2SummarizerCard( + details + ).logDiv; + } + + let state = + [...l2SummarizerCards] + .reverse() + .find((candidate) => !candidate.responseSettled); + + if (!state) { + state = + createL2SummarizerCard(); + } + + state.responseSettled = true; + state.responseDetails = + String(details ?? ""); + + setL4LoggerButtonVisible( + state.responseButton, + true + ); + setL4LoggerButtonTone( + state.responseButton, + meta && meta.memory_changed === false + ? "muted" + : "blue" + ); + + return state.logDiv; +} + function settleL4SummarizerCardForTerminalEvent( message, meta, @@ -1522,6 +1688,17 @@ function appendLog( details, ); + const l2SummarizerLog = + handleL2SummarizerLog( + normalized.message, + normalized.details, + meta + ); + + if (l2SummarizerLog) { + return l2SummarizerLog; + } + settleL4SummarizerCardForTerminalEvent( normalized.message, meta diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 5221cfef..34a58c89 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -57,6 +57,7 @@ function extractTraceReason( parsed.summary || parsed.explanation || parsed.trace_reason + || parsed.reason || "" ).trim(); @@ -462,15 +463,10 @@ const consolePanel = document.getElementById("console-panel"); "panel-collapsed" ); - if (panel.dataset.expandedHeight) { - panel.style.height = - panel.dataset.expandedHeight; - delete panel.dataset.expandedHeight; - } else { - panel.style.removeProperty( - "height" - ); - } + panel.style.removeProperty( + "height" + ); + delete panel.dataset.expandedHeight; restorePanelDimension( panel, diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 14fc8c6c..3e030c63 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -5,7 +5,8 @@ const SVG_NS = "http://www.w3.org/2000/svg"; const AVATAR_EVENT = "jin:runtime-avatar-snapshot"; - const THINK_RUNTIME_CITATION_HOVER_EVENT = "jin:think-runtime-citation-hover"; + const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = "jin:think-runtime-citation-highlight"; + const MEMORY_ROW_AVATAR_HOVER_EVENT = "jin:memory-row-avatar-hover"; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = "jin:memory-reference-highlight"; const MEMORY_REFERENCE_ALIAS_DATASET_KEY = @@ -86,6 +87,14 @@ const settingsPanel = document.getElementById("settings-panel"); const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; + const buildCitationRecordIdentity = + typeof window.JinRuntime.buildCitationRecordIdentity === "function" + ? window.JinRuntime.buildCitationRecordIdentity + : () => ""; + const buildAvatarMemoryHoverId = + typeof window.JinRuntime.buildAvatarMemoryHoverId === "function" + ? window.JinRuntime.buildAvatarMemoryHoverId + : () => ""; const memoryReferenceHelpers = window.JinRuntime.memoryReferences || {}; const containsMemoryReference = @@ -110,7 +119,6 @@ let centerColorTransitionTimer = null; const memoryReferenceHighlightState = { persistentText: "", - hoverText: "", }; function clamp(value, min, max) { @@ -152,6 +160,24 @@ } } + function getAvatarMemoryReferenceDisplayKey(value) { + const key = String(value || "").trim(); + const runtimeModel = + window.JinRuntime + && window.JinRuntime.memoryModel; + + if ( + !key + || !runtimeModel + || !runtimeModel.runtimeMemoryDisplay + || typeof runtimeModel.runtimeMemoryDisplay.convertKeyToName !== "function" + ) { + return ""; + } + + return runtimeModel.runtimeMemoryDisplay.convertKeyToName(key); + } + function hashString(value) { let hash = 2166136261; @@ -342,6 +368,18 @@ }); } + function extractActiveMemoryId(value) { + const match = + String(value || "") + .match( + /\[\s*active_memory_id\s*:\s*([a-z0-9]{6})\s*\]/i + ); + + return match + ? String(match[1] || "").trim().toLowerCase() + : ""; + } + function getSnapshotLines(snapshot) { const sourceLines = snapshot && Array.isArray(snapshot.lines) ? snapshot.lines @@ -357,9 +395,15 @@ key, value, text, + avatarMemoryHoverId: + buildAvatarMemoryHoverId( + "runtime", + line && line.id || `line-${index}` + ), referenceAliases: normalizeMemoryReferenceAliases([ key, + getAvatarMemoryReferenceDisplayKey(key), line && line.id, line && line.active_memory_id, ...collectMemoryMetadataReferenceAliases(value), @@ -580,9 +624,16 @@ key, value, text: lineText, + avatarMemoryHoverId: + buildAvatarMemoryHoverId( + "active", + extractActiveMemoryId(text) + || `record-${index}` + ), referenceAliases: normalizeMemoryReferenceAliases([ key, + getAvatarMemoryReferenceDisplayKey(key), ...collectMemoryMetadataReferenceAliases(value), ]), citationText: @@ -633,10 +684,16 @@ title, summary, pinned: Boolean(report.pinned), + avatarMemoryHoverId: + buildAvatarMemoryHoverId( + "delayed", + id + ), referenceAliases: normalizeMemoryReferenceAliases([ id, report.id, + title, ]), }; }) @@ -680,10 +737,22 @@ key, value, text: lineText, + avatarMemoryHoverId: + buildAvatarMemoryHoverId( + "l4", + id + ), + citationIdentity: + buildCitationRecordIdentity( + id, + key, + value + ), referenceAliases: normalizeMemoryReferenceAliases([ id, key, + getAvatarMemoryReferenceDisplayKey(key), ]), citationText: normalizeRuntimeCitationIdentity(lineText), @@ -719,8 +788,10 @@ const dashGroup = createSvgElement("g", { class: classNames.join(" "), + "data-avatar-memory-hover-id": options.avatarMemoryHoverId || null, "data-runtime-line-key": options.citationKey || null, "data-runtime-line-text": options.citationText || null, + "data-runtime-line-identity": options.citationIdentity || null, "data-delayed-memory-id": options.delayedMemoryId || null, "data-l4-fact-id": options.l4FactId || null, }); @@ -847,6 +918,7 @@ color, glowColor: ACTIVE_MEMORY_RING_COLOR, opacity: 0.76, + avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.key), citationText: record.citationText, @@ -875,9 +947,11 @@ color, glowColor: L4_MEMORY_RING_COLOR, opacity: 0.52, + avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.key), citationText: record.citationText, + citationIdentity: record.citationIdentity, l4FactId: record.id, referenceAliases: record.referenceAliases, title: `L4 ${record.id} · ${record.key}: ${record.value}`, @@ -904,6 +978,7 @@ : color, opacity: pinned ? 0.82 : 0.36, pinned, + avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.id), delayedMemoryId: record.id, @@ -1279,6 +1354,10 @@ }); orbitGroup.dataset.runtimeLineIndex = String(record.index); + if (record.avatarMemoryHoverId) { + orbitGroup.dataset.avatarMemoryHoverId = + record.avatarMemoryHoverId; + } orbitGroup.dataset.runtimeLineKey = normalizeRuntimeCitationIdentity(record.key); orbitGroup.dataset.runtimeLineText = @@ -1419,14 +1498,57 @@ let currentRenderedSnapshotIndex = null; const activeThinkRuntimeCitationSources = new Map(); + let memoryRowAvatarHoverState = null; - function normalizeThinkRuntimeCitationHoverDetail(detail) { + function normalizeMemoryRowAvatarHoverDetail(detail) { + if (!detail || detail.active !== true) { + return null; + } + + const avatarMemoryHoverId = + String(detail.avatarMemoryHoverId || "").trim(); + + return avatarMemoryHoverId + ? { avatarMemoryHoverId } + : null; + } + + function applyMemoryRowAvatarHoverGlow() { + const svg = avatarRoot.querySelector("svg"); + + if (!svg) { + return; + } + + svg.querySelectorAll( + ".jin-avatar-orbit, .jin-avatar-counter-orbit, .jin-avatar-memory-dash" + ).forEach((node) => { + const matched = Boolean( + memoryRowAvatarHoverState + && node.dataset.avatarMemoryHoverId + === memoryRowAvatarHoverState.avatarMemoryHoverId + ); + + node.classList.toggle( + "is-memory-hover-hit", + matched + ); + }); + } + + function normalizeThinkRuntimeCitationHighlightDetail(detail) { if (!detail || detail.active !== true) { return null; } const sourceId = String(detail.sourceId || "unknown-think"); + const lineIdentities = + new Set( + (Array.isArray(detail.lineIdentities) ? detail.lineIdentities : []) + .map(normalizeRuntimeCitationIdentity) + .filter(Boolean) + ); const lineKeys = new Set( (Array.isArray(detail.lineKeys) ? detail.lineKeys : []) @@ -1440,27 +1562,35 @@ .filter(Boolean) ); - if (!lineKeys.size && !lineTexts.size) { + if ( + !lineIdentities.size + && !lineKeys.size + && !lineTexts.size + ) { return null; } return { sourceId, + lineIdentities, lineKeys, lineTexts, }; } function getActiveThinkRuntimeCitationIdentitySets() { + const lineIdentities = new Set(); const lineKeys = new Set(); const lineTexts = new Set(); activeThinkRuntimeCitationSources.forEach((state) => { + state.lineIdentities.forEach(identity => lineIdentities.add(identity)); state.lineKeys.forEach(key => lineKeys.add(key)); state.lineTexts.forEach(line => lineTexts.add(line)); }); return { + lineIdentities, lineKeys, lineTexts, }; @@ -1481,6 +1611,10 @@ ".jin-avatar-orbit[data-runtime-line-key], .jin-avatar-counter-orbit[data-runtime-line-key], .jin-avatar-memory-dash" ) ).forEach((orbitGroup) => { + const lineIdentity = + normalizeRuntimeCitationIdentity( + orbitGroup.dataset.runtimeLineIdentity + ); const lineKey = normalizeRuntimeCitationIdentity( orbitGroup.dataset.runtimeLineKey @@ -1490,8 +1624,12 @@ orbitGroup.dataset.runtimeLineText ); const cited = - (lineKey && activeIdentities.lineKeys.has(lineKey)) - || (lineText && activeIdentities.lineTexts.has(lineText)); + lineIdentity + ? activeIdentities.lineIdentities.has(lineIdentity) + : ( + (lineKey && activeIdentities.lineKeys.has(lineKey)) + || (lineText && activeIdentities.lineTexts.has(lineText)) + ); orbitGroup.classList.toggle( "is-runtime-cited", @@ -1501,11 +1639,29 @@ } function getActiveMemoryReferenceText() { - return ( - memoryReferenceHighlightState.hoverText - || memoryReferenceHighlightState.persistentText - || "" - ); + return memoryReferenceHighlightState.persistentText || ""; + } + + function buildAvatarMemoryReferenceAliasUsage(nodes) { + const usage = new Map(); + + nodes.forEach((node) => { + getAvatarMemoryReferenceAliases(node).forEach((alias) => { + const identity = + normalizeRuntimeCitationIdentity(alias); + + if (!identity) { + return; + } + + usage.set( + identity, + Number(usage.get(identity) || 0) + 1 + ); + }); + }); + + return usage; } function applyMemoryReferenceGlow() { @@ -1516,13 +1672,24 @@ } const sourceText = getActiveMemoryReferenceText(); + const recordNodes = Array.from( + svg.querySelectorAll("[data-memory-reference-aliases]") + ); + const aliasUsage = + buildAvatarMemoryReferenceAliasUsage(recordNodes); - svg.querySelectorAll("[data-memory-reference-aliases]") - .forEach((recordNode) => { + recordNodes.forEach((recordNode) => { const matched = Boolean( sourceText && getAvatarMemoryReferenceAliases(recordNode) - .some(alias => containsMemoryReference(sourceText, alias)) + .some(alias => ( + Number( + aliasUsage.get( + normalizeRuntimeCitationIdentity(alias) + ) || 0 + ) === 1 + && containsMemoryReference(sourceText, alias) + )) ); recordNode.classList.toggle( @@ -1534,21 +1701,22 @@ function handleMemoryReferenceHighlight(event) { const detail = event && event.detail || {}; - const source = detail.source === "hover" - ? "hover" - : "persistent"; - const stateKey = source === "hover" - ? "hoverText" - : "persistentText"; - - memoryReferenceHighlightState[stateKey] = + + if (detail.source !== "persistent") { + return; + } + + memoryReferenceHighlightState.persistentText = detail.active === false ? "" : String(detail.text || ""); + // The newest JIN response replaces the previous turn's citation glow. + activeThinkRuntimeCitationSources.clear(); + applyMemoryReferenceGlow(); + applyThinkRuntimeCitationGlow(); } - function buildRenderSignature( snapshot, lines, @@ -1642,6 +1810,7 @@ avatarRoot.style.setProperty("--jin-avatar-center-color", centerColor); applyThinkRuntimeCitationGlow(); applyMemoryReferenceGlow(); + applyMemoryRowAvatarHoverGlow(); lastRenderSignature = signature; } @@ -1904,12 +2073,21 @@ handleMemoryReferenceHighlight ); - window.addEventListener(THINK_RUNTIME_CITATION_HOVER_EVENT, (event) => { + window.addEventListener(MEMORY_ROW_AVATAR_HOVER_EVENT, (event) => { + memoryRowAvatarHoverState = + normalizeMemoryRowAvatarHoverDetail( + event && event.detail || {} + ); + + applyMemoryRowAvatarHoverGlow(); + }); + + window.addEventListener(THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT, (event) => { const detail = event && event.detail || {}; const sourceId = String(detail.sourceId || "unknown-think"); const state = - normalizeThinkRuntimeCitationHoverDetail(detail); + normalizeThinkRuntimeCitationHighlightDetail(detail); if (state) { activeThinkRuntimeCitationSources.set( diff --git a/ui/static/js/runtime/runtime-core.js b/ui/static/js/runtime/runtime-core.js index c0aa764e..9d23fc4a 100644 --- a/ui/static/js/runtime/runtime-core.js +++ b/ui/static/js/runtime/runtime-core.js @@ -15,6 +15,47 @@ .trim(); } + function buildCitationRecordIdentity( + id, + key, + value + ) { + const parts = [ + id, + key, + value, + ].map(normalizeCitationIdentity); + + if (parts.some(part => !part)) { + return ""; + } + + // Keep the full normalized tuple instead of a lossy short hash. This is + // still a compact identity token in the UI, but cannot collapse two L4 + // records merely because they share the same human-readable key. + return JSON.stringify(parts); + } + + function buildAvatarMemoryHoverId( + kind, + id + ) { + const normalizedKind = + normalizeCitationIdentity(kind); + const normalizedId = + normalizeCitationIdentity(id); + + if (!normalizedKind || !normalizedId) { + return ""; + } + + return `${normalizedKind}:${normalizedId}`; + } + root.normalizeCitationIdentity = normalizeCitationIdentity; + root.buildCitationRecordIdentity = + buildCitationRecordIdentity; + root.buildAvatarMemoryHoverId = + buildAvatarMemoryHoverId; }()); diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index ed02b23a..4a286c45 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -44,7 +44,25 @@ return Number.isFinite(number) ? number : fallback; } - function normalizeFact(value, pending) { + function normalizeFactId(value, pending) { + const text = normalizeText(value).toUpperCase(); + const pattern = pending ? /^PF([1-9]\d*)$/ : /^F([1-9]\d*)$/; + const match = text.match(pattern); + if (!match) { + return ""; + } + return `${pending ? "PF" : "F"}${Number(match[1])}`; + } + + function factIdNumber(value, pending) { + const id = normalizeFactId(value, pending); + if (!id) { + return 0; + } + return Number(id.slice(pending ? 2 : 1)) || 0; + } + + function normalizeFact(value, pending, assignedId) { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } @@ -55,14 +73,13 @@ return null; } - const idPrefix = pending ? "l4p_" : "l4_"; - const rawId = normalizeText(value.id); - const generatedId = storage.buildFactsMemoryContentHash( - `${key}\n${factValue}` - ).replace(/^h/, ""); + const id = normalizeFactId(assignedId || value.id, pending); + if (!id) { + return null; + } return { - id: rawId.startsWith(idPrefix) ? rawId : `${idPrefix}${generatedId}`, + id, key, value: factValue, category: normalizeText(value.category || "other") || "other", @@ -85,45 +102,161 @@ }; } - function normalizeFacts(values, pending) { - const result = []; - const seen = new Set(); + function migrateStoreIds(value) { + const source = value && typeof value === "object" && !Array.isArray(value) + ? value + : {}; + const rawFacts = Array.isArray(source.facts) ? source.facts : []; + const rawPending = Array.isArray(source.pending_facts) + ? source.pending_facts + : []; + const rawDeleted = Array.isArray(source.deleted_fact_ids) + ? source.deleted_fact_ids + : []; + + const usedFacts = new Set(); + const usedPending = new Set(); + rawFacts.forEach((fact) => { + const number = factIdNumber(fact && fact.id, false); + if (number) usedFacts.add(number); + }); + rawPending.forEach((fact) => { + const number = factIdNumber(fact && fact.id, true); + if (number) usedPending.add(number); + }); - (Array.isArray(values) ? values : []).forEach((value) => { - const fact = normalizeFact(value, pending); - if (!fact || seen.has(fact.id)) { + let nextFact = Math.max( + 1, + Math.floor(normalizeNumber(source.next_fact_id, 1)), + Math.max(0, ...usedFacts) + 1 + ); + let nextPending = Math.max( + 1, + Math.floor(normalizeNumber(source.next_pending_fact_id, 1)), + Math.max(0, ...usedPending) + 1 + ); + const idMap = new Map(); + let migrated = Number(source.version || 0) < 2; + + function allocate(rawId, pending) { + const text = normalizeText(rawId); + const current = normalizeFactId(text, pending); + if (current) { + return current; + } + if (text && idMap.has(text)) { + return idMap.get(text); + } + + const legacy = pending + ? /^l4p_[a-z0-9_-]+$/i.test(text) + : /^l4_[a-z0-9_-]+$/i.test(text); + if (text && !legacy) { + return ""; + } + + const used = pending ? usedPending : usedFacts; + let number = pending ? nextPending : nextFact; + while (used.has(number)) number += 1; + used.add(number); + const id = `${pending ? "PF" : "F"}${number}`; + if (pending) nextPending = number + 1; + else nextFact = number + 1; + if (text) idMap.set(text, id); + migrated = true; + return id; + } + + const facts = []; + rawFacts.forEach((rawFact) => { + if (!rawFact || typeof rawFact !== "object" || Array.isArray(rawFact)) { return; } - seen.add(fact.id); - result.push(fact); + const oldId = normalizeText(rawFact.id); + const id = allocate(oldId, false) || allocate("", false); + if (oldId && oldId !== id) idMap.set(oldId, id); + const fact = normalizeFact(rawFact, false, id); + if (fact) facts.push(fact); }); - return result; - } + const pendingFacts = []; + rawPending.forEach((rawFact) => { + if (!rawFact || typeof rawFact !== "object" || Array.isArray(rawFact)) { + return; + } + const oldId = normalizeText(rawFact.id); + const id = allocate(oldId, true) || allocate("", true); + if (oldId && oldId !== id) idMap.set(oldId, id); + const fact = normalizeFact(rawFact, true, id); + if (fact) pendingFacts.push(fact); + }); - function normalizeStore(value) { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return { - version: 1, - revision: 0, - updated_at: "", - facts: [], - pending_facts: [], - }; + const deletedFactIds = []; + rawDeleted.forEach((rawId) => { + const text = normalizeText(rawId); + const id = normalizeFactId(text, false) + || idMap.get(text) + || (/^l4_[a-z0-9_-]+$/i.test(text) ? allocate(text, false) : ""); + if (id && !deletedFactIds.includes(id)) deletedFactIds.push(id); + }); + + function remapSourceIds(fact) { + fact.source_fact_ids = normalizeList(fact.source_fact_ids) + .map((rawId) => { + return normalizeFactId(rawId, true) + || normalizeFactId(rawId, false) + || idMap.get(rawId) + || (/^l4p_[a-z0-9_-]+$/i.test(rawId) ? allocate(rawId, true) : "") + || (/^l4_[a-z0-9_-]+$/i.test(rawId) ? allocate(rawId, false) : ""); + }) + .filter(Boolean); } + facts.forEach(remapSourceIds); + pendingFacts.forEach(remapSourceIds); return { - version: 1, + version: 2, revision: Math.max( 0, - Math.floor(normalizeNumber(value.revision, 0)) - ), - updated_at: normalizeText(value.updated_at), - facts: normalizeFacts(value.facts, false), - pending_facts: normalizeFacts(value.pending_facts, true), + Math.floor(normalizeNumber(source.revision, 0)) + ) + (migrated ? 1 : 0), + updated_at: normalizeText(source.updated_at), + facts, + pending_facts: pendingFacts, + deleted_fact_ids: deletedFactIds, + next_fact_id: nextFact, + next_pending_fact_id: nextPending, }; } + function normalizeStore(value) { + const migrated = migrateStoreIds(value); + const seenFacts = new Set(); + const seenPending = new Set(); + + migrated.facts = migrated.facts.filter((fact) => { + if (seenFacts.has(fact.id)) return false; + seenFacts.add(fact.id); + return !migrated.deleted_fact_ids.includes(fact.id); + }); + const processedPendingIds = new Set(); + migrated.facts.forEach((fact) => { + normalizeList(fact.source_fact_ids).forEach((sourceId) => { + const pendingId = normalizeFactId(sourceId, true); + if (pendingId) processedPendingIds.add(pendingId); + }); + }); + migrated.pending_facts = migrated.pending_facts.filter((fact) => { + if (seenPending.has(fact.id) || processedPendingIds.has(fact.id)) { + return false; + } + seenPending.add(fact.id); + return true; + }); + + return migrated; + } + function readStore() { return normalizeStore( storage.readBrowserMemory(longTermFactsStorageKey) diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 91169ab6..c7639bb9 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -21,19 +21,26 @@ const ACTIVE_MEMORY_PAUSE_HOLD_MS = 500; const MEMORY_DELETE_HOLD_MS = 1500; - const THINK_RUNTIME_CITATION_HOVER_EVENT = "jin:think-runtime-citation-hover"; - const RUNTIME_MEMORY_LINE_HOVER_SOURCE_ID = "runtime-memory-line-hover"; - const DELAYED_MEMORY_ROW_HOVER_SOURCE_ID = "delayed-memory-row-hover"; + const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = "jin:think-runtime-citation-highlight"; + const MEMORY_ROW_AVATAR_HOVER_EVENT = "jin:memory-row-avatar-hover"; const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; + const buildCitationRecordIdentity = + typeof window.JinRuntime.buildCitationRecordIdentity === "function" + ? window.JinRuntime.buildCitationRecordIdentity + : () => ""; + const buildAvatarMemoryHoverId = + typeof window.JinRuntime.buildAvatarMemoryHoverId === "function" + ? window.JinRuntime.buildAvatarMemoryHoverId + : () => ""; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = "jin:memory-reference-highlight"; const MEMORY_REFERENCE_ALIAS_DATASET_KEY = "memoryReferenceAliases"; const memoryReferenceHighlightState = { persistentText: "", - hoverText: "", }; + const activeThinkMemoryCitationSources = new Map(); let memoryReferenceEventsBound = false; @@ -230,14 +237,40 @@ return aliases; } + function extractActiveMemoryId(value) { + const match = + String(value || "") + .match( + /\[\s*active_memory_id\s*:\s*([a-z0-9]{6})\s*\]/i + ); + + return match + ? String(match[1] || "").trim().toLowerCase() + : ""; + } + function collectMemoryRecordReferenceAliases(record) { if (!record || typeof record !== "object") { return []; } + const key = + String(record.key || "").trim(); + const displayKey = + key + && memoryModel + && memoryModel.runtimeMemoryDisplay + && typeof memoryModel.runtimeMemoryDisplay.convertKeyToName === "function" + ? memoryModel.runtimeMemoryDisplay.convertKeyToName(key) + : ""; + return normalizeMemoryReferenceAliases([ - record.key, + key, + displayKey, + record.title, + record.name, record.id, + record._storage_key, record.active_memory_id, ...collectMemoryMetadataReferenceAliases( record.value @@ -294,11 +327,29 @@ } function getActiveMemoryReferenceText() { - return ( - memoryReferenceHighlightState.hoverText - || memoryReferenceHighlightState.persistentText - || "" - ); + return memoryReferenceHighlightState.persistentText || ""; + } + + function buildMemoryReferenceAliasUsage(rows) { + const usage = new Map(); + + rows.forEach((row) => { + getMemoryReferenceAliases(row).forEach((alias) => { + const identity = + normalizeMemoryReferenceSearchText(alias).trim(); + + if (!identity) { + return; + } + + usage.set( + identity, + Number(usage.get(identity) || 0) + 1 + ); + }); + }); + + return usage; } function applyMemoryReferenceHighlights() { @@ -308,16 +359,25 @@ const sourceText = getActiveMemoryReferenceText(); - - runtimeMemoryText - .querySelectorAll( + const rows = Array.from( + runtimeMemoryText.querySelectorAll( `[data-memory-reference-aliases]` ) - .forEach((row) => { + ); + const aliasUsage = + buildMemoryReferenceAliasUsage(rows); + + rows.forEach((row) => { const matched = Boolean( sourceText && getMemoryReferenceAliases(row) .some(alias => ( + Number( + aliasUsage.get( + normalizeMemoryReferenceSearchText(alias).trim() + ) || 0 + ) === 1 + && containsMemoryReference( sourceText, alias @@ -330,27 +390,193 @@ matched ); }); + + applyThinkMemoryCitationHighlights(); + } + + function sortHighlightedMemoryRows() { + if (!runtimeMemoryText) { + return; + } + + const rows = Array.from( + runtimeMemoryText.querySelectorAll( + ".runtime-memory-line:not(.runtime-memory-user-idle)" + ) + ); + + if (rows.length < 2) { + return; + } + + rows.forEach((row, index) => { + if (row.dataset.memoryHighlightSortIndex === undefined) { + row.dataset.memoryHighlightSortIndex = String(index); + } + }); + + const sortedRows = rows + .slice() + .sort((left, right) => { + const leftHighlighted = + left.classList.contains("runtime-memory-reference-hit") + || left.classList.contains("runtime-memory-citation-hit"); + const rightHighlighted = + right.classList.contains("runtime-memory-reference-hit") + || right.classList.contains("runtime-memory-citation-hit"); + + if (leftHighlighted !== rightHighlighted) { + return leftHighlighted ? -1 : 1; + } + + return ( + Number(left.dataset.memoryHighlightSortIndex || 0) + - Number(right.dataset.memoryHighlightSortIndex || 0) + ); + }); + + const orderChanged = sortedRows.some( + (row, index) => row !== rows[index] + ); + + if (!orderChanged) { + return; + } + + sortedRows.forEach( + row => runtimeMemoryText.appendChild(row) + ); + + const userIdleRow = + runtimeMemoryText.querySelector(".runtime-memory-user-idle"); + + if (userIdleRow) { + runtimeMemoryText.appendChild(userIdleRow); + } + } + + function getActiveThinkMemoryCitationIdentitySets() { + const lineIdentities = new Set(); + const lineKeys = new Set(); + const lineTexts = new Set(); + + activeThinkMemoryCitationSources.forEach((state) => { + state.lineIdentities.forEach(identity => lineIdentities.add(identity)); + state.lineKeys.forEach(key => lineKeys.add(key)); + state.lineTexts.forEach(text => lineTexts.add(text)); + }); + + return { + lineIdentities, + lineKeys, + lineTexts, + }; + } + + function applyThinkMemoryCitationHighlights() { + if (!runtimeMemoryText) { + return; + } + + const activeIdentities = + getActiveThinkMemoryCitationIdentitySets(); + + runtimeMemoryText + .querySelectorAll( + "[data-runtime-memory-line-key]" + ) + .forEach((row) => { + const lineIdentity = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineIdentity + ); + const lineKey = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineKey + ); + const lineText = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineText + ); + const matched = + lineIdentity + ? activeIdentities.lineIdentities.has(lineIdentity) + : ( + (lineKey && activeIdentities.lineKeys.has(lineKey)) + || (lineText && activeIdentities.lineTexts.has(lineText)) + ); + + row.classList.toggle( + "runtime-memory-citation-hit", + Boolean(matched) + ); + }); + + sortHighlightedMemoryRows(); + } + + function handleThinkMemoryCitationHighlight(event) { + const detail = event && event.detail || {}; + const sourceId = + String(detail.sourceId || "unknown-memory-citation"); + + if (detail.active !== true) { + activeThinkMemoryCitationSources.delete(sourceId); + applyThinkMemoryCitationHighlights(); + return; + } + + const lineIdentities = new Set( + (Array.isArray(detail.lineIdentities) ? detail.lineIdentities : []) + .map(normalizeRuntimeCitationIdentity) + .filter(Boolean) + ); + const lineKeys = new Set( + (Array.isArray(detail.lineKeys) ? detail.lineKeys : []) + .map(normalizeRuntimeCitationIdentity) + .filter(Boolean) + ); + const lineTexts = new Set( + (Array.isArray(detail.lineTexts) ? detail.lineTexts : []) + .map(normalizeRuntimeCitationIdentity) + .filter(Boolean) + ); + + if ( + !lineIdentities.size + && !lineKeys.size + && !lineTexts.size + ) { + activeThinkMemoryCitationSources.delete(sourceId); + } else { + activeThinkMemoryCitationSources.set( + sourceId, + { lineIdentities, lineKeys, lineTexts } + ); + } + + applyThinkMemoryCitationHighlights(); } function handleMemoryReferenceHighlight(event) { const detail = event && event.detail || {}; - const source = - detail.source === "hover" - ? "hover" - : "persistent"; - const stateKey = - source === "hover" - ? "hoverText" - : "persistentText"; - - memoryReferenceHighlightState[stateKey] = + + if (detail.source !== "persistent") { + return; + } + + memoryReferenceHighlightState.persistentText = detail.active === false ? "" : String(detail.text || ""); + // A new JIN response owns the citation state for the turn. + // Drop structured hits from the previous response before the new + // reasoning analysis publishes its own exact runtime-line matches. + activeThinkMemoryCitationSources.clear(); + applyMemoryReferenceHighlights(); } - function bindMemoryReferenceHighlightEvents() { if (memoryReferenceEventsBound) { return; @@ -360,6 +586,10 @@ MEMORY_REFERENCE_HIGHLIGHT_EVENT, handleMemoryReferenceHighlight ); + window.addEventListener( + THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT, + handleThinkMemoryCitationHighlight + ); memoryReferenceEventsBound = true; } @@ -1005,7 +1235,48 @@ runtimeMemoryHistory.snapshots.length - 1; } + let lastRuntimeAvatarSnapshotDispatchSignature = null; + + function buildRuntimeAvatarSnapshotDispatchSignature(snapshot) { + if (!snapshot || typeof snapshot !== "object") { + return "empty"; + } + + const lines = Array.isArray(snapshot.lines) + ? snapshot.lines + : []; + + return JSON.stringify({ + runtime_memory_id: snapshot.runtime_memory_id || null, + index: runtimeMemoryHistory + ? runtimeMemoryHistory.index + : -1, + total_diff: snapshot.total_diff || 0, + raw_memory: String(snapshot.raw_memory || ""), + lines: lines.map(line => ({ + id: line && line.id || "", + active_memory_id: line && line.active_memory_id || "", + key: line && line.key || "", + value: line && line.value || "", + status: line && line.status || "", + key_status: line && line.key_status || "", + value_status: line && line.value_status || "", + key_change_ratio: Number(line && line.key_change_ratio || 0), + value_change_ratio: Number(line && line.value_change_ratio || 0), + })), + }); + } + function dispatchRuntimeAvatarSnapshot(snapshot) { + const signature = + buildRuntimeAvatarSnapshotDispatchSignature(snapshot); + + if (signature === lastRuntimeAvatarSnapshotDispatchSignature) { + return; + } + + lastRuntimeAvatarSnapshotDispatchSignature = signature; + window.dispatchEvent( new CustomEvent("jin:runtime-avatar-snapshot", { detail: { @@ -1024,6 +1295,7 @@ function renderRuntimeMemorySnapshot(options = {}) { requireRuntimeMemoryHistory(); clearRuntimeMemoryLineAvatarHover(); + clearDelayedMemoryAvatarHover(); clampRuntimeMemoryHistoryIndex(); ensureRuntimeMemoryDisplayModeAvailable(); updateRuntimeMemoryTitleState(); @@ -1278,42 +1550,37 @@ return true; } + function dispatchMemoryRowAvatarHover(detail) { + window.dispatchEvent( + new CustomEvent( + MEMORY_ROW_AVATAR_HOVER_EVENT, + { + detail: detail || { + active: false, + }, + } + ) + ); + } + function dispatchRuntimeMemoryLineAvatarHover( row, active ) { - const lineKey = - row - ? normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineKey - ) - : ""; - const lineText = + const avatarMemoryHoverId = row - ? normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineText - ) + ? String(row.dataset.avatarMemoryHoverId || "").trim() : ""; - window.dispatchEvent( - new CustomEvent( - THINK_RUNTIME_CITATION_HOVER_EVENT, - { - detail: active && (lineKey || lineText) - ? { - active: true, - sourceId: RUNTIME_MEMORY_LINE_HOVER_SOURCE_ID, - lineKeys: lineKey ? [lineKey] : [], - lineTexts: lineText ? [lineText] : [], - } - : { - active: false, - sourceId: RUNTIME_MEMORY_LINE_HOVER_SOURCE_ID, - lineKeys: [], - lineTexts: [], - }, + dispatchMemoryRowAvatarHover( + active && avatarMemoryHoverId + ? { + active: true, + avatarMemoryHoverId, + } + : { + active: false, } - ) ); } @@ -1328,33 +1595,30 @@ report, active ) { - const memoryId = - normalizeRuntimeCitationIdentity( + const avatarMemoryHoverId = + buildAvatarMemoryHoverId( + "delayed", report && report._storage_key ); - window.dispatchEvent( - new CustomEvent( - THINK_RUNTIME_CITATION_HOVER_EVENT, - { - detail: active && memoryId - ? { - active: true, - sourceId: `${DELAYED_MEMORY_ROW_HOVER_SOURCE_ID}:${memoryId}`, - lineKeys: [memoryId], - lineTexts: [], - } - : { - active: false, - sourceId: `${DELAYED_MEMORY_ROW_HOVER_SOURCE_ID}:${memoryId || "none"}`, - lineKeys: [], - lineTexts: [], - }, + dispatchMemoryRowAvatarHover( + active && avatarMemoryHoverId + ? { + active: true, + avatarMemoryHoverId, + } + : { + active: false, } - ) ); } + function clearDelayedMemoryAvatarHover() { + dispatchMemoryRowAvatarHover({ + active: false, + }); + } + function renderRuntimeMemoryLines( snapshot, persistGlow = false, @@ -1376,11 +1640,21 @@ const showLiveUserIdle = isLatestRuntimeMemorySnapshot(); + const sourceLines = + (snapshot.lines || []) + .map((line, sourceIndex) => ({ + ...line, + avatar_memory_hover_id: + buildAvatarMemoryHoverId( + "runtime", + line && line.id || `line-${sourceIndex}` + ), + })); const lines = showLiveUserIdle - ? (snapshot.lines || []) + ? sourceLines .filter(line => !memoryModel.isUserIdleRuntimeMemoryLine(line)) - : snapshot.lines || []; + : sourceLines; if (!lines.length) { const rawMemory = @@ -1439,6 +1713,26 @@ row.dataset.runtimeMemoryLineIndex = String(index); + row.dataset.memoryHighlightSortIndex = + String(index); + const avatarMemoryHoverId = + String( + line && line.avatar_memory_hover_id || "" + ).trim(); + + if (avatarMemoryHoverId) { + row.dataset.avatarMemoryHoverId = + avatarMemoryHoverId; + } + const lineIdentity = + normalizeRuntimeCitationIdentity( + line.citation_identity + ); + + if (lineIdentity) { + row.dataset.runtimeMemoryLineIdentity = + lineIdentity; + } row.dataset.runtimeMemoryLineKey = normalizeRuntimeCitationIdentity( line.key || "note" @@ -2007,7 +2301,7 @@ "title" ); - reports.forEach((report) => { + reports.forEach((report, index) => { const title = String(report.title || "").trim(); @@ -2019,6 +2313,8 @@ row.className = "runtime-memory-line runtime-memory-delayed-row"; + row.dataset.memoryHighlightSortIndex = + String(index); if (Boolean(report.pinned)) { row.classList.add( @@ -2062,12 +2358,16 @@ normalizeRuntimeCitationIdentity( report._storage_key ); + row.dataset.avatarMemoryHoverId = + buildAvatarMemoryHoverId( + "delayed", + report._storage_key + ); setMemoryReferenceAliases( row, - [ - report._storage_key, - report.id, - ] + collectMemoryRecordReferenceAliases( + report + ) ); row.appendChild( @@ -2828,18 +3128,31 @@ fact ) { + const id = + String(fact.id || "").trim(); const key = String(fact.key || "").trim(); const value = String(fact.value || "").trim(); return { - id: String(fact.id || "").trim(), + id, key, value: memoryModel.appendProperties( value, formatLongTermFactMetadata(fact) ), + avatar_memory_hover_id: + buildAvatarMemoryHoverId( + "l4", + id + ), + citation_identity: + buildCitationRecordIdentity( + id, + key, + value + ), status: "same", key_status: "same", value_status: "same", @@ -2917,7 +3230,15 @@ ); appendRuntimeMemoryLineRows( - records.map(memoryModel.parseRuntimeMemoryLine), + records.map((record, index) => ({ + ...memoryModel.parseRuntimeMemoryLine(record), + avatar_memory_hover_id: + buildAvatarMemoryHoverId( + "active", + extractActiveMemoryId(record) + || `record-${index}` + ), + })), false, { interactiveActiveMemory: true, diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 78ce60fc..c2cef0df 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1123,10 +1123,10 @@ .split(/[\s,;]+/) .forEach(function (candidate) { const factId = - String(candidate || "").trim().toLowerCase(); + String(candidate || "").trim().toUpperCase(); if ( - !/^l4_[a-z0-9_-]+$/.test(factId) + !/^F[1-9]\d*$/.test(factId) || seen.has(factId) ) { return; diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 81b23b5d..9dafe126 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -25,11 +25,13 @@ const websocketClientId = const websocketReconnectBaseDelay = 700; const websocketReconnectMaxDelay = 5000; +const websocketReconnectMaxAttempts = 3; let websocketHasOpened = false; let ws = null; let websocketReconnectTimer = null; let websocketReconnectAttempts = 0; +let websocketReconnectAwaitingFocus = false; let websocketDisconnectedLogged = false; let persistedSessionBootstrapSent = false; let generationRunning = false; @@ -190,7 +192,22 @@ function clearWebSocketReconnectTimer() { function scheduleWebSocketReconnect() { - if (websocketReconnectTimer) { + if ( + websocketReconnectTimer + || isWebSocketOpen() + || ( + ws + && ws.readyState === WebSocket.CONNECTING + ) + ) { + return; + } + + if ( + websocketReconnectAttempts + >= websocketReconnectMaxAttempts + ) { + websocketReconnectAwaitingFocus = true; return; } @@ -206,6 +223,17 @@ function scheduleWebSocketReconnect() { websocketReconnectTimer = setTimeout( function () { websocketReconnectTimer = null; + + if ( + isWebSocketOpen() + || ( + ws + && ws.readyState === WebSocket.CONNECTING + ) + ) { + return; + } + connectWebSocket(); }, delay @@ -439,6 +467,7 @@ async function handleSocketOpen() { clearWebSocketReconnectTimer(); websocketReconnectAttempts = 0; + websocketReconnectAwaitingFocus = false; websocketDisconnectedLogged = false; const isSoftReconnect = @@ -620,35 +649,87 @@ function connectWebSocket() { || ws.readyState === WebSocket.CONNECTING ) ) { - return; + return false; } - ws = + const socket = new WebSocket( buildWebSocketUrl() ); - ws.onmessage = + ws = socket; + + socket.onmessage = handleSocketMessage; - ws.onopen = - handleSocketOpen; + socket.onopen = function () { + if (ws !== socket) { + return; + } - ws.onclose = - handleSocketClose; + void handleSocketOpen(); + }; + + socket.onclose = function () { + if (ws !== socket) { + return; + } + + ws = null; + handleSocketClose(); + }; - ws.onerror = function () { + socket.onerror = function () { clearInterruptedRuntimeGlow(); - if (ws) { - ws.close(); + if (ws === socket) { + socket.close(); } }; + return true; + } window.connectWebSocket = connectWebSocket; +function retryWebSocketOnFocus() { + + if ( + isWebSocketOpen() + || ( + ws + && ws.readyState === WebSocket.CONNECTING + ) + ) { + return; + } + + if (!websocketReconnectAwaitingFocus) { + return; + } + + clearWebSocketReconnectTimer(); + websocketReconnectAttempts = 0; + websocketReconnectAwaitingFocus = false; + connectWebSocket(); + +} + +window.addEventListener( + "focus", + retryWebSocketOnFocus +); + +document.addEventListener( + "visibilitychange", + function () { + if (!document.hidden) { + retryWebSocketOnFocus(); + } + } +); + function initializeSocketClient() { if (socketClientInitialized) { diff --git a/ui/static/js/status.js b/ui/static/js/status.js index 25b9ed21..55b8e097 100644 --- a/ui/static/js/status.js +++ b/ui/static/js/status.js @@ -211,8 +211,43 @@ async function updateRuntime(options = {}) { } +function runtimeStatusIsHealthy() { + + const runtimeConfig = + window.jinRuntimeConfig; + + if ( + !runtimeConfig + || !runtimeConfig.runtimeStatus + ) { + return false; + } + + const status = + runtimeConfig.runtimeStatus; + + return Boolean( + status.service + && ( + runtimeConfig.useServiceAsBrain + || status.brain + ) + ); + +} + function refreshRuntimeStatus() { + // Focus/visibility changes are not a polling loop. If the WebSocket is + // already connected and the latest model status is healthy, there is + // nothing to probe and /api/status would only hit LM Studio again. + if ( + window.jinWebSocketConnected === true + && runtimeStatusIsHealthy() + ) { + return; + } + if ( Date.now() - lastRuntimeStatusStartedAt < STATUS_REFRESH_COOLDOWN_MS diff --git a/ui/static/js/think-citations.js b/ui/static/js/think-citations.js index 442a1dad..460f47b2 100644 --- a/ui/static/js/think-citations.js +++ b/ui/static/js/think-citations.js @@ -4,9 +4,14 @@ const THINK_RULE_CITATIONS_ENDPOINT = "/api/debug/rule-citations"; const THINK_RULE_WORKER_URL = - "/static/js/think-rule-worker.js?v=rule-citations-4"; - const THINK_RUNTIME_CITATION_HOVER_EVENT = - "jin:think-runtime-citation-hover"; + "/static/js/think-rule-worker.js?v=rule-citations-6"; + const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = + "jin:think-runtime-citation-highlight"; + const buildCitationRecordIdentity = + window.JinRuntime + && typeof window.JinRuntime.buildCitationRecordIdentity === "function" + ? window.JinRuntime.buildCitationRecordIdentity + : () => ""; let thinkRuleCitationWorker = null; let thinkRuleCitationRegistryPromise = null; @@ -127,12 +132,26 @@ match && match.sourceType === "rule" ) { - return 2; + return 4; } if ( match && match.sourceType === "runtime" + ) { + return 3; + } + + if ( + match + && match.sourceType === "active" + ) { + return 2; + } + + if ( + match + && match.sourceType === "l4" ) { return 1; } @@ -249,9 +268,13 @@ const label = match.sourceType === "runtime" ? "RUNTIME" - : match.sourceType === "session" - ? "SESSION" - : "RULE"; + : match.sourceType === "active" + ? "ACTIVE" + : match.sourceType === "l4" + ? "L4" + : match.sourceType === "session" + ? "SESSION" + : "RULE"; return [ `${label} - ${match.constantName || "unknown"} - ${match.level || "match"} - ${score}%`, @@ -268,9 +291,13 @@ const sourceClass = match.sourceType === "runtime" ? "runtime" - : match.sourceType === "session" - ? "session" - : "rule"; + : match.sourceType === "active" + ? "active" + : match.sourceType === "l4" + ? "l4" + : match.sourceType === "session" + ? "session" + : "rule"; return [ "think-rule-hit", @@ -327,6 +354,7 @@ idPrefix, defaultConstantName, sourceSnapshotIndex = null, + sourceLineIdentity = "", } = options; const fragments = []; @@ -389,6 +417,7 @@ sourceSnapshotIndex, sourceLineKey: key || defaultConstantName, sourceLineText: line, + sourceLineIdentity, minScore: 0.72, } ); @@ -497,6 +526,111 @@ } + function buildActiveMemoryCitationFragments() { + + const runtimeApi = + window.JinRuntime + && window.JinRuntime.runtime; + + if ( + !runtimeApi + || typeof runtimeApi.getActiveMemoryRecords !== "function" + ) { + return []; + } + + const records = + runtimeApi.getActiveMemoryRecords(); + + if (!Array.isArray(records)) { + return []; + } + + return records.flatMap((record, index) => { + const memoryText = + String(record || "").trim(); + + if (!memoryText) { + return []; + } + + return buildMemoryCitationFragments( + memoryText, + { + source: `activeMemory[${index}]`, + sourceType: "active", + citationType: "active_memory_citation", + layer: "active", + idPrefix: `active:${index}`, + defaultConstantName: `active_memory_${index + 1}`, + } + ); + }); + + } + + function buildL4CitationFragments() { + + const l4Memory = + window.JinRuntime + && window.JinRuntime.l4Memory; + + if ( + !l4Memory + || typeof l4Memory.getFacts !== "function" + ) { + return []; + } + + const facts = + l4Memory.getFacts(); + + if (!Array.isArray(facts)) { + return []; + } + + return facts.flatMap((fact, index) => { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + ) { + return []; + } + + const id = + String(fact.id || "").trim(); + const key = + String(fact.key || "").trim(); + const value = + String(fact.value || fact.content || "").trim(); + const sourceLineIdentity = + buildCitationRecordIdentity( + id, + key, + value + ); + + if (!key || !value) { + return []; + } + + return buildMemoryCitationFragments( + `${key}: ${value}`, + { + source: `l4Fact[${id || index}]`, + sourceType: "l4", + citationType: "l4_citation", + layer: "l4", + idPrefix: `l4:${id || index}`, + defaultConstantName: key, + sourceLineIdentity, + } + ); + }); + + } + function buildSessionCitationFragments() { const storage = @@ -557,11 +691,16 @@ } - function buildThinkRuntimeCitationHoverState(matches) { + function buildThinkRuntimeCitationHighlightState(matches) { const runtimeMatches = (Array.isArray(matches) ? matches : []) - .filter(match => match && match.sourceType === "runtime"); + .filter(match => ( + match + && ["runtime", "active", "l4"].includes( + match.sourceType + ) + )); const lineKeys = Array.from(new Set( @@ -573,6 +712,15 @@ .filter(Boolean) )); + const lineIdentities = + Array.from(new Set( + runtimeMatches + .map(match => normalizeThinkRuntimeCitationIdentity( + match.sourceLineIdentity + )) + .filter(Boolean) + )); + const lineTexts = Array.from(new Set( runtimeMatches @@ -584,18 +732,23 @@ .filter(Boolean) )); - if (!lineKeys.length && !lineTexts.length) { + if ( + !lineIdentities.length + && !lineKeys.length + && !lineTexts.length + ) { return null; } return { + lineIdentities, lineKeys, lineTexts, }; } - function dispatchThinkRuntimeCitationHover( + function dispatchThinkRuntimeCitationHighlight( thinkContent, active ) { @@ -606,25 +759,27 @@ const state = active - ? thinkContent.__jinRuntimeCitationHoverState + ? thinkContent.__jinRuntimeCitationHighlightState : null; const sourceId = String(thinkContent.dataset.thinkId || "unknown-think"); window.dispatchEvent( new CustomEvent( - THINK_RUNTIME_CITATION_HOVER_EVENT, + THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT, { detail: state ? { active: true, sourceId, + lineIdentities: [...state.lineIdentities], lineKeys: [...state.lineKeys], lineTexts: [...state.lineTexts], } : { active: false, sourceId, + lineIdentities: [], lineKeys: [], lineTexts: [], }, @@ -636,28 +791,33 @@ function shouldRevealThinkRuntimeCitations(thinkContent) { - if ( - !thinkContent - || !thinkContent.__jinRuntimeCitationHoverState - ) { - return false; - } + return Boolean( + thinkContent + && thinkContent.__jinRuntimeCitationHighlightState + ); - const hovered = - typeof thinkContent.matches === "function" - && thinkContent.matches(":hover"); - const autoRevealing = - thinkContent.classList.contains( - "is-rule-highlight-revealing" - ); + } + function resetThinkCitationHighlightTurn() { - return hovered || autoRevealing; + document + .querySelectorAll( + ".jin-think-content.has-rule-highlights" + ) + .forEach((thinkContent) => { + thinkContent.classList.remove( + "has-rule-highlights" + ); - } + dispatchThinkRuntimeCitationHighlight( + thinkContent, + false + ); + }); + } function syncThinkRuntimeCitationHighlight(thinkContent) { - dispatchThinkRuntimeCitationHover( + dispatchThinkRuntimeCitationHighlight( thinkContent, shouldRevealThinkRuntimeCitations( thinkContent @@ -790,8 +950,8 @@ job.matches = matches; - element.__jinRuntimeCitationHoverState = - buildThinkRuntimeCitationHoverState( + element.__jinRuntimeCitationHighlightState = + buildThinkRuntimeCitationHighlightState( matches ); @@ -803,54 +963,6 @@ } - function pulseThinkRuleHighlights(job) { - - const element = - job.element; - - if ( - !element - || element.dataset.thinkId !== job.thinkId - ) { - return; - } - - if (element.__jinThinkRulePulseTimer) { - clearTimeout( - element.__jinThinkRulePulseTimer - ); - } - - element.classList.remove( - "is-rule-highlight-revealing" - ); - - void element.offsetWidth; - - element.classList.add( - "is-rule-highlight-revealing" - ); - - syncThinkRuntimeCitationHighlight( - element - ); - - element.__jinThinkRulePulseTimer = setTimeout( - () => { - element.classList.remove( - "is-rule-highlight-revealing" - ); - element.__jinThinkRulePulseTimer = null; - - syncThinkRuntimeCitationHighlight( - element - ); - }, - 5000 - ); - - } - function handleThinkRuleWorkerMessage(event) { const data = @@ -887,17 +999,9 @@ data.type === "ruleMatchesDone" ) { job.done = true; - if ( - renderThinkRuleHighlights( - job - ) - ) { - requestAnimationFrame( - () => pulseThinkRuleHighlights( - job - ) - ); - } + renderThinkRuleHighlights( + job + ); activeThinkRuleCitationJobs.delete( thinkId ); @@ -985,6 +1089,8 @@ ...buildRuntimeCitationFragments( currentJob.runtimeCitationIndex ), + ...buildActiveMemoryCitationFragments(), + ...buildL4CitationFragments(), ...buildSessionCitationFragments(), ]; @@ -1018,6 +1124,7 @@ } window.JinThinkCitations = { + resetThinkCitationHighlightTurn, startThinkRuleCitationAnalysis, syncThinkRuntimeCitationHighlight, }; diff --git a/ui/static/js/think-rule-worker.js b/ui/static/js/think-rule-worker.js index e2cf132c..0b70167e 100644 --- a/ui/static/js/think-rule-worker.js +++ b/ui/static/js/think-rule-worker.js @@ -321,6 +321,7 @@ function buildMatch( sourceSnapshotIndex: fragment.sourceSnapshotIndex, sourceLineKey: fragment.sourceLineKey, sourceLineText: fragment.sourceLineText, + sourceLineIdentity: fragment.sourceLineIdentity, }; } @@ -331,12 +332,26 @@ function sourcePriority( match && match.sourceType === "rule" ) { - return 2; + return 4; } if ( match && match.sourceType === "runtime" + ) { + return 3; + } + + if ( + match + && match.sourceType === "active" + ) { + return 2; + } + + if ( + match + && match.sourceType === "l4" ) { return 1; } diff --git a/ui/templates/index.html b/ui/templates/index.html index a5b9bd48..9d798c6f 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -11,13 +11,13 @@ - + - + - - + + @@ -498,7 +498,7 @@ "runtimeConfig": runtime_config, } | tojson }} - + @@ -506,23 +506,23 @@ - + - - + + - + - + - + - + diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 3c22bf04..81fdf48b 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -4,10 +4,7 @@ from .delayed_memory_utils import generate_delayed_memory_report_id -LONG_TERM_FACT_ID_RE = re.compile( - r"^l4_[a-z0-9_-]+$", - re.IGNORECASE, -) +LONG_TERM_FACT_ID_RE = re.compile(r"^F[1-9]\d*$", re.IGNORECASE) DELAYED_MEMORY_FIELD_RE = re.compile( r"(?im)^[^\S\r\n]*(title|summary|tags|body|long_term_facts_ids)" @@ -35,7 +32,7 @@ def normalize_long_term_fact_ids(value) -> list[str]: fact_id = str( candidate or "" - ).strip().casefold() + ).strip().upper() if ( not fact_id diff --git a/utils/actions/update_l4_facts_utils.py b/utils/actions/update_l4_facts_utils.py index cf181dd8..aca01bd0 100644 --- a/utils/actions/update_l4_facts_utils.py +++ b/utils/actions/update_l4_facts_utils.py @@ -1,9 +1,11 @@ from __future__ import annotations import json +import re MAX_UPDATE_L4_FACTS_MESSAGE_CHARS = 1200 +L4_FACT_ID_RE = re.compile(r"^F[1-9]\d*$", re.IGNORECASE) def parse_update_l4_facts_payload(payload: str) -> dict: @@ -28,9 +30,9 @@ def parse_update_l4_facts_payload(payload: str) -> dict: seen = set() for raw_fact_id in raw_fact_ids: - fact_id = str(raw_fact_id or "").strip() + fact_id = str(raw_fact_id or "").strip().upper() - if not fact_id.startswith("l4_"): + if not L4_FACT_ID_RE.fullmatch(fact_id): return {} if fact_id in seen: continue diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index e11e47fd..43512621 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -243,7 +243,7 @@ def get_runtime_l4_fact_ids( "", ) or "" - ).strip().casefold() + ).strip().upper() if fact_id: fact_ids.add(fact_id) @@ -2859,7 +2859,7 @@ def get_conversation_activity_diff( patch_sources = ( getattr( context, - "runtime_l2_pending_patches", + "runtime_l1_diff_history", None, ) or getattr( diff --git a/utils/long_term_facts_file_store.py b/utils/long_term_facts_file_store.py index f40d9f6d..2d5c2244 100644 --- a/utils/long_term_facts_file_store.py +++ b/utils/long_term_facts_file_store.py @@ -46,9 +46,24 @@ def load_long_term_facts_store( f"cannot load {path.name}: {error}", ] - return normalize_l4_store( + normalized = normalize_l4_store( raw_value, - ), [] + ) + + # Persist one-way ID/schema migrations immediately so the file store never + # oscillates between legacy hash IDs and compact F/PF IDs across restarts. + if normalized != raw_value: + try: + persist_long_term_facts_store( + normalized, + root=root, + ) + except OSError as error: + return normalized, [ + f"cannot persist migrated {path.name}: {error}", + ] + + return normalized, [] def persist_long_term_facts_store( diff --git a/utils/python_skill_asset_utils.py b/utils/python_skill_asset_utils.py index 9e9418dd..ca7806ee 100644 --- a/utils/python_skill_asset_utils.py +++ b/utils/python_skill_asset_utils.py @@ -384,32 +384,11 @@ async def _resolve_reader_output_limit( context_window: int, ) -> int: - configured = getattr( - client, - "configured_max_tokens", - None, - ) detected = getattr( client, "detected_max_tokens", None, ) - prefer_server_limit = bool( - getattr( - config, - "RUNTIME_MAX_TOKENS_FALLBACK_TO_SERVER", - False, - ) - ) - - if configured and not prefer_server_limit: - return max( - 128, - min( - int(configured), - int(context_window), - ), - ) if detected: return max( @@ -441,28 +420,12 @@ async def _resolve_reader_output_limit( ), ) - if configured: - return max( - 128, - min( - int(configured), - int(context_window), - ), - ) - + # LM Studio usually exposes the loaded context_length rather than a + # separate completion cap. In that case the live context itself is the + # upper generation ceiling; prompt budgeting applies the real safe limit. return max( 128, - min( - int(context_window), - int( - getattr( - config, - "SERVICE_MAX_TOKENS", - 4096, - ) - or 4096 - ), - ), + int(context_window), ) @@ -1655,40 +1618,33 @@ def _resolve_reader_budgets( ) if fits: - # `max_tokens` includes hidden/model reasoning on several local - # reasoning-capable models. Keep the visible accumulated result compact, - # but reserve up to one extra result-sized allowance so the very first - # request can actually reach its answer instead of hitting `length` and - # entering a retry/split loop. - reasoning_allowance = max( - 256, - result_cap, + # `max_tokens` is one shared generation cap for hidden reasoning + the + # visible answer. Do not split that budget into fixed reasoning/answer + # shares. Reserve enough room for the expected result, give the reader + # its useful chunk ceiling, then let generation use every token left. + generation_limit = max( + hard_minimum_output_tokens, + int(output_token_limit or context_window), ) - desired_generation_budget = min( + minimum_generation_budget = min( + generation_limit, max( hard_minimum_output_tokens, - int(output_token_limit or 0), - ), - result_cap + reasoning_allowance, - ) - balanced_generation_budget = max( - result_cap, - int(available_tokens * 0.55), - ) - output_budget = max( - hard_minimum_output_tokens, - min( - desired_generation_budget, - balanced_generation_budget, - available_tokens - minimum_chunk_tokens, + result_cap, ), ) - chunk_room = available_tokens - output_budget chunk_tokens = min( maximum_chunk_tokens, max( hard_minimum_chunk_tokens, - chunk_room, + available_tokens - minimum_generation_budget, + ), + ) + output_budget = max( + hard_minimum_output_tokens, + min( + generation_limit, + available_tokens - chunk_tokens, ), ) chunk_words = max( @@ -1698,6 +1654,7 @@ def _resolve_reader_budgets( * 0.65 ), ) + reasoning_allowance = 0 else: reasoning_allowance = 0 output_budget = max( diff --git a/utils/tokens.py b/utils/tokens.py index 63c6674e..5c053550 100644 --- a/utils/tokens.py +++ b/utils/tokens.py @@ -12,22 +12,18 @@ def estimate_tokens( word_estimate = len( text.split() ) - char_estimate = ceil( - len(text) / 4 + byte_estimate = ceil( + len( + text.encode( + "utf-8" + ) + ) / 4 ) - if word_estimate <= 1: - return max( - 1, - char_estimate, - ) - return max( 1, - min( - word_estimate, - char_estimate, - ), + word_estimate, + byte_estimate, ) From 4474cfb65e36a9f99b0ac574c14ec54a00c74a3a Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:29:45 +0300 Subject: [PATCH 021/168] fix: stream validator - fix for long term facts loop detection --- tests/test_stream_validator.py | 42 ++++++++++++++++++++++++++++++++++ utils/stream_validator.py | 20 +++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index 5f2fc82a..a050ed8f 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -244,6 +244,48 @@ def test_stream_validator_allows_repeated_numeric_stream_fragments(): assert validator.last_failure_loop_preview == "" +def test_stream_validator_does_not_split_fact_ids_at_provider_chunk_edges(): + validator = StreamValidator() + + chunks = ["Cluster B contains: "] + for fact_id in range(5, 13): + chunks.extend(["F", f"{fact_id}, "]) + + for chunk in chunks: + clean, is_valid = validator.filter_chunk(chunk) + + assert clean == chunk + assert is_valid + + assert validator.last_failure_reason is None + assert validator.last_failure_preview == "" + assert validator.last_failure_loop_preview == "" + + +def test_stream_validator_still_catches_words_split_from_whitespace_chunks(): + validator = StreamValidator() + + for repeat_index in range(8): + clean, is_valid = validator.filter_chunk("wait") + assert clean == "wait" + assert is_valid + + clean, is_valid = validator.filter_chunk(" ") + + if repeat_index < 7: + assert clean == " " + assert is_valid + continue + + assert clean == "" + assert not is_valid + + assert validator.last_failure_reason == ( + "Repeated word loop detected." + ) + assert validator.last_failure_loop_preview == "wait" + + def test_stream_validator_allows_short_repeated_sentences(): validator = StreamValidator() diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 14250ee8..74322b96 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -128,6 +128,11 @@ def __init__(self): self.history_paragraphs = set() self.recent_words = [] + # A provider chunk boundary is not a word boundary. Keep the + # unfinished trailing token so streamed identifiers such as + # ``F`` + ``5,`` are validated as ``F5`` instead of eight fake + # repeated ``F`` words. + self.word_fragment = "" self.validation_marker_buffer = "" self.last_failure_reason: str | None = None @@ -536,7 +541,20 @@ def validate_word_loops( self, chunk: str, ): - words = chunk.split(" ") + text = self.word_fragment + chunk + words = text.split() + + # Streaming providers may split one lexical token across chunks + # (for example: ``" F"`` then ``"5,"``). Do not treat the chunk + # edge as whitespace. Hold the trailing token until a real + # whitespace boundary arrives. + if text and not text[-1].isspace(): + if words: + self.word_fragment = words.pop() + else: + self.word_fragment = text + else: + self.word_fragment = "" for word in words: From d587c1baec82afa1b70b9f06a1edbf6b76d51324 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:35:20 +0300 Subject: [PATCH 022/168] feature: L4 memory layer - add anchor and absorbed fact handling for delayed memory - migrate delayed memory actions to load/unload naming - update delayed memory through partial JSON payloads - clean delayed memory references when L4 facts are deleted --- clients/brain_client.py | 8 +- contracts/append_delayed_memory.json | 20 -- contracts/load_delayed_memory.json | 20 ++ contracts/remove_delayed_memory.json | 18 -- contracts/rules_assembler.py | 20 +- contracts/save_delayed_memory.json | 14 +- contracts/unload_delayed_memory.json | 18 ++ contracts/update_delayed_memory.json | 19 +- runtime/L4_memory.py | 119 ++++++++++-- runtime/L4_memory_utils.py | 38 +++- runtime/stream.py | 12 +- tests/runtime_actions/test_delayed_memory.py | 174 +++++++++++------- tests/runtime_actions/test_stream_filter.py | 8 +- tests/runtime_actions/test_update_l4_facts.py | 6 +- tests/test_brain_runtime_actions.py | 24 +-- tests/test_delayed_memory_file_store.py | 27 ++- tests/test_l4_memory.py | 140 +++++++++++++- ui/static/js/runtime/runtime-storage.js | 19 +- ui/static/js/socket/delayed-memory.js | 52 +++++- utils/actions/__init__.py | 4 + utils/actions/common_action_utils.py | 16 +- utils/actions/dispatcher.py | 12 +- ..._utils.py => load_delayed_memory_utils.py} | 2 +- utils/actions/save_delayed_memory_utils.py | 101 ++++++++-- utils/actions/update_delayed_memory_utils.py | 108 ++++++----- utils/brain_client_utils.py | 101 ++++++---- utils/context/delayed_memory.py | 4 +- utils/context/session_actions.py | 2 +- utils/delayed_memory_file_store.py | 49 ++--- utils/session_actions_history.py | 20 +- 30 files changed, 847 insertions(+), 328 deletions(-) delete mode 100644 contracts/append_delayed_memory.json create mode 100644 contracts/load_delayed_memory.json delete mode 100644 contracts/remove_delayed_memory.json create mode 100644 contracts/unload_delayed_memory.json rename utils/actions/{append_delayed_memory_utils.py => load_delayed_memory_utils.py} (91%) diff --git a/clients/brain_client.py b/clients/brain_client.py index b3e7536a..4f53ce25 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -7,14 +7,14 @@ config, ) from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, RUNTIME_ACTION_WEB_SEARCH, @@ -861,8 +861,8 @@ def get_action_counter_display_payloads() -> dict: ] = applied_colors delayed_memory_display_actions = ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, ) delayed_memory_entries = [ diff --git a/contracts/append_delayed_memory.json b/contracts/append_delayed_memory.json deleted file mode 100644 index 8de7979c..00000000 --- a/contracts/append_delayed_memory.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "append_delayed_memory": { - "version": 1, - "description": "Allows the model to append a delayed memory report into the session context.", - "runtime_action": "APPEND_DELAYED_MEMORY", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": false - }, - "rules": [ - "Emit one marker per saved delayed memory. You may emit multiple markers to append multiple reports; already appended ids are ignored.", - "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in .", - "Available for appending reports are listed in the block.", - "If a report already appended, use content placed in block ." - ], - "close_tag": false - } -} diff --git a/contracts/load_delayed_memory.json b/contracts/load_delayed_memory.json new file mode 100644 index 00000000..b685a8c4 --- /dev/null +++ b/contracts/load_delayed_memory.json @@ -0,0 +1,20 @@ +{ + "load_delayed_memory": { + "version": 2, + "description": "Allows the model to load a delayed memory report into the session context.", + "runtime_action": "LOAD_DELAYED_MEMORY", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": false + }, + "rules": [ + "Emit one marker per saved delayed memory. You may emit multiple markers to load multiple reports; already loaded ids are ignored.", + "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in .", + "Available reports are listed in the block.", + "If a report is already loaded, use the content placed in ." + ], + "close_tag": false + } +} diff --git a/contracts/remove_delayed_memory.json b/contracts/remove_delayed_memory.json deleted file mode 100644 index b163ff76..00000000 --- a/contracts/remove_delayed_memory.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "remove_delayed_memory": { - "version": 1, - "description": "Allows the model to remove an appended delayed memory report from the session context.", - "runtime_action": "REMOVE_DELAYED_MEMORY", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": false - }, - "rules": [ - "Emit marker to remove single APPENDED_DELAYED_MEMORY from context in the next turn/message; saved reports are never deleted from storage", - "id - is a placeholder word; replace it with the 6-character id prefix shown in ." - ], - "close_tag": false - } -} diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index 919c1f29..ff41040d 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -31,8 +31,8 @@ ("RESOLVE_TODO", "CAN_RUNTIME_TODO"), ("CHECK_TODO", "CAN_RUNTIME_TODO"), ("SAVE_DELAYED_MEMORY_CONTENT", "CAN_SAVE_DELAYED_MEMORY"), - ("APPEND_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), - ("REMOVE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), + ("LOAD_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), + ("UNLOAD_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("UPDATE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), ("SAVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), ("RESOLVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), @@ -298,8 +298,8 @@ def normalize_runtime_action_names(enabled_actions=None) -> tuple[str, ...]: if normalized_name == "SAVE_DELAYED_MEMORY_CONTENT": normalized_names.extend(( - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", )) @@ -532,8 +532,8 @@ def append_rules(action_name: str) -> None: continue if normalized_name in { - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", } and not _context_has_delayed_memory_reports(context): continue @@ -561,11 +561,11 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT = get_runtime_action_name( "save_delayed_memory" ) -RUNTIME_ACTION_APPEND_DELAYED_MEMORY = get_runtime_action_name( - "append_delayed_memory" +RUNTIME_ACTION_LOAD_DELAYED_MEMORY = get_runtime_action_name( + "load_delayed_memory" ) -RUNTIME_ACTION_REMOVE_DELAYED_MEMORY = get_runtime_action_name( - "remove_delayed_memory" +RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY = get_runtime_action_name( + "unload_delayed_memory" ) RUNTIME_ACTION_UPDATE_DELAYED_MEMORY = get_runtime_action_name( "update_delayed_memory" diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 4e2e8d55..d947c3c3 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -1,6 +1,6 @@ { "save_delayed_memory": { - "version": 1, + "version": 2, "description": "Allows the model to save a delayed memory report.", "runtime_action": "SAVE_DELAYED_MEMORY_CONTENT", "private_marker": "", @@ -16,18 +16,22 @@ }, "rules": [ "Use this marker only after the user explicitly asks to create/save a delayed memory report or explicitly accepts a proposal to consolidate L4 facts into delayed memory.", - "Skip save_delayed_memory when the user request is to save session or append an already existing delayed memory report.", + "Skip save_delayed_memory when the user request is to save session or load an already existing delayed memory report.", "Emit the fulfilled form inside the marker:", "", "title:", "summary:", "tags:", "body:", - "long_term_facts_ids:", + "anchor_fact_ids:", + "absorbed_fact_ids:", "", "You must fulfill title, summary, tags, and body so the report is valid for runtime processing.", - "When the report consolidates facts from , fill long_term_facts_ids with the exact F ids copied from the [ id: ... ] suffixes of every fact absorbed by the report, separated by commas.", - "Do not list unrelated ids. Leave long_term_facts_ids empty when the report does not consolidate L4 facts.", + "When the report consolidates facts from , classify each referenced F id as either anchor or absorbed.", + "anchor_fact_ids are important atomic facts that must remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", + "absorbed_fact_ids are details represented by the report and are hidden from while the report exists.", + "Use exact ids copied from the [ id: ... ] suffixes. Do not list unrelated ids and never use PF pending ids.", + "A fact id must not be present in both arrays. Leave both arrays empty when the report does not consolidate L4 facts.", "DO NOT add any other text while making a report. Do not notify or acknowledge saving; immediately emit only the marker." ], "close_tag": true diff --git a/contracts/unload_delayed_memory.json b/contracts/unload_delayed_memory.json new file mode 100644 index 00000000..88cf4604 --- /dev/null +++ b/contracts/unload_delayed_memory.json @@ -0,0 +1,18 @@ +{ + "unload_delayed_memory": { + "version": 2, + "description": "Allows the model to unload a delayed memory report from the session context.", + "runtime_action": "UNLOAD_DELAYED_MEMORY", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": false + }, + "rules": [ + "Emit to unload one currently loaded APPENDED_DELAYED_MEMORY from context in the next turn/message; the saved report is never deleted from storage.", + "id - is a placeholder word; replace it with the 6-character id prefix shown in ." + ], + "close_tag": false + } +} diff --git a/contracts/update_delayed_memory.json b/contracts/update_delayed_memory.json index ed6b4fa0..c676b95c 100644 --- a/contracts/update_delayed_memory.json +++ b/contracts/update_delayed_memory.json @@ -1,7 +1,7 @@ { "update_delayed_memory": { - "version": 1, - "description": "Allows the model to append selected fields to an existing delayed memory report.", + "version": 2, + "description": "Allows the model to append selected data to an existing delayed memory report without rewriting its existing content.", "runtime_action": "UPDATE_DELAYED_MEMORY", "private_marker": "", "triggers": [], @@ -11,16 +11,17 @@ }, "rules": [ "Use this marker only after the user explicitly asks to update an existing delayed memory report or explicitly accepts a proposal to extend one.", - "The marker appends only tags, body, and long_term_facts_ids to an existing report. It never replaces title, summary, or existing content.", - "Emit the fulfilled form:", + "The update is additive. It may append new tags, new body text, new anchor_fact_ids, and/or new absorbed_fact_ids. It never replaces title, summary, or existing body content.", + "Emit the fulfilled marker with one JSON object inside it:", "", - "tags:", - "long_term_facts_ids:", - "body:", + "{\"tags\":[\"tag\"],\"body\":\"new text\",\"anchor_fact_ids\":[\"F1\"],\"absorbed_fact_ids\":[\"F2\"]}", "", "Replace id with the 6-character report id shown in .", - "List only new tags, new body content, and exact F L4 ids actually absorbed by this update. F means a committed fact; never use PF pending ids here.", - "At least one of tags, long_term_facts_ids, or body must be non-empty.", + "The JSON object may contain any subset of tags, body, anchor_fact_ids, absorbed_fact_ids; at least one field must be present and non-empty.", + "Use exact F ids copied from . F means a committed fact; never use PF pending ids here.", + "anchor_fact_ids remain visible in and receive a [ delayed_memory_id: ... ] suffix while the report exists.", + "absorbed_fact_ids are represented by the report and are hidden from while the report exists.", + "A fact id must not be placed in both arrays. If a fact is important enough to remain directly visible, classify it as an anchor.", "Do not add any other text while updating a report. Immediately emit only the marker." ], "close_tag": true diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 2d58ece7..564cae6c 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -32,7 +32,9 @@ restore_l4_fact_to_store, ) from utils.actions.save_delayed_memory_utils import ( + collect_anchor_fact_report_ids, collect_long_term_fact_ids_from_reports, + normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, ) from utils.long_term_facts_file_store import ( @@ -403,19 +405,27 @@ def refresh_runtime_l4_archived_fact_ids( context, ) -> set[str]: + reports = getattr( + context, + "delayed_memory_reports", + {}, + ) fact_ids = collect_long_term_fact_ids_from_reports( - getattr( - context, - "delayed_memory_reports", - {}, + reports + ) + # Anchor is a global visibility guarantee: if any delayed report keeps a + # fact as an anchor, another report cannot accidentally hide it. + anchor_fact_ids = set( + collect_anchor_fact_report_ids( + reports ) ) + fact_ids.difference_update(anchor_fact_ids) context.runtime_l4_archived_fact_ids = set( fact_ids ) return context.runtime_l4_archived_fact_ids - def ensure_runtime_l4_state(context) -> dict: context.runtime_facts_memory_records = normalize_facts_memory_records( getattr(context, "runtime_facts_memory_records", []) @@ -526,20 +536,53 @@ def remap_delayed_memory_l4_fact_ids( if not isinstance(report, dict): continue - current_ids = normalize_long_term_fact_ids( - report.get("long_term_facts_ids", []) + current_anchor_ids, current_absorbed_ids = ( + normalize_delayed_memory_fact_roles( + report.get("anchor_fact_ids", []), + report.get("absorbed_fact_ids", []), + report.get("long_term_facts_ids", []), + ) + ) + removed_anchor = any( + fact_id in removed_ids + for fact_id in current_anchor_ids + ) + removed_absorbed = any( + fact_id in removed_ids + for fact_id in current_absorbed_ids ) - if not any(fact_id in removed_ids for fact_id in current_ids): + + if not removed_anchor and not removed_absorbed: continue - next_ids = normalize_long_term_fact_ids([ - *(fact_id for fact_id in current_ids if fact_id not in removed_ids), - *replacement_ids, - ]) + next_anchor_ids = [ + fact_id + for fact_id in current_anchor_ids + if fact_id not in removed_ids + ] + next_absorbed_ids = [ + fact_id + for fact_id in current_absorbed_ids + if fact_id not in removed_ids + ] + + if removed_anchor: + next_anchor_ids.extend(replacement_ids) + if removed_absorbed: + next_absorbed_ids.extend(replacement_ids) + + next_anchor_ids, next_absorbed_ids = ( + normalize_delayed_memory_fact_roles( + next_anchor_ids, + next_absorbed_ids, + ) + ) updated_report = { **report, - "long_term_facts_ids": next_ids, + "anchor_fact_ids": next_anchor_ids, + "absorbed_fact_ids": next_absorbed_ids, } + updated_report.pop("long_term_facts_ids", None) reports[report_id] = updated_report changed_reports[report_id] = updated_report @@ -1311,6 +1354,29 @@ def l4_fact_matches_archived_ids( ) +def _get_l4_fact_anchor_report_ids( + fact: dict, + anchor_report_ids_by_fact_id: dict[str, list[str]], +) -> list[str]: + candidate_ids = [ + fact.get("id", ""), + *( + fact.get("source_fact_ids", []) + if isinstance(fact.get("source_fact_ids", []), list) + else [] + ), + ] + report_ids = [] + + for candidate_id in candidate_ids: + fact_id = str(candidate_id or "").strip().upper() + for report_id in anchor_report_ids_by_fact_id.get(fact_id, []): + if report_id not in report_ids: + report_ids.append(report_id) + + return report_ids + + def build_runtime_l4_memory_context(*, context) -> str: if not l4_memory_enabled(): return "" @@ -1319,6 +1385,10 @@ def build_runtime_l4_memory_context(*, context) -> str: archived_fact_ids = refresh_runtime_l4_archived_fact_ids( context ) + reports = getattr(context, "delayed_memory_reports", {}) + anchor_report_ids_by_fact_id = collect_anchor_fact_report_ids( + reports + ) active_facts = [ fact for fact in store.get("facts") or [] @@ -1327,8 +1397,22 @@ def build_runtime_l4_memory_context(*, context) -> str: archived_fact_ids, ) ] + delayed_memory_ids_by_fact_id = {} + + for fact in active_facts: + fact_id = str(fact.get("id", "") or "").strip().upper() + if not fact_id: + continue + report_ids = _get_l4_fact_anchor_report_ids( + fact, + anchor_report_ids_by_fact_id, + ) + if report_ids: + delayed_memory_ids_by_fact_id[fact_id] = report_ids + return format_long_term_memory_context( - active_facts + active_facts, + delayed_memory_ids_by_fact_id=delayed_memory_ids_by_fact_id, ) @@ -1372,10 +1456,17 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: tag_suffix="DELETED", deleted_fact=deleted_fact, ) + delayed_memory_change = remap_delayed_memory_l4_fact_ids( + context, + removed_fact_ids=[target_id], + replacement_fact_ids=[], + ) await emit_l4_memory_update( context, change={"removed_ids": [target_id], "changed": True}, ) + if delayed_memory_change.get("changed"): + await emit_delayed_memory_reference_update(context) return True diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index db0b1f69..547c1f99 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -2093,8 +2093,17 @@ def format_l4_merge_operation_details(change: dict) -> str: ) -def format_long_term_memory_context(facts: list[dict]) -> str: +def format_long_term_memory_context( + facts: list[dict], + *, + delayed_memory_ids_by_fact_id=None, +) -> str: lines = [] + delayed_memory_ids_by_fact_id = ( + delayed_memory_ids_by_fact_id + if isinstance(delayed_memory_ids_by_fact_id, dict) + else {} + ) for fact in facts: line = format_l4_fact_line( @@ -2111,8 +2120,32 @@ def format_long_term_memory_context(facts: list[dict]) -> str: if not line or not fact_id: continue + suffix = f" [ id: {fact_id} ]" + delayed_memory_ids = delayed_memory_ids_by_fact_id.get( + fact_id.upper(), + [], + ) + if isinstance(delayed_memory_ids, str): + delayed_memory_ids = [delayed_memory_ids] + + seen_delayed_memory_ids = set() + for delayed_memory_id in delayed_memory_ids or []: + normalized_delayed_memory_id = str( + delayed_memory_id or "" + ).strip().casefold() + if ( + not normalized_delayed_memory_id + or normalized_delayed_memory_id in seen_delayed_memory_ids + ): + continue + seen_delayed_memory_ids.add(normalized_delayed_memory_id) + suffix += ( + " [ delayed_memory_id: " + f"{normalized_delayed_memory_id} ]" + ) + lines.append( - f"{line} [ id: {fact_id} ]" + f"{line}{suffix}" ) if not lines: @@ -2124,6 +2157,5 @@ def format_long_term_memory_context(facts: list[dict]) -> str: ) return f"\n{body}\n" - def clone_l4_store(store) -> dict: return deepcopy(normalize_l4_store(store)) diff --git a/runtime/stream.py b/runtime/stream.py index 2410c0d7..b6b5d50c 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -43,13 +43,13 @@ should_pause_action_guard_for_confirmation, ) from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, build_runtime_action_display_text, get_runtime_action_display_name, @@ -1257,8 +1257,8 @@ def get_runtime_action_display_id( return action_id if action.name in { - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, }: report_id, _report = ( self.get_delayed_memory_runtime_action_report( @@ -1282,8 +1282,8 @@ def get_delayed_memory_runtime_action_report( ): if action.name not in { - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, }: return "", None diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index cadeb0eb..0541054f 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -137,7 +137,8 @@ def test_parses_delayed_memory_content_payload(self): "A complete, self-sufficient summary..." ), "pinned": False, - "long_term_facts_ids": [], + "anchor_fact_ids": [], + "absorbed_fact_ids": [], "created_session_id": "session-1", "created_time": "2026-06-29T12:00:00", }, @@ -163,14 +164,53 @@ def test_parses_long_term_fact_ids_for_delayed_memory_report(self): ) self.assertEqual( - report_value["long_term_facts_ids"], + report_value["absorbed_fact_ids"], [ "F1", "F2", ], ) + self.assertNotIn("long_term_facts_ids", report_value) + def test_parses_anchor_and_absorbed_fact_ids_for_delayed_memory_report(self): + + report = parse_delayed_memory_content_payload( + ( + "title: Social context\n" + "summary: Consolidated social details.\n" + "tags: social\n" + "body: Reusable summary.\n" + "anchor_fact_ids: F1, F1\n" + "absorbed_fact_ids: F2, F3, F1" + ) + ) + report_value = next(iter(report.values())) + + self.assertEqual(report_value["anchor_fact_ids"], ["F1"]) + self.assertEqual( + report_value["absorbed_fact_ids"], + ["F2", "F3"], + ) + + def test_update_delayed_memory_json_accepts_single_non_empty_field(self): + + self.assertEqual( + parse_update_delayed_memory_payload( + 'abc123\n{"anchor_fact_ids":["F8"]}' + ), + { + "id": "abc123", + "anchor_fact_ids": ["F8"], + }, + ) + self.assertEqual( + parse_update_delayed_memory_payload( + 'abc123\n{"body":""}' + ), + {}, + ) + def test_build_report_keeps_only_existing_l4_fact_ids(self): context = SimpleNamespace( @@ -205,11 +245,12 @@ def test_build_report_keeps_only_existing_l4_fact_ids(self): report_value = report["abc123"] self.assertEqual( - report_value["long_term_facts_ids"], + report_value["absorbed_fact_ids"], [ "F1", ], ) + self.assertNotIn("long_term_facts_ids", report_value) def test_extracts_delayed_memory_content_block(self): @@ -344,8 +385,8 @@ def test_extracts_delayed_memory_action_markers(self): result = extract_runtime_actions( ( "\n" - "\n" - "\n" + "\n" + "\n" ), enabled_actions=[ "CAN_SAVE_DELAYED_MEMORY", @@ -360,11 +401,11 @@ def test_extracts_delayed_memory_action_markers(self): result.actions, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="d4e5f6", ), ), @@ -380,7 +421,7 @@ def test_stream_filter_holds_split_delayed_memory_action_marker(self): ) first = stream_filter.filter( - "" @@ -402,7 +443,7 @@ def test_stream_filter_holds_split_delayed_memory_action_marker(self): second.actions, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="h0qa49", ), ), @@ -418,7 +459,7 @@ def test_stream_filter_holds_split_internal_delayed_memory_action_marker(self): ) first = stream_filter.filter( - "\nRemoved it from the session." @@ -440,7 +481,7 @@ def test_stream_filter_holds_split_internal_delayed_memory_action_marker(self): second.actions, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="kdhpjo", ), ), @@ -912,7 +953,7 @@ def test_append_delayed_memory_uses_appended_context_block(self): context, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), ), @@ -967,7 +1008,7 @@ def test_append_delayed_memory_uses_appended_context_block(self): self.assertEqual( context.emitter.events[0]["text"], ( - "Appending: " + "Loading: " + context.delayed_memory_reports[ "a1b2c3" ]["title"] @@ -980,7 +1021,7 @@ def test_append_delayed_memory_uses_appended_context_block(self): self.assertEqual( context.runtime_session_action_history[0]["text"], ( - "Delayed memory appended: " + "Delayed memory loaded: " + context.delayed_memory_reports[ "a1b2c3" ]["title"] @@ -1021,11 +1062,11 @@ def test_started_append_delayed_memory_events_are_report_scoped(self): asyncio.run( runtime_stream.emit_started_runtime_actions(( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="b2c3d4", ), )) @@ -1044,13 +1085,13 @@ def test_started_append_delayed_memory_events_are_report_scoped(self): [ ( "a1b2c3", - "APPEND_DELAYED_MEMORY: First report", + "LOAD_DELAYED_MEMORY: First report", "a1b2c3", "First report", ), ( "b2c3d4", - "APPEND_DELAYED_MEMORY: Second report", + "LOAD_DELAYED_MEMORY: Second report", "b2c3d4", "Second report", ), @@ -1094,15 +1135,15 @@ def test_append_delayed_memory_keeps_multiple_reports(self): context, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="b2c3d4", ), ), @@ -1158,8 +1199,8 @@ def test_append_delayed_memory_keeps_multiple_reports(self): for item in context.runtime_session_action_history ], [ - "Delayed memory appended: First report", - "Delayed memory appended: Second report", + "Delayed memory loaded: First report", + "Delayed memory loaded: Second report", ], ) self.assertEqual( @@ -1175,13 +1216,13 @@ def test_append_delayed_memory_keeps_multiple_reports(self): [ ( "a1b2c3", - "Appending: First report", + "Loading: First report", "a1b2c3", "First report", ), ( "b2c3d4", - "Appending: Second report", + "Loading: Second report", "b2c3d4", "Second report", ), @@ -1218,7 +1259,7 @@ def test_invalid_append_delayed_memory_id_returns_failure_tool_result(self): context, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="c7dtso", ), ), @@ -1253,7 +1294,7 @@ def test_invalid_append_delayed_memory_id_returns_failure_tool_result(self): tool_results, ) self.assertIn( - '', + '', tool_results, ) self.assertIn( @@ -1293,11 +1334,11 @@ def test_append_delayed_memory_tracks_session_metadata(self): context, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), ), @@ -1359,7 +1400,7 @@ def test_append_delayed_memory_tracks_session_metadata(self): next_context, ( RuntimeActionCall( - name="APPEND_DELAYED_MEMORY", + name="LOAD_DELAYED_MEMORY", payload="a1b2c3", ), ), @@ -1421,7 +1462,7 @@ def test_remove_delayed_memory_only_detaches_from_context(self): context, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="a1b2c3", ), ), @@ -1455,11 +1496,11 @@ def test_remove_delayed_memory_only_detaches_from_context(self): ) self.assertEqual( context.emitter.events[0]["text"], - "Removing: Pinned report", + "Unloading: Pinned report", ) self.assertEqual( context.runtime_session_action_history[0]["text"], - "Delayed memory removed from context: Pinned report", + "Delayed memory unloaded from context: Pinned report", ) @@ -1511,11 +1552,11 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): context, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="a1b2c3", ), RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="b2c3d4", ), ), @@ -1556,8 +1597,8 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): for event in context.emitter.events ], [ - "Removing: First report", - "Removing: Second report", + "Unloading: First report", + "Unloading: Second report", ], ) @@ -1588,9 +1629,9 @@ def test_invalid_remove_delayed_memory_id_returns_failed_result(self): } extracted = extract_runtime_actions( - "", + "", enabled_actions=( - "REMOVE_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", ), ) @@ -1598,7 +1639,7 @@ def test_invalid_remove_delayed_memory_id_returns_failed_result(self): extracted.actions, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="Test report (summary check)", ), ), @@ -1628,7 +1669,7 @@ def test_invalid_remove_delayed_memory_id_returns_failed_result(self): "invalid_delayed_memory_id", ) self.assertIn( - '', + '', build_tool_results_context( context ), @@ -1676,7 +1717,7 @@ def test_missing_remove_delayed_memory_id_returns_failed_result(self): context, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="c7dtso", ), ), @@ -1711,7 +1752,7 @@ def test_missing_remove_delayed_memory_id_returns_failed_result(self): tool_results, ) self.assertIn( - '', + '', tool_results, ) self.assertIn( @@ -1732,12 +1773,10 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): ) second = stream_filter.filter( ( - "tags:\n" - "[social, context]\n" - "long_term_facts_ids:\n" - "F1, F2\n" - "body:\n" - "Additional durable context.\n" + '{"tags":["social","context"],' + '"anchor_fact_ids":["F1"],' + '"absorbed_fact_ids":["F2"],' + '"body":"Additional durable context."}\n' ) ) third = stream_filter.filter( @@ -1767,8 +1806,10 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): "social", "context", ], - "long_term_facts_ids": [ + "anchor_fact_ids": [ "F1", + ], + "absorbed_fact_ids": [ "F2", ], "body": "Additional durable context.", @@ -1781,10 +1822,10 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): result = extract_runtime_actions( ( "\n" - "tags: social, context\n" - "long_term_facts_ids: F1, F99\n" - "body:\n" - "Additional durable context.\n" + '{"tags":["social","context"],' + '"anchor_fact_ids":["F1"],' + '"absorbed_fact_ids":["F99"],' + '"body":"Additional durable context."}\n' "" ), enabled_actions=( @@ -1814,8 +1855,10 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): "social", "context", ], - "long_term_facts_ids": [ + "anchor_fact_ids": [ "F1", + ], + "absorbed_fact_ids": [ "F99", ], "body": "Additional durable context.", @@ -1840,9 +1883,10 @@ def test_update_delayed_memory_appends_only_supported_fields(self): "social", ], "body": "Original body.", - "long_term_facts_ids": [ + "anchor_fact_ids": [ "F1", ], + "absorbed_fact_ids": [], "pinned": False, }, } @@ -1869,10 +1913,11 @@ def test_update_delayed_memory_appends_only_supported_fields(self): name="UPDATE_DELAYED_MEMORY", payload=( "a1b2c3\n" - "tags: context, social\n" - "long_term_facts_ids: F2, F99\n" - "body:\n" - "Additional body." + + json.dumps({ + "tags": ["context", "social"], + "absorbed_fact_ids": ["F2", "F99"], + "body": "Additional body.", + }) ), ), ), @@ -1889,9 +1934,14 @@ def test_update_delayed_memory_appends_only_supported_fields(self): ], ) self.assertEqual( - report["long_term_facts_ids"], + report["anchor_fact_ids"], [ "F1", + ], + ) + self.assertEqual( + report["absorbed_fact_ids"], + [ "F2", ], ) @@ -1984,7 +2034,7 @@ def test_remove_pinned_delayed_memory_returns_failed_result(self): context, ( RuntimeActionCall( - name="REMOVE_DELAYED_MEMORY", + name="UNLOAD_DELAYED_MEMORY", payload="a1b2c3", ), ), diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index 3e37ae63..2c14cc7d 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -468,8 +468,8 @@ def test_extracts_self_closing_runtime_markers_without_blocks(self): "todo-1", ), ( - "", - "APPEND_DELAYED_MEMORY", + "", + "LOAD_DELAYED_MEMORY", "a1b2c3", ), ) @@ -680,8 +680,8 @@ def test_resolve_and_remove_actions_share_payload_normalization(self): "todo-1", ), ( - "", - "REMOVE_DELAYED_MEMORY", + "", + "UNLOAD_DELAYED_MEMORY", "d4e5f6", ), ( diff --git a/tests/runtime_actions/test_update_l4_facts.py b/tests/runtime_actions/test_update_l4_facts.py index cf570e66..eeff4f0b 100644 --- a/tests/runtime_actions/test_update_l4_facts.py +++ b/tests/runtime_actions/test_update_l4_facts.py @@ -153,9 +153,13 @@ async def test_runtime_action_updates_l4_in_background(self): replacement_id = facts[0]["id"] self.assertEqual( - context.delayed_memory_reports["abc123"]["long_term_facts_ids"], + context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], [replacement_id], ) + self.assertNotIn( + "long_term_facts_ids", + context.delayed_memory_reports["abc123"], + ) lifecycle = [ event diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 72040baa..f0f37716 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -124,8 +124,8 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: expected_actions.extend( ( "SAVE_DELAYED_MEMORY_CONTENT", - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", ) ) @@ -1758,7 +1758,7 @@ def test_append_delayed_history_splits_by_raw_id(self): context, 0, [{ - "name": "APPEND_DELAYED_MEMORY", + "name": "LOAD_DELAYED_MEMORY", "marker_count": 2, "payloads": [ "Shared title", @@ -1775,12 +1775,12 @@ def test_append_delayed_history_splits_by_raw_id(self): context.runtime_session_action_history[0]["parts"], [ { - "text": "APPEND_DELAYED_MEMORY", + "text": "LOAD_DELAYED_MEMORY", "detail": "Shared title", "id": "abc123", }, { - "text": "APPEND_DELAYED_MEMORY", + "text": "LOAD_DELAYED_MEMORY", "detail": "Shared title", "id": "def456", }, @@ -1798,7 +1798,7 @@ def test_remove_delayed_history_splits_by_raw_id(self): context, 0, [{ - "name": "REMOVE_DELAYED_MEMORY", + "name": "UNLOAD_DELAYED_MEMORY", "marker_count": 2, "payloads": [ "First report", @@ -1815,12 +1815,12 @@ def test_remove_delayed_history_splits_by_raw_id(self): context.runtime_session_action_history[0]["parts"], [ { - "text": "REMOVE_DELAYED_MEMORY", + "text": "UNLOAD_DELAYED_MEMORY", "detail": "First report", "id": "abc123", }, { - "text": "REMOVE_DELAYED_MEMORY", + "text": "UNLOAD_DELAYED_MEMORY", "detail": "Second report", "id": "def456", }, @@ -2596,7 +2596,7 @@ async def stream(self, **_kwargs): yield { "type": "content", "content": ( - "\n\n" + "\n\n" f"{visible_answer}\n\n" "" ), @@ -2679,7 +2679,7 @@ async def collect(context): for event in context.runtime_action_events ], [ - "append_delayed_memory", + "load_delayed_memory", "clean_tool_results", ], ) @@ -3553,11 +3553,11 @@ def test_prompt_lists_available_delayed_memory_below_session_state(self): prompt, ) self.assertIn( - "", + "", prompt, ) self.assertIn( - "", + "", prompt, ) self.assertNotIn( diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index f4c6fe41..111cc8a7 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -31,8 +31,10 @@ def build_report( ], "body": body, "pinned": True, - "long_term_facts_ids": [ + "anchor_fact_ids": [ "F1", + ], + "absorbed_fact_ids": [ "F2", ], "created_session_id": "session-a", @@ -81,9 +83,14 @@ def test_persist_and_load_direct_report_shape(self): True, ) self.assertEqual( - payload["long_term_facts_ids"], + payload["anchor_fact_ids"], [ "F1", + ], + ) + self.assertEqual( + payload["absorbed_fact_ids"], + [ "F2", ], ) @@ -111,9 +118,14 @@ def test_persist_and_load_direct_report_shape(self): True, ) self.assertEqual( - reports["48ggds"]["long_term_facts_ids"], + reports["48ggds"]["anchor_fact_ids"], [ "F1", + ], + ) + self.assertEqual( + reports["48ggds"]["absorbed_fact_ids"], + [ "F2", ], ) @@ -215,7 +227,7 @@ def test_local_storage_reports_override_file_fallback_by_id(self): browser_report = self.build_report( title="Browser title", ) - browser_report["long_term_facts_ids"] = [] + browser_report["absorbed_fact_ids"] = [] browser_reports = { "48ggds": browser_report, } @@ -234,9 +246,14 @@ def test_local_storage_reports_override_file_fallback_by_id(self): "File only", ) self.assertEqual( - merged["48ggds"]["long_term_facts_ids"], + merged["48ggds"]["anchor_fact_ids"], [ "F1", + ], + ) + self.assertEqual( + merged["48ggds"]["absorbed_fact_ids"], + [ "F2", ], ) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 870760eb..063c414f 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1141,6 +1141,140 @@ def test_context_formats_all_facts_without_metadata(self): self.assertNotIn("source_session_ids", context_block) self.assertIn("[source_session_ids: session-a]", ui_line) + def test_delayed_report_anchor_stays_visible_with_report_suffix(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "user.name", + "value": "Sergey", + }, + { + "id": "F2", + "key": "social.friend", + "value": "Taras is a personal friend.", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social context", + "anchor_fact_ids": ["F1"], + "absorbed_fact_ids": ["F2"], + }, + } + + context_block = build_runtime_l4_memory_context( + context=context + ) + + self.assertIn( + "user.name: Sergey [ id: F1 ] [ delayed_memory_id: abc123 ]", + context_block, + ) + self.assertNotIn("social.friend", context_block) + self.assertEqual( + context.runtime_l4_archived_fact_ids, + {"F2"}, + ) + + def test_anchor_visibility_wins_over_absorbed_reference_in_other_report(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "user.name", + "value": "Sergey", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Identity", + "anchor_fact_ids": ["F1"], + }, + "def456": { + "title": "Social", + "absorbed_fact_ids": ["F1"], + }, + } + + context_block = build_runtime_l4_memory_context(context=context) + + self.assertIn("user.name: Sergey [ id: F1 ]", context_block) + self.assertIn("[ delayed_memory_id: abc123 ]", context_block) + self.assertEqual(context.runtime_l4_archived_fact_ids, set()) + + async def test_delete_l4_fact_cleans_delayed_memory_fact_arrays(self): + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "user.name", + "value": "Sergey", + }, + { + "id": "F2", + "key": "social.friend", + "value": "Taras", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social context", + "anchor_fact_ids": ["F1"], + "absorbed_fact_ids": ["F2"], + }, + } + context.delayed_memory_file_store_enabled = False + + self.assertTrue(await delete_l4_memory_fact(context, "F1")) + self.assertEqual( + context.delayed_memory_reports["abc123"]["anchor_fact_ids"], + [], + ) + self.assertEqual( + context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], + ["F2"], + ) + + self.assertTrue(await delete_l4_memory_fact(context, "F2")) + self.assertEqual( + context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], + [], + ) + self.assertNotIn( + "long_term_facts_ids", + context.delayed_memory_reports["abc123"], + ) + self.assertGreaterEqual( + sum( + 1 + for event in context.emitter.events + if event.get("type") == "delayed_memory_store_snapshot" + ), + 2, + ) + def test_delayed_report_fact_ids_hide_only_from_brain_context(self): context = RuntimeContext( websocket=None, @@ -1296,12 +1430,16 @@ async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): context.delayed_memory_reports[ "abc123" ][ - "long_term_facts_ids" + "absorbed_fact_ids" ], [ "F1", ], ) + self.assertNotIn( + "long_term_facts_ids", + context.delayed_memory_reports["abc123"], + ) self.assertEqual( context.runtime_l4_archived_fact_ids, { diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index c2cef0df..a17ffec6 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1217,9 +1217,24 @@ String(report.body || "").trim(), pinned: Boolean(report.pinned), - long_term_facts_ids: + anchor_fact_ids: normalizeLongTermFactIds( - report.long_term_facts_ids + report.anchor_fact_ids + ), + absorbed_fact_ids: + normalizeLongTermFactIds( + [ + ...(Array.isArray(report.absorbed_fact_ids) + ? report.absorbed_fact_ids + : []), + ...(Array.isArray(report.long_term_facts_ids) + ? report.long_term_facts_ids + : []), + ] + ).filter( + factId => !normalizeLongTermFactIds( + report.anchor_fact_ids + ).includes(factId) ), created_session_id: String(report.created_session_id || "").trim(), diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index 165d1d44..40b472b1 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -102,6 +102,30 @@ function generateDelayedMemoryReportId( } +function normalizeDelayedMemoryFactIds( + value +) { + + const source = + Array.isArray(value) + ? value + : String(value || "").split(/[;,\s]+/); + const seen = new Set(); + + return source + .map(item => String(item || "").trim().toUpperCase()) + .filter( + factId => { + if (!/^F[1-9]\d*$/.test(factId) || seen.has(factId)) { + return false; + } + seen.add(factId); + return true; + } + ); + +} + function parseDelayedMemoryReportPayload( payload ) { @@ -116,7 +140,7 @@ function parseDelayedMemoryReportPayload( } const fieldPattern = - /^[^\S\r\n]*(title|summary|tags|body)[^\S\r\n]*:[^\S\r\n]*(.*)$/gim; + /^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|absorbed_fact_ids|long_term_facts_ids)[^\S\r\n]*:[^\S\r\n]*(.*)$/gim; const matches = []; let match = fieldPattern.exec(text); @@ -160,7 +184,16 @@ function parseDelayedMemoryReportPayload( .filter(Boolean) .join("\n") .trim() - : field.inline; + : [ + "anchor_fact_ids", + "absorbed_fact_ids", + "long_term_facts_ids", + ].includes(field.name) + ? [field.inline, blockValue] + .filter(Boolean) + .join(" ") + .trim() + : field.inline; } ); @@ -182,6 +215,18 @@ function parseDelayedMemoryReportPayload( currentReports ); + const anchorFactIds = + normalizeDelayedMemoryFactIds( + fields.anchor_fact_ids + ); + const anchorFactIdSet = + new Set(anchorFactIds); + const absorbedFactIds = + normalizeDelayedMemoryFactIds([ + ...normalizeDelayedMemoryFactIds(fields.absorbed_fact_ids), + ...normalizeDelayedMemoryFactIds(fields.long_term_facts_ids), + ]).filter(factId => !anchorFactIdSet.has(factId)); + return { [key]: { title, @@ -194,6 +239,8 @@ function parseDelayedMemoryReportPayload( .filter(Boolean), body: String(fields.body || "").trim(), + anchor_fact_ids: anchorFactIds, + absorbed_fact_ids: absorbedFactIds, created_session_id: String(window.jinRuntimeSessionId || websocketClientId || "").trim(), created_time: @@ -373,7 +420,6 @@ function syncDelayedMemoryReportsToRuntime() { } - window.syncDelayedMemoryReportsToRuntime = syncDelayedMemoryReportsToRuntime; diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index b5719ac4..724577af 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -53,7 +53,9 @@ match_regexp_templates, ) from .save_delayed_memory_utils import ( + collect_anchor_fact_report_ids, collect_long_term_fact_ids_from_reports, + normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, parse_delayed_memory_content_payload, ) @@ -96,7 +98,9 @@ "parse_update_l4_facts_payload", "normalize_runtime_action_name", "normalize_runtime_action_names", + "collect_anchor_fact_report_ids", "collect_long_term_fact_ids_from_reports", + "normalize_delayed_memory_fact_roles", "normalize_long_term_fact_ids", "parse_delayed_memory_content_payload", "parse_idle_seconds", diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 07819ba3..98b8457e 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -5,7 +5,7 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_APPEND_SKILL, - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, RUNTIME_ACTION_ASSET_ACTION, @@ -17,7 +17,7 @@ RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_REMOVE_SKILL, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, @@ -34,7 +34,7 @@ _clean_internal_action_query, _get_internal_action_placeholder_payloads, ) -from .append_delayed_memory_utils import build_append_delayed_memory_payload +from .load_delayed_memory_utils import build_load_delayed_memory_payload from .append_skill_utils import ( build_append_skill_payload, plural_skill_marker_action_name as _plural_skill_marker_action_name, @@ -326,8 +326,6 @@ def normalize_runtime_action_name( "SAVE_SESSION": RUNTIME_ACTION_SAVE_SESSION, "SAVE_DELAYED_MEMORY": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, "SAVE_DELAYED_MEMORY_CONTENT": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, - "APPEND_DELAYED_MEMORY": RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - "REMOVE_DELAYED_MEMORY": RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, "UPDATE_DELAYED_MEMORY": RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, "SAVE_ACTIVE_MEMORY": RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, "RESOLVE_ACTIVE_MEMORY": RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, @@ -394,10 +392,10 @@ def normalize_runtime_action_names( if normalized_name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: normalized_names.append( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY + RUNTIME_ACTION_LOAD_DELAYED_MEMORY ) normalized_names.append( - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY ) normalized_names.append( RUNTIME_ACTION_UPDATE_DELAYED_MEMORY @@ -442,8 +440,8 @@ def normalize_runtime_action_names( RUNTIME_ACTION_RESOLVE_TODO: build_resolve_action_payload, RUNTIME_ACTION_CHECK_TODO: build_check_todo_payload, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: build_save_delayed_memory_payload, - RUNTIME_ACTION_APPEND_DELAYED_MEMORY: build_append_delayed_memory_payload, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY: build_resolve_action_payload, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY: build_load_delayed_memory_payload, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY: build_resolve_action_payload, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY: build_update_delayed_memory_payload, RUNTIME_ACTION_APPEND_SKILL: build_append_skill_payload, RUNTIME_ACTION_REMOVE_SKILL: build_resolve_action_payload, diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index c246c0b3..cf30c64c 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -1,5 +1,5 @@ from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, @@ -10,7 +10,7 @@ RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, RUNTIME_ACTION_REMOVE_SKILL, RUNTIME_ACTION_RESOLVE_TODO, @@ -720,7 +720,7 @@ def accept_runtime_action_once_per_message( ) continue - if action.name == RUNTIME_ACTION_APPEND_DELAYED_MEMORY: + if action.name == RUNTIME_ACTION_LOAD_DELAYED_MEMORY: if not accept_runtime_action_once_per_message( action ): @@ -734,7 +734,7 @@ def accept_runtime_action_once_per_message( ) continue - if action.name == RUNTIME_ACTION_REMOVE_DELAYED_MEMORY: + if action.name == RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY: if not accept_runtime_action_once_per_message( action ): @@ -1257,13 +1257,13 @@ def accept_runtime_action_once_per_message( append_delayed_memory_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_APPEND_DELAYED_MEMORY + if action.name == RUNTIME_ACTION_LOAD_DELAYED_MEMORY ] remove_delayed_memory_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_REMOVE_DELAYED_MEMORY + if action.name == RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY ] update_delayed_memory_actions = [ diff --git a/utils/actions/append_delayed_memory_utils.py b/utils/actions/load_delayed_memory_utils.py similarity index 91% rename from utils/actions/append_delayed_memory_utils.py rename to utils/actions/load_delayed_memory_utils.py index df1df9e8..c02ddb87 100644 --- a/utils/actions/append_delayed_memory_utils.py +++ b/utils/actions/load_delayed_memory_utils.py @@ -4,7 +4,7 @@ from .delayed_memory_utils import is_delayed_memory_report_id -def build_append_delayed_memory_payload( +def build_load_delayed_memory_payload( query: str, placeholder_payloads=(), ) -> str | None: diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 81fdf48b..0ab78afb 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -7,7 +7,8 @@ LONG_TERM_FACT_ID_RE = re.compile(r"^F[1-9]\d*$", re.IGNORECASE) DELAYED_MEMORY_FIELD_RE = re.compile( - r"(?im)^[^\S\r\n]*(title|summary|tags|body|long_term_facts_ids)" + r"(?im)^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|" + r"absorbed_fact_ids|long_term_facts_ids)" r"[^\S\r\n]*:[^\S\r\n]*(.*)$", ) @@ -49,9 +50,43 @@ def normalize_long_term_fact_ids(value) -> list[str]: return fact_ids +def normalize_delayed_memory_fact_roles( + anchor_fact_ids=None, + absorbed_fact_ids=None, + legacy_long_term_fact_ids=None, +) -> tuple[list[str], list[str]]: + """Normalize delayed-memory L4 references. + + Legacy ``long_term_facts_ids`` are treated as absorbed references. A fact + can only have one role inside a report; anchor wins when an id appears in + both lists so an important fact can never be hidden by the same report. + """ + + anchor_ids = normalize_long_term_fact_ids( + anchor_fact_ids or [] + ) + absorbed_ids = normalize_long_term_fact_ids([ + *normalize_long_term_fact_ids( + absorbed_fact_ids or [] + ), + *normalize_long_term_fact_ids( + legacy_long_term_fact_ids or [] + ), + ]) + anchor_set = set(anchor_ids) + absorbed_ids = [ + fact_id + for fact_id in absorbed_ids + if fact_id not in anchor_set + ] + + return anchor_ids, absorbed_ids + + def collect_long_term_fact_ids_from_reports( reports, ) -> set[str]: + """Compatibility name: return only facts absorbed by delayed memory.""" if not isinstance(reports, dict): return set() @@ -62,18 +97,48 @@ def collect_long_term_fact_ids_from_reports( if not isinstance(report, dict): continue - fact_ids.update( - normalize_long_term_fact_ids( - report.get( - "long_term_facts_ids", - [], - ) - ) + _anchor_ids, absorbed_ids = normalize_delayed_memory_fact_roles( + report.get("anchor_fact_ids", []), + report.get("absorbed_fact_ids", []), + report.get("long_term_facts_ids", []), ) + fact_ids.update(absorbed_ids) return fact_ids +def collect_anchor_fact_report_ids( + reports, +) -> dict[str, list[str]]: + """Map each anchor L4 fact id to delayed-memory report ids referencing it.""" + + if not isinstance(reports, dict): + return {} + + report_ids_by_fact: dict[str, list[str]] = {} + + for report_id, report in reports.items(): + if not isinstance(report, dict): + continue + + normalized_report_id = str(report_id or "").strip().casefold() + if not normalized_report_id: + continue + + anchor_ids, _absorbed_ids = normalize_delayed_memory_fact_roles( + report.get("anchor_fact_ids", []), + report.get("absorbed_fact_ids", []), + report.get("long_term_facts_ids", []), + ) + + for fact_id in anchor_ids: + report_ids_by_fact.setdefault(fact_id, []) + if normalized_report_id not in report_ids_by_fact[fact_id]: + report_ids_by_fact[fact_id].append(normalized_report_id) + + return report_ids_by_fact + + def parse_delayed_memory_content_payload( payload: str, *, @@ -135,7 +200,11 @@ def parse_delayed_memory_content_payload( ) if part ).strip() - elif field_name == "long_term_facts_ids": + elif field_name in { + "anchor_fact_ids", + "absorbed_fact_ids", + "long_term_facts_ids", + }: value = normalize_long_term_fact_ids( " ".join( part @@ -174,6 +243,12 @@ def parse_delayed_memory_content_payload( if tag.strip() ] + anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + fields.get("anchor_fact_ids", []), + fields.get("absorbed_fact_ids", []), + fields.get("long_term_facts_ids", []), + ) + report_id = generate_delayed_memory_report_id( () ) @@ -197,12 +272,8 @@ def parse_delayed_memory_content_payload( or "" ).strip(), "pinned": False, - "long_term_facts_ids": normalize_long_term_fact_ids( - fields.get( - "long_term_facts_ids", - [], - ) - ), + "anchor_fact_ids": anchor_fact_ids, + "absorbed_fact_ids": absorbed_fact_ids, "created_session_id": str( created_session_id or "" diff --git a/utils/actions/update_delayed_memory_utils.py b/utils/actions/update_delayed_memory_utils.py index 1f6db094..e9c03280 100644 --- a/utils/actions/update_delayed_memory_utils.py +++ b/utils/actions/update_delayed_memory_utils.py @@ -1,27 +1,27 @@ from __future__ import annotations -import re +import json from .delayed_memory_utils import is_delayed_memory_report_id -from .save_delayed_memory_utils import normalize_long_term_fact_ids +from .save_delayed_memory_utils import ( + normalize_delayed_memory_fact_roles, + normalize_long_term_fact_ids, +) -UPDATE_DELAYED_MEMORY_FIELD_RE = re.compile( - r"(?im)^[^\S\r\n]*(tags|body|long_term_facts_ids)" - r"[^\S\r\n]*:[^\S\r\n]*(.*)$", -) +UPDATE_DELAYED_MEMORY_FIELDS = frozenset({ + "tags", + "body", + "anchor_fact_ids", + "absorbed_fact_ids", +}) def _normalize_tags(value) -> list[str]: if isinstance(value, list): source = value else: - text = str(value or "").strip() - - if text.startswith("[") and text.endswith("]"): - text = text[1:-1] - - source = text.split(",") + source = str(value or "").split(",") tags = [] seen = set() @@ -48,52 +48,58 @@ def parse_update_delayed_memory_payload(payload: str) -> dict: first_line, separator, remainder = text.partition("\n") report_id = first_line.strip().casefold() - if not is_delayed_memory_report_id(report_id): + if not is_delayed_memory_report_id(report_id) or not separator: return {} - body_text = remainder if separator else "" - field_matches = list(UPDATE_DELAYED_MEMORY_FIELD_RE.finditer(body_text)) + try: + raw_update = json.loads(remainder.strip()) + except (json.JSONDecodeError, TypeError, ValueError): + return {} + + if not isinstance(raw_update, dict) or not raw_update: + return {} - if not field_matches: + unknown_fields = set(raw_update) - UPDATE_DELAYED_MEMORY_FIELDS + if unknown_fields: return {} - fields = {} - - for index, match in enumerate(field_matches): - field_name = str(match.group(1) or "").casefold() - inline_value = str(match.group(2) or "").strip() - next_start = ( - field_matches[index + 1].start() - if index + 1 < len(field_matches) - else len(body_text) - ) - block_value = body_text[match.end():next_start].strip("\n") - combined = "\n".join( - part - for part in (inline_value, block_value) - if part - ).strip() - - if field_name == "tags": - fields[field_name] = _normalize_tags(combined) - elif field_name == "long_term_facts_ids": - fields[field_name] = normalize_long_term_fact_ids(combined) - else: - fields[field_name] = combined - - tags = fields.get("tags", []) - fact_ids = fields.get("long_term_facts_ids", []) - body = str(fields.get("body", "") or "").strip() - - if not tags and not fact_ids and not body: + update = {"id": report_id} + + if "tags" in raw_update: + tags = _normalize_tags(raw_update.get("tags")) + if tags: + update["tags"] = tags + + if "body" in raw_update: + body = str(raw_update.get("body", "") or "").strip() + if body: + update["body"] = body + + requested_anchor_ids = ( + normalize_long_term_fact_ids(raw_update.get("anchor_fact_ids", [])) + if "anchor_fact_ids" in raw_update + else [] + ) + requested_absorbed_ids = ( + normalize_long_term_fact_ids(raw_update.get("absorbed_fact_ids", [])) + if "absorbed_fact_ids" in raw_update + else [] + ) + anchor_ids, absorbed_ids = normalize_delayed_memory_fact_roles( + requested_anchor_ids, + requested_absorbed_ids, + ) + + if "anchor_fact_ids" in raw_update and anchor_ids: + update["anchor_fact_ids"] = anchor_ids + + if "absorbed_fact_ids" in raw_update and absorbed_ids: + update["absorbed_fact_ids"] = absorbed_ids + + if len(update) == 1: return {} - return { - "id": report_id, - "tags": tags, - "long_term_facts_ids": fact_ids, - "body": body, - } + return update def build_update_delayed_memory_payload( diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 43512621..872b9916 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -61,7 +61,7 @@ def get_brain_runtime_config(): from xml.etree import ElementTree from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_APPEND_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, @@ -71,7 +71,7 @@ def get_brain_runtime_config(): RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_REMOVE_DELAYED_MEMORY, + RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_REMOVE_SKILL, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, @@ -113,6 +113,7 @@ def get_brain_runtime_config(): parse_delayed_memory_content_payload, parse_idle_seconds, normalize_jin_color_payload, + normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, refresh_active_memory_runtime_metadata, strip_active_memory_runtime_metadata, @@ -330,23 +331,35 @@ def build_delayed_memory_report( report_id ) - requested_l4_fact_ids = normalize_long_term_fact_ids( - value.get( - "long_term_facts_ids", - [], + requested_anchor_fact_ids, requested_absorbed_fact_ids = ( + normalize_delayed_memory_fact_roles( + value.get("anchor_fact_ids", []), + value.get("absorbed_fact_ids", []), + value.get("long_term_facts_ids", []), ) ) available_l4_fact_ids = get_runtime_l4_fact_ids( context ) + anchor_fact_ids = [ + fact_id + for fact_id in requested_anchor_fact_ids + if fact_id in available_l4_fact_ids + ] + absorbed_fact_ids = [ + fact_id + for fact_id in requested_absorbed_fact_ids + if fact_id in available_l4_fact_ids + ] + anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + anchor_fact_ids, + absorbed_fact_ids, + ) enriched_report[report_id] = { **value, - "long_term_facts_ids": [ - fact_id - for fact_id in requested_l4_fact_ids - if fact_id in available_l4_fact_ids - ], + "anchor_fact_ids": anchor_fact_ids, + "absorbed_fact_ids": absorbed_fact_ids, "pinned": bool(value.get("pinned", False)), "created_session_id": ( str( @@ -421,6 +434,7 @@ def build_delayed_memory_report( ) ), } + enriched_report[report_id].pop("long_term_facts_ids", None) return enriched_report @@ -2044,25 +2058,47 @@ def update_delayed_memory_report( ) available_l4_fact_ids = get_runtime_l4_fact_ids(context) - requested_l4_fact_ids = normalize_long_term_fact_ids( - update.get("long_term_facts_ids", []) + requested_anchor_fact_ids = normalize_long_term_fact_ids( + update.get("anchor_fact_ids", []) + ) + requested_absorbed_fact_ids = normalize_long_term_fact_ids( + update.get("absorbed_fact_ids", []) ) - accepted_l4_fact_ids = [ + accepted_anchor_fact_ids = [ fact_id - for fact_id in requested_l4_fact_ids + for fact_id in requested_anchor_fact_ids if fact_id in available_l4_fact_ids ] + accepted_absorbed_fact_ids = [ + fact_id + for fact_id in requested_absorbed_fact_ids + if fact_id in available_l4_fact_ids + ] + + current_anchor_fact_ids, current_absorbed_fact_ids = ( + normalize_delayed_memory_fact_roles( + report.get("anchor_fact_ids", []), + report.get("absorbed_fact_ids", []), + report.get("long_term_facts_ids", []), + ) + ) + next_anchor_fact_ids, next_absorbed_fact_ids = ( + normalize_delayed_memory_fact_roles( + [ + *current_anchor_fact_ids, + *accepted_anchor_fact_ids, + ], + [ + *current_absorbed_fact_ids, + *accepted_absorbed_fact_ids, + ], + ) + ) next_tags = _append_unique_delayed_memory_values( report.get("tags", []), update.get("tags", []), casefold=True, ) - next_fact_ids = normalize_long_term_fact_ids([ - *normalize_long_term_fact_ids( - report.get("long_term_facts_ids", []) - ), - *accepted_l4_fact_ids, - ]) appended_body = str(update.get("body", "") or "").strip() current_body = str(report.get("body", "") or "").rstrip() next_body = current_body @@ -2076,9 +2112,8 @@ def update_delayed_memory_report( if ( next_tags == list(report.get("tags", []) or []) - and next_fact_ids == normalize_long_term_fact_ids( - report.get("long_term_facts_ids", []) - ) + and next_anchor_fact_ids == current_anchor_fact_ids + and next_absorbed_fact_ids == current_absorbed_fact_ids and next_body == current_body ): return build_delayed_memory_failure_result( @@ -2090,9 +2125,11 @@ def update_delayed_memory_report( updated_report = { **report, "tags": next_tags, - "long_term_facts_ids": next_fact_ids, + "anchor_fact_ids": next_anchor_fact_ids, + "absorbed_fact_ids": next_absorbed_fact_ids, "body": next_body, } + updated_report.pop("long_term_facts_ids", None) reports[report_id] = updated_report appended_reports = get_appended_delayed_memory_report(context) @@ -2133,7 +2170,6 @@ def update_delayed_memory_report( "file_errors": file_errors, } - def include_pinned_delayed_memory_reports( context, ) -> dict: @@ -2331,16 +2367,16 @@ def build_delayed_memory_action_text( if action == "append_delayed_memory": return ( - f"Append failed: {title}" + f"Load failed: {title}" if failed - else f"Appending: {title}" + else f"Loading: {title}" ) if action == "remove_delayed_memory": return ( - f"Remove failed: {title}" + f"Unload failed: {title}" if failed - else f"Removing: {title}" + else f"Unloading: {title}" ) if action == "update_delayed_memory": @@ -2421,10 +2457,10 @@ def build_delayed_memory_history_text( return f"Delayed memory saved: {title}" if action == "append_delayed_memory": - return f"Delayed memory appended: {title}" + return f"Delayed memory loaded: {title}" if action == "remove_delayed_memory": - return f"Delayed memory removed from context: {title}" + return f"Delayed memory unloaded from context: {title}" if action == "update_delayed_memory": return f"Delayed memory updated: {title}" @@ -2933,4 +2969,3 @@ def has_zero_diff_stall_alert( ) - diff --git a/utils/context/delayed_memory.py b/utils/context/delayed_memory.py index 31e9b96d..4e368c25 100644 --- a/utils/context/delayed_memory.py +++ b/utils/context/delayed_memory.py @@ -94,7 +94,7 @@ def format_delayed_memory_result_sections( if result.get("ok") is False: sections.append( ( - "APPEND_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", format_delayed_memory_failure_result( result ), @@ -105,7 +105,7 @@ def format_delayed_memory_result_sections( if action == "remove_delayed_memory": sections.append( ( - "REMOVE_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", ( format_delayed_memory_failure_result if result.get("ok") is False diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index 5bb89316..d6a9d893 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -179,7 +179,7 @@ def _format_memory_action_context_part( if normalized_action in { "RESOLVE_ACTIVE_MEMORY", - "REMOVE_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", }: resolved_id = part_id or detail diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index 022ea3ee..e4e1732d 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -10,7 +10,7 @@ is_delayed_memory_report_id, ) from utils.actions.save_delayed_memory_utils import ( - normalize_long_term_fact_ids, + normalize_delayed_memory_fact_roles, ) @@ -138,6 +138,11 @@ def normalize_delayed_memory_report( or created_time, limit=MAX_DELAYED_MEMORY_TIME_CHARS, ) + anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + report.get("anchor_fact_ids", []), + report.get("absorbed_fact_ids", []), + report.get("long_term_facts_ids", []), + ) return normalized_id, { "title": title, @@ -153,12 +158,8 @@ def normalize_delayed_memory_report( limit=MAX_DELAYED_MEMORY_BODY_CHARS, ), "pinned": bool(report.get("pinned", False)), - "long_term_facts_ids": normalize_long_term_fact_ids( - report.get( - "long_term_facts_ids", - [], - ) - ), + "anchor_fact_ids": anchor_fact_ids, + "absorbed_fact_ids": absorbed_fact_ids, "created_session_id": _clean_text( report.get("created_session_id", "") or report.get("session", ""), @@ -185,7 +186,6 @@ def normalize_delayed_memory_report( ), } - def normalize_delayed_memory_reports(value) -> dict[str, dict]: if not isinstance(value, dict): @@ -250,22 +250,22 @@ def merge_delayed_memory_reports( ).intersection( primary_reports ): - reports[report_id][ - "long_term_facts_ids" - ] = normalize_long_term_fact_ids([ - *fallback_reports[report_id].get( - "long_term_facts_ids", - [], - ), - *primary_reports[report_id].get( - "long_term_facts_ids", - [], - ), - ]) + anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + [ + *fallback_reports[report_id].get("anchor_fact_ids", []), + *primary_reports[report_id].get("anchor_fact_ids", []), + ], + [ + *fallback_reports[report_id].get("absorbed_fact_ids", []), + *primary_reports[report_id].get("absorbed_fact_ids", []), + ], + ) + reports[report_id]["anchor_fact_ids"] = anchor_fact_ids + reports[report_id]["absorbed_fact_ids"] = absorbed_fact_ids + reports[report_id].pop("long_term_facts_ids", None) return reports - def delayed_memory_filename( report_id: str, title: str, @@ -322,8 +322,11 @@ def build_delayed_memory_file_payload( ], "body": clean_report["body"], "pinned": clean_report["pinned"], - "long_term_facts_ids": clean_report[ - "long_term_facts_ids" + "anchor_fact_ids": clean_report[ + "anchor_fact_ids" + ], + "absorbed_fact_ids": clean_report[ + "absorbed_fact_ids" ], "appended_times": clean_report["appended_times"], "append_streak": clean_report["append_streak"], diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index 81a9768e..843c2a8e 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -123,7 +123,7 @@ def get_current_action_sequence_started_at( ACTION_DISPLAY_ALIASES = { "append_asset_file": "Appended asset file", - "append_delayed_memory": "Appended delayed memory", + "append_delayed_memory": "Loaded delayed memory", "append_skill": "Appended skill", "append_wildcard_file": "Appended wildcard file", "asset_action": "Asset action", @@ -136,11 +136,13 @@ def get_current_action_sequence_started_at( "generate_prompt_batch": "Generated prompt batch", "list_skills": "Listed skills", "list_wildcards": "Listed wildcards", + "load_delayed_memory": "Loaded delayed memory", "preview_file": "Previewed file", "read_asset_file": "Read asset file", "read_asset_text": "Read asset text", - "remove_delayed_memory": "Removed delayed memory", + "remove_delayed_memory": "Unloaded delayed memory", "remove_skill": "Removed skill", + "unload_delayed_memory": "Unloaded delayed memory", "resolve_active_memory": "Resolved active memory", "run_document_reader": "Read document iteratively", "run_python_skill": "Ran Python skill", @@ -160,6 +162,7 @@ def get_current_action_sequence_started_at( "generate": "Generated", "hide": "Hidden", "list": "Listed", + "load": "Loaded", "preview": "Previewed", "read": "Read", "remove": "Removed", @@ -167,6 +170,7 @@ def get_current_action_sequence_started_at( "run": "Ran", "sample": "Sampled", "save": "Saved", + "unload": "Unloaded", "update": "Updated", "write": "Wrote", } @@ -1045,8 +1049,8 @@ def _build_session_action_marker_detail( "SAVE_ACTIVE_MEMORY", "RESOLVE_ACTIVE_MEMORY", "IDLE", - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", }: return normalized_payload @@ -1077,8 +1081,8 @@ def _build_session_action_marker_detail( "SAVE_ACTIVE_MEMORY", "RESOLVE_ACTIVE_MEMORY", "SAVE_DELAYED_MEMORY_CONTENT", - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", } @@ -1210,8 +1214,8 @@ def _build_payload_distinct_session_action_parts( if ( action_name in { - "APPEND_DELAYED_MEMORY", - "REMOVE_DELAYED_MEMORY", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", "UPDATE_DELAYED_MEMORY", } and payload_key From 89433923c1a3df0f2f0be15c1dd27a722c4c1f38 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:47:16 +0300 Subject: [PATCH 023/168] feature: L4 memory layer - fix tests --- tests/test_brain_prompt_memory.py | 24 +--- tests/test_chat_runtime_marker_ui.py | 2 +- ...ayed_memory_pinned_glow_client_contract.py | 2 +- tests/test_l2_memory.py | 105 ++++++++---------- tests/test_l4_logger_cards_client_contract.py | 2 +- ..._memory_citation_layers_client_contract.py | 8 +- ...t_memory_reference_sync_client_contract.py | 4 +- ..._runtime_avatar_l4_ring_client_contract.py | 4 +- 8 files changed, 63 insertions(+), 88 deletions(-) diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index cb4a2396..2e96368c 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -1121,11 +1121,7 @@ def test_brain_prompt_includes_conversation_activity(self): runtime_l2_memory=( "possible pattern: repeated greeting loop; Occurrences: 3" ), - runtime_l2_pending_patches=[ - { - "total_diff": 29.85, - }, - ], + runtime_conversation_activity_diff=29.85, runtime_zero_diff_alert=None, deep_thought_count=0, runtime_search_result="", @@ -1187,11 +1183,7 @@ def test_brain_prompt_marks_critical_conversation_activity(self): runtime_l2_memory=( "possible pattern: repeated greeting loop; Occurrences: 4" ), - runtime_l2_pending_patches=[ - { - "total_diff": 9.85, - }, - ], + runtime_conversation_activity_diff=9.85, runtime_zero_diff_alert=None, deep_thought_count=0, runtime_search_result="", @@ -1235,11 +1227,7 @@ def test_brain_prompt_marks_activity_below_twenty_as_critical(self): context = SimpleNamespace( runtime_memory="topic: active loop diagnostics", runtime_l2_memory="", - runtime_l2_pending_patches=[ - { - "total_diff": 19, - }, - ], + runtime_conversation_activity_diff=19, runtime_zero_diff_alert=None, deep_thought_count=0, runtime_search_result="", @@ -1271,11 +1259,7 @@ def test_brain_prompt_caps_conversation_activity_at_full(self): context = SimpleNamespace( runtime_memory="topic: active exchange", runtime_l2_memory="", - runtime_l2_pending_patches=[ - { - "total_diff": 142, - }, - ], + runtime_conversation_activity_diff=142, runtime_zero_diff_alert=None, deep_thought_count=0, runtime_search_result="", diff --git a/tests/test_chat_runtime_marker_ui.py b/tests/test_chat_runtime_marker_ui.py index 9b1c9ac5..c3b372f2 100644 --- a/tests/test_chat_runtime_marker_ui.py +++ b/tests/test_chat_runtime_marker_ui.py @@ -79,7 +79,7 @@ def test_chat_script_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/js/chat.js?v=chat-core-6', + '/static/js/chat.js?v=memory-reference-sync-2', source, ) diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index 58903007..7da21dee 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,7 +47,7 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-reference-sync-1", source) + self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-2", source) self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-17", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) diff --git a/tests/test_l2_memory.py b/tests/test_l2_memory.py index d4114358..822d00bd 100644 --- a/tests/test_l2_memory.py +++ b/tests/test_l2_memory.py @@ -6,7 +6,6 @@ BEHAVIOR_VS_INTENT, CONFIRMABLE_KEYS, EVIDENCE_LINE_LIFECYCLE, - L2_PATCH_WINDOW, OCCURRENCE_COUNTING, OUTPUT_FORMAT, PATTERN_EVIDENCE_LINES, @@ -208,9 +207,7 @@ async def test_runtime_l1_diff_log_formats_float_noise(self): 1, ) self.assertIn( - "[MEMORY:L1] L1 diff +167.3; " - "recent diffs [4.65, 296.85, 167.3]; " - "avg 156.27; range 292.2;", + "[MEMORY:L1] L1 diff +167.3", logger.service_logs[0], ) self.assertNotIn( @@ -230,7 +227,7 @@ async def test_l2_memory_waits_for_repeated_patch_keys(self): }, logger=logger, runtime_l2_memory="", - runtime_l2_pending_patches=[ + runtime_l1_diff_history=[ { "turn_number": 1, "snapshot_index": 1, @@ -298,7 +295,7 @@ async def test_l2_memory_waits_for_repeated_patch_keys(self): }, ], runtime_l2_last_turn=0, - user_message_count=L2_PATCH_WINDOW, + user_message_count=5, ) updated_memory = await maybe_summarize_runtime_l2_memory( @@ -337,47 +334,51 @@ async def test_l2_memory_keeps_evidence_but_drops_unconfirmed_pattern(self): runtime_memory_snapshots=[], runtime_memory_snapshot_index=0, runtime_l2_memory="", - runtime_l2_pending_patches=[ + runtime_l1_diff_history=[ { - "turn_number": index + 1, - "snapshot_index": index + 1, + "turn_number": 1, + "snapshot_index": 1, "total_diff": 120, "changes": { "added": [ { - "key": "topic", - "value": f"value {index}", + "key": "diagnostic_signal", + "value": "first repeated key episode", }, ], }, - } - for index in range(L2_PATCH_WINDOW - 1) - ] + [ + }, { - "turn_number": 10, - "snapshot_index": 10, - "total_diff": 140, - "user_message": "ping", - "user_messages": [ - "ping", - ], + "turn_number": 2, + "snapshot_index": 2, + "total_diff": 80, "changes": { "added": [ { - "key": "topic", - "value": "value final", + "key": "status", + "value": "separator episode", }, + ], + }, + }, + { + "turn_number": 3, + "snapshot_index": 3, + "total_diff": 140, + "changes": { + "changed": [ { - "key": "user_message", - "value": "ping", + "previous_key": "diagnostic_signal", + "previous_value": "first repeated key episode", + "current_key": "diagnostic_signal", + "current_value": "second repeated key episode", }, ], }, }, ], - runtime_l1_diff_history=[], runtime_l2_last_turn=0, - user_message_count=L2_PATCH_WINDOW, + user_message_count=3, ) updated_memory = await maybe_summarize_runtime_l2_memory( @@ -425,7 +426,7 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel runtime_memory_snapshots=[], runtime_memory_snapshot_index=0, runtime_l2_memory="", - runtime_l2_pending_patches=[ + runtime_l1_diff_history=[ { "turn_number": 1, "snapshot_index": 1, @@ -433,7 +434,7 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel "changes": { "added": [ { - "key": "topic", + "key": "diagnostic_signal", "value": "early broad update", }, ], @@ -461,9 +462,9 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel "changes": { "changed": [ { - "previous_key": "topic", + "previous_key": "diagnostic_signal", "previous_value": "large rewrite", - "current_key": "topic", + "current_key": "diagnostic_signal", "current_value": "memory mechanics", }, ], @@ -476,9 +477,9 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel "changes": { "changed": [ { - "previous_key": "topic", + "previous_key": "diagnostic_signal", "previous_value": "memory mechanics", - "current_key": "topic", + "current_key": "diagnostic_signal", "current_value": "pattern trigger", }, ], @@ -491,9 +492,9 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel "changes": { "changed": [ { - "previous_key": "topic", + "previous_key": "diagnostic_signal", "previous_value": "pattern trigger", - "current_key": "topic", + "current_key": "diagnostic_signal", "current_value": "L2 window", }, ], @@ -521,25 +522,15 @@ async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(sel "changes": { "changed": [ { - "previous_key": "topic", + "previous_key": "diagnostic_signal", "previous_value": "L2 window", - "current_key": "topic", + "current_key": "diagnostic_signal", "current_value": "repeated keys", }, ], }, }, ], - runtime_l1_diff_history=[ - { - "snapshot_index": 1, - "total_diff": 110, - }, - { - "snapshot_index": 7, - "total_diff": 104.69, - }, - ], runtime_l2_last_turn=0, user_message_count=7, session_id="test-session", @@ -576,28 +567,28 @@ async def emit(event): context.runtime_l2_last_turn, 7, ) - self.assertEqual( - context.runtime_l2_pending_patches, - [], - ) self.assertEqual( len(context.runtime_l1_diff_history), - 2, + 7, + ) + self.assertIn( + "Selected L1 recurrence evidence", + service_client.calls[0]["user_prompt"], ) self.assertIn( - "Recent L1 patches", + "signal added: diagnostic_signal: early broad update", service_client.calls[0]["user_prompt"], ) self.assertIn( - "total_diff: 199.05", + "signal changed: diagnostic_signal: memory mechanics", service_client.calls[0]["user_prompt"], ) self.assertNotIn( - "total_diff: 110", + "large rewrite", service_client.calls[0]["user_prompt"], ) self.assertNotIn( - "total_diff: 254", + "adjust trigger", service_client.calls[0]["user_prompt"], ) self.assertEqual( @@ -609,7 +600,7 @@ async def emit(event): logger.summarizer_logs[0][1], ) self.assertIn( - "total_diff: 199.05", + "signal added: diagnostic_signal: early broad update", logger.summarizer_logs[0][1], ) self.assertEqual( diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index 2bfcc6bc..ab14f7f6 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -64,7 +64,7 @@ def test_restore_message_round_trip_is_registered(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=l4-logger-cards-2', source) + self.assertIn('/static/css/runtime-memory.css?v=memory-hover-sync-2', source) self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-restore-1', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index bc12ce56..f6efd78b 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -111,11 +111,11 @@ def test_win95_keeps_reference_and_citation_hits_consistent(self): def test_cache_versions_are_bumped_for_citation_sync_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-reference-sync-1", source) + self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-2", source) self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-14", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-7", source) - self.assertIn("/static/js/think-citations.js?v=think-citations-3", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-18", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-11", source) + self.assertIn("/static/js/think-citations.js?v=think-citations-4", source) self.assertIn("/static/js/chat.js?v=memory-reference-sync-2", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 4091e2b2..f2090598 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -143,11 +143,11 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=memory-reference-sync-1', + '/static/css/runtime-memory.css?v=memory-hover-sync-2', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-14', + '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-18', source, ) self.assertIn( diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 3bc6e1b9..90f11c20 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -61,11 +61,11 @@ def test_avatar_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/css/runtime-avatar.css?v=memory-rings-3", + "/static/css/runtime-avatar.css?v=memory-rings-4", source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-6", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-11", source, ) From 82b71e8c76f2c49c407fdc13e6e09fb451a89ca1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:22:51 +0300 Subject: [PATCH 024/168] feature: L4 memory layer - replace delayed memory absorbed facts with facts_ids - document live avatar behavior - fix tests --- LIVE_AVATAR.md | 658 ++++++++++++++++++ README.md | 1 + agent/nodes/brain.py | 21 +- contracts/save_delayed_memory.json | 10 +- contracts/update_delayed_memory.json | 13 +- runtime/L4_memory.py | 290 +++++++- runtime/stream.py | 55 ++ tests/runtime_actions/test_active_memory.py | 97 +++ tests/runtime_actions/test_delayed_memory.py | 57 +- .../test_skill_marker_semantics.py | 47 ++ tests/runtime_actions/test_update_l4_facts.py | 6 +- tests/test_brain_asset_flow.py | 42 +- tests/test_brain_runtime_actions.py | 100 ++- tests/test_delayed_memory_client_contract.py | 2 +- tests/test_delayed_memory_file_store.py | 72 +- ...ayed_memory_pinned_glow_client_contract.py | 4 +- tests/test_l4_logger_cards_client_contract.py | 20 +- tests/test_l4_memory.py | 165 ++++- ..._memory_citation_layers_client_contract.py | 8 +- ...t_memory_reference_sync_client_contract.py | 231 +++++- ..._runtime_avatar_l4_ring_client_contract.py | 47 +- ui/static/css/runtime-avatar.css | 69 +- ui/static/css/runtime-memory.css | 59 ++ ui/static/js/logger/log-entries.js | 3 + ui/static/js/runtime/runtime-avatar.js | 355 +++++++++- ui/static/js/runtime/runtime-l4-memory.js | 207 +++++- ui/static/js/runtime/runtime-memory-view.js | 596 +++++++++++++++- ui/static/js/runtime/runtime-storage.js | 54 +- ui/static/js/runtime/runtime.js | 225 +++++- ui/static/js/socket/delayed-memory.js | 51 +- ui/static/js/socket/event-handlers.js | 8 +- ui/static/js/think-citations.js | 9 +- ui/templates/index.html | 22 +- utils/actions/__init__.py | 2 + utils/actions/active_memory_actions.py | 65 +- utils/actions/dispatcher.py | 22 + utils/actions/save_delayed_memory_utils.py | 95 ++- utils/actions/update_delayed_memory_utils.py | 23 +- utils/brain_client_utils.py | 57 +- utils/context/context_exports.py | 2 + utils/context/session_actions.py | 34 + utils/delayed_memory_file_store.py | 24 +- utils/session_actions_history.py | 177 ++++- websocket/bootstrap.py | 8 +- 44 files changed, 3791 insertions(+), 322 deletions(-) create mode 100644 LIVE_AVATAR.md diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md new file mode 100644 index 00000000..9bceb20a --- /dev/null +++ b/LIVE_AVATAR.md @@ -0,0 +1,658 @@ +LIVE_AVATAR v1.0 + +# Live Avatar Visual Manual + +This manual documents the visual side of the live avatar: what each part on the avatar means, how it reacts to memory state, and where to change each visual behavior. + +It is intentionally not a full architecture document. The goal is practical: if you look at the avatar and ask "what is this ring?" or "where do I make this brighter, faster, wider, or calmer?", this file should point you to the exact place. + +## The Mental Picture + +The live avatar is a memory radar. The inner moving rings represent the current runtime memory snapshot. The outer dashed rings represent memory systems around that snapshot: L4 long-term facts, delayed reports, and active memory. The center is the JIN accent light; it also feeds the scene tint. + +Read it like this: + +| Visual cue | Meaning | +|-----------------------------|---| +| Inner moving rings | Current runtime memory lines | +| Faster inner motion | Larger runtime memory diff | +| Color shifts in inner rings | Keyword/emotional content in runtime memory | +| Small nodes and stripes | Pending or long/notable runtime memory details | +| L4 blue dashes | Long-term facts | +| Dim L4 dots | L4 facts already represented by delayed reports | +| Delayed report dashes | Stored delayed memory reports | +| Bright delayed dashes | Pinned delayed memory reports | +| Bright outer dashes | Active memory records | +| Center color | JIN accent color and scene tint | +| Glow on hover/citation | A memory item is being pointed at right now | + +## Main Files To Touch + +Most visual changes happen in one file: + +`ui/static/js/runtime/runtime-avatar.js` + +Use this file for rings, radii, colors, speed, dash opacity, seeded random texture, center rendering, and avatar-level event reactions. + +The CSS glow and panel presentation live here: + +`ui/static/css/runtime-avatar.css` + +Use this file for hover glow, citation glow, memory dash glow, panel collapsed behavior, and reduced-motion behavior. + +A few visual knobs are outside those two files: + +| Need | File | Change | +|---|---|---| +| Avatar panel size | `ui/static/css/base.css` | `--runtime-avatar-panel-size` | +| Center color tint applied to scene | `ui/static/js/runtime/runtime-avatar.js` | `setCenterColor()`, `JIN_SCENE_COLOR_INTENSITY` | +| L4 archived/visible classification | `ui/static/js/runtime/runtime-l4-memory.js` | `getVisibleFacts()`, `getFactsWithArchiveState()` | +| Long-term facts panel visibility | `ui/static/js/runtime/runtime.js` | `getVisibleLongTermMemoryFacts()` | +| Browser cache after visual edits | `ui/templates/index.html` | bump query string for changed CSS/JS | + +## Visual Stack + +The avatar is drawn as a single SVG inside `#jin-runtime-avatar`. Each render replaces the old SVG with a fresh one. + +The stack is built in this order: + +1. SVG definitions: gradients and glow filters. +2. Static scaffold: faint background circles and radial guide lines. +3. Runtime rings: one moving orbit per runtime memory line. +4. Memory signal rings: L4, delayed, active dashes. +5. Center core: the central light and glow. +6. Runtime/citation/reference/hover classes are reapplied. + +The main render function is: + +```js +renderAvatar(snapshot, options = {}) +``` + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Important helpers: + +| Helper | Role | +|---|---| +| `appendDefs()` | SVG filters and gradients | +| `appendStaticScaffold()` | background radar structure | +| `computeRingRecords()` | turns runtime lines into orbit records | +| `appendOrbit()` | draws one runtime memory orbit | +| `appendMemorySignalRings()` | draws L4, delayed, active dash rings | +| `appendCenter()` | draws center core | +| `applyThinkRuntimeCitationGlow()` | applies citation glow | +| `applyMemoryReferenceGlow()` | applies reference glow | +| `applyMemoryRowAvatarHoverGlow()` | applies hover glow | + +## Static Scaffold + +The scaffold is the quiet radar structure behind the live memory rings. It does not represent a memory item directly. It gives the avatar depth and makes the active rings easier to read. + +It is drawn by: + +```js +appendStaticScaffold(svg, overallColor, random) +``` + +Change these when you want the background to feel denser, cleaner, brighter, or more technical: + +| Visual part | Where | +|---|---| +| Concentric scaffold circles | `STATIC_SCAFFOLD_RADII` | +| Radial guide line range | `STATIC_RADIAL_LINE_INNER_RADIUS`, `STATIC_RADIAL_LINE_OUTER_RADIUS` | +| Scaffold circle opacity | `appendStaticScaffold()` circle attributes | +| Radial line opacity | `appendStaticScaffold()` line attributes | +| Warm radial accents | `AMBER_ACCENT` and `index % 5` logic | + +Good edit examples: + +- To make the avatar calmer, lower scaffold `stroke-opacity`. +- To make it more technical, add more scaffold radii or increase dash visibility. +- To make it less noisy, reduce radial guide lines or their opacity. + +## Runtime Rings + +Runtime rings are the inner moving orbits. They are tied to the current runtime memory snapshot. Each visible runtime memory line becomes one ring. + +These rings are the most "alive" part of the avatar: they change radius, speed, color, dash texture, and decoration based on current runtime memory. + +### Runtime Ring Meaning + +| Visual signal | Meaning | +|---|---| +| Number of rings | Number of runtime memory lines | +| Ring radius | Relative length of that memory line | +| Ring speed | Snapshot diff intensity | +| Ring direction | Seeded random direction | +| Ring color | Keyword palette plus emotional/alert influence | +| Extra stripes | The line is long compared to the others | +| Small colored nodes | The line contains pending-related language | + +### Runtime Ring Controls + +| Change | File | Exact place | +|---|---|---| +| Inner/outer radius range | `runtime-avatar.js` | `MIN_RING_RADIUS`, `MAX_RING_RADIUS` | +| How radius is calculated | `runtime-avatar.js` | `computeRingRecords()` | +| Speed | `runtime-avatar.js` | `appendOrbit()` -> `baseSpeed`, `effectiveSpeed`, `duration` | +| Dash texture | `runtime-avatar.js` | `appendOrbit()` -> `dashLength`, `gapLength`, `strokeWidth` | +| Arc fragments | `runtime-avatar.js` | `appendOrbit()` -> `arcCount`, `appendArcCircle()` | +| Long-line stripes | `runtime-avatar.js` | `appendLongFieldStripes()` | +| Pending nodes | `runtime-avatar.js` | `appendPendingNodes()` | + +### Runtime Ring Speed + +The runtime orbit speed is based on snapshot diff: + +```js +const baseSpeed = 11 + random() * 36; +const effectiveSpeed = baseSpeed * (diffPercent / 100); +const duration = effectiveSpeed > 0.05 ? 360 / effectiveSpeed : 9999; +``` + +If the runtime memory barely changed, the rings nearly stop. If the diff is high, they move faster. + +To make runtime rings calmer: + +```js +const baseSpeed = 6 + random() * 18; +``` + +To make them more energetic: + +```js +const baseSpeed = 18 + random() * 52; +``` + +## Runtime Colors + +Runtime colors come from a weighted palette. The avatar looks at text in the current runtime snapshot and blends colors if specific words appear. + +The default palettes live near the top of: + +`ui/static/js/runtime/runtime-avatar.js` + +| Palette | What it does | +|---|---| +| `KEYWORD_PALETTE` | Softly pushes rings toward thematic colors | +| `AGGRESSIVE_PALETTE` | Strong alert-like coloring for high-priority words | +| `PENDING_NODE_PALETTE` | Colors small pending nodes | + +Current keyword palette: + +| Words | Color | +|---|---| +| `jin`, `runtime` | `#22d9b5` | +| `user` | `#9276d8` | +| `memory` | `#e1a449` | + +Current aggressive palette: + +| Words | Color | +|---|---| +| `angry`, `aggressive` | `#ff0000` | + +To add a soft visual theme: + +```js +[["project", "repo", "code"], "#65c99a"] +``` + +To add a warning theme: + +```js +[["error", "failure", "blocked"], "#ff4b4b"] +``` + +## Memory Signal Rings + +The memory signal rings are the outer dashed rings. They are not runtime memory lines. They represent separate memory systems around the current runtime state. + +They are configured in: + +```js +MEMORY_RING_LAYOUT +``` + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +| Ring | Radius | Meaning | +|---|---:|---| +| L4 | `156` | Long-term facts | +| Delayed | `166` | Delayed memory reports | +| Active | `176` | Active memory records | + +Each item becomes one dash. So 10 L4 facts means 10 L4 dashes. 3 delayed reports means 3 delayed dashes. + +### Memory Ring Layout Controls + +| Field | Meaning | +|---|---| +| `radius` | How far from center the ring sits | +| `strokeWidth` | Thickness of dash stroke | +| `minArcDegrees` | Smallest dash length | +| `maxArcDegrees` | Largest dash length | +| `arcRatio` | How much of each item slot is filled | +| `startAngle` | Where the first dash starts | + +Use these fields when you want to move a ring inward/outward or change how dense the dashes feel. + +## L4 Facts Ring + +The L4 ring shows long-term facts. Each dash is one L4 fact. + +There are two visual states: + +| L4 state | Meaning | Avatar | +|---|---|---| +| Visible fact | Fact is directly available in long-term context | normal blue dash | +| Archived fact | Fact is represented by a delayed report | dim blue dash | + +Current opacity: + +```js +opacity: record.archived ? 0.26 : 0.52 +``` + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Place: + +L4 branch inside `appendMemorySignalRing()`. + +### L4 Archived Meaning + +Archived does not mean deleted. It means the fact is already covered by a delayed report and should not be shown as a direct long-term context line. + +Visual behavior: + +- It stays visible in the avatar. +- It becomes half as bright. +- It disappears from the long-term facts panel. + +Anchor facts are the exception. If a delayed report lists a fact in `anchor_fact_ids`, that fact remains visible and is not dimmed. + +### L4 Visual Edit Guide + +| Desired change | File | Edit | +|---|---|---| +| Make archived facts dimmer | `runtime-avatar.js` | lower `0.26` | +| Make archived facts brighter | `runtime-avatar.js` | raise `0.26` | +| Make all L4 facts brighter | `runtime-avatar.js` | raise `0.52` | +| Change L4 color | `runtime-avatar.js` | `L4_MEMORY_RING_COLOR` | +| Move L4 ring | `runtime-avatar.js` | `MEMORY_RING_LAYOUT.l4.radius` | +| Make L4 dashes longer | `runtime-avatar.js` | `MEMORY_RING_LAYOUT.l4.arcRatio` or `maxArcDegrees` | +| Hide archived facts from avatar entirely | `runtime-avatar.js` | use `getVisibleFacts()` instead of `getFactsWithArchiveState()` in `getL4MemoryAvatarRecords()` | + +The archived/visible classification itself is in: + +`ui/static/js/runtime/runtime-l4-memory.js` + +Important functions: + +| Function | Role | +|---|---| +| `getArchivedFactIdSet()` | Collects hidden fact ids from delayed reports | +| `factMatchesArchivedIds()` | Checks direct id and `source_fact_ids` | +| `getVisibleFacts()` | Returns only facts visible in long-term panel | +| `getFactsWithArchiveState()` | Returns all facts with `archived` flag for avatar | + +## Delayed Reports Ring + +The delayed ring shows delayed memory reports. Each dash is one report. + +Normal reports are dimmer. Pinned reports are much brighter, almost white. + +| Report state | Avatar opacity | Color | +|---|---:|---| +| Normal | `0.36` | `DELAYED_MEMORY_RING_COLOR` mixed with overall avatar color | +| Pinned | `0.82` | `PINNED_DELAYED_MEMORY_RING_COLOR` | + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Places: + +- delayed branch inside `appendMemorySignalRing()` +- live pin update in `setDelayedMemoryDashPinned()` + +Use this section when you want reports to feel more or less present. + +| Desired change | Edit | +|---|---| +| Make normal reports more visible | raise `0.36` | +| Make pinned reports less intense | lower `0.82` | +| Make pinned color less white | change `PINNED_DELAYED_MEMORY_RING_COLOR` | +| Move delayed ring | change `MEMORY_RING_LAYOUT.delayed.radius` | +| Make report dashes thicker | change `MEMORY_RING_LAYOUT.delayed.strokeWidth` | + +## Active Memory Ring + +The active ring shows active memory records. This is the outermost memory signal ring and is intentionally bright. + +Current behavior: + +| Visual rule | Value | +|---|---| +| Color | `ACTIVE_MEMORY_RING_COLOR` | +| Opacity | `0.76` | +| Radius | `MEMORY_RING_LAYOUT.active.radius` = `176` | + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Places: + +- `getActiveMemoryAvatarRecords()` +- active branch inside `appendMemorySignalRing()` +- `MEMORY_RING_LAYOUT.active` + +Active memory records come from strings like: + +```text +active_memory: ... +active_memory_2: ... +``` + +If you want active memory to feel less dominant, lower opacity or move the ring inward. + +## Center Core + +The center is a visual anchor. It is drawn after all rings and sits on top. + +It is made of: + +- three thin circles; +- a soft radial glow; +- a soft inner circle; +- a core; +- a bright point. + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Main places: + +| Visual part | Edit | +|---|---| +| Center circles | `appendCenter()` | +| Core radius/opacity | `appendCenter()` | +| Default color | `DEFAULT_CENTER_COLOR` | +| Color transition speed | `CENTER_COLOR_STEP_MS` | +| Scene tint strength | `JIN_SCENE_COLOR_INTENSITY` | + +`setCenterColor()` also updates page-level CSS variables: + +| Variable | Meaning | +|---|---| +| `--jin-color` | Current JIN accent | +| `--scene-base-color` | Scene background tint | +| `--scene-jin-tint-alpha` | Scene tint opacity | + +Lower `JIN_SCENE_COLOR_INTENSITY` if the center color affects the page too much. + +## Glow States + +Glow is mostly CSS. JavaScript only decides which class to apply. + +File: + +`ui/static/css/runtime-avatar.css` + +| Class | Trigger | Meaning | +|---|---|---| +| `is-memory-hover-hit` | Hovering the matching memory row | "User is pointing at this memory item" | +| `is-runtime-cited` | Hovering or activating a think citation | "This memory item is cited" | +| `is-memory-reference-hit` | JIN response text references a unique alias | "This memory item was mentioned" | +| `is-memory-pinned` | Delayed report is pinned | "This report is important" | +| `is-memory-archived` | L4 fact is archived by report | Marker for hidden-from-context facts | + +Use CSS for brightness, drop-shadow, stroke-width, and transition feel. Use JS only if the matching logic changes. + +## Hover Matching + +Hover matching connects rows in the memory panel to shapes in the avatar. + +The shared identity is: + +```js +buildAvatarMemoryHoverId(kind, id) +``` + +File: + +`ui/static/js/runtime/runtime-core.js` + +Shapes: + +| Memory type | Hover id shape | +|---|---| +| Runtime line | `runtime:` | +| L4 fact | `l4:` | +| Delayed report | `delayed:` | +| Active memory | `active:` | + +L4 and delayed memory also cross-highlight through their fact links. Hovering an L4 fact lights any delayed report that lists the fact in `anchor_fact_ids`. Hovering a delayed report lights the related L4 dots and dashes from its `facts_ids`, if the report has linked facts. + +If hover glow stops working, check that the row and the SVG node have the same `data-avatar-memory-hover-id`. + +## Citation Matching + +Citation matching connects think citations to avatar shapes. + +For visual changes, edit CSS: + +`ui/static/css/runtime-avatar.css` + +For matching logic, edit: + +`ui/static/js/runtime/runtime-avatar.js` + +Function: + +```js +applyThinkRuntimeCitationGlow() +``` + +For L4 facts, matching uses a strict identity tuple: + +```js +buildCitationRecordIdentity(id, key, value) +``` + +This prevents two facts with the same key from glowing incorrectly. + +## Central Button + +The center button is visually part of the avatar. It currently refreshes the avatar drawing. + +DOM id: + +`#fact-check-trigger` + +Visual style: + +`ui/static/css/runtime-avatar.css` + +Click behavior: + +`ui/static/js/socket/input.js` + +Current click behavior: + +```js +window.JinRuntime.avatar.refresh() +``` + +The old manual fact-check WebSocket path still exists separately, but the center click currently redraws the avatar rather than starting fact-check. + +## Edit Recipes + +### Make archived L4 facts almost invisible + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Change: + +```js +opacity: record.archived ? 0.12 : 0.52 +``` + +### Make normal delayed reports stronger + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Change: + +```js +opacity: pinned ? 0.82 : 0.50 +``` + +### Make active memory less dominant + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Change active opacity: + +```js +opacity: 0.58 +``` + +Or move the ring inward: + +```js +MEMORY_RING_LAYOUT.active.radius +``` + +### Move all memory dashes outward + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Increase: + +```js +MEMORY_RING_LAYOUT.l4.radius +MEMORY_RING_LAYOUT.delayed.radius +MEMORY_RING_LAYOUT.active.radius +``` + +Keep spacing between rings so hover glow does not visually merge. + +### Make runtime rings calmer + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Change: + +```js +const baseSpeed = 6 + random() * 18; +``` + +### Make memory signal rings rotate faster + +File: + +`ui/static/js/runtime/runtime-avatar.js` + +Change `getMemoryRingAnimation()`: + +```js +active: [28, 54], +delayed: [40, 80], +l4: [34, 70], +``` + +Lower duration means faster rotation. + +### Change hover glow intensity + +File: + +`ui/static/css/runtime-avatar.css` + +Edit: + +```css +.jin-avatar-orbit.is-memory-hover-hit, +.jin-avatar-counter-orbit.is-memory-hover-hit, +.jin-avatar-memory-dash.is-memory-hover-hit +``` + +### Change citation glow intensity + +File: + +`ui/static/css/runtime-avatar.css` + +Edit: + +```css +.jin-avatar-memory-dash.is-runtime-cited, +.jin-avatar-orbit.is-runtime-cited, +.jin-avatar-counter-orbit.is-runtime-cited +``` + +### Change avatar panel size + +File: + +`ui/static/css/base.css` + +Edit: + +```css +--runtime-avatar-panel-size +``` + +Then check collapsed mode and win95 theme. + +## Visual QA Checklist + +Use this after avatar visual changes. + +| Check | Expected result | +|---|---| +| Page reload | Avatar appears in the runtime panel | +| Center click | Avatar redraws with same data and fresh seeded layout | +| Runtime memory row hover | Matching inner orbit glows | +| Long-term fact row hover | Matching L4 dash glows | +| Delayed report row hover | Matching delayed dash glows | +| Active memory row hover | Matching active dash glows | +| Think citation hover | Matching orbit or dash gets citation glow | +| Pinned delayed report | Dash becomes brighter and whiter | +| Unpinned delayed report | Dash returns to normal dim blue | +| Archived L4 fact | Dash remains but is dimmer | +| Long-term facts panel | Archived L4 facts are not listed | +| Collapsed panel | Avatar keeps stable size | +| Win95 theme | Avatar still fits | +| Reduced motion | Orbit animations stop | + +## Final Rule Of Thumb + +If the change is about what the avatar means, edit the record collectors or L4 visibility helpers. + +If the change is about how the avatar looks, edit `runtime-avatar.js` constants/functions or `runtime-avatar.css`. + +If the change is about whether a row and a dash glow together, check `buildAvatarMemoryHoverId()` and `data-avatar-memory-hover-id`. + +If the change is visible in browser but not after reload, bump the script or stylesheet query string in `ui/templates/index.html`. diff --git a/README.md b/README.md index e8b2e19b..c6ae4f4f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ The JIN interface combines the chat stream, collapsible panels with model teleme

      Live Avatar turns JIN's memory state into a moving map of cognitive density.

      Each L1 field becomes its own inner orbit, with its position shaped by the amount of stored context and its rotation speed driven by interaction flow. The outer rings track L4 facts, Delayed Memory, and Active Memory as individual signals.

      +

      Hovering an L4 fact also highlights its linked Delayed Memory report when one exists; hovering that Delayed Memory report highlights the related L4 points and dashes.

      When JIN references a rule or memory item, the corresponding orbit lights up, revealing which part of the runtime is actively influencing the current thought.

      diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index a8d4039a..aa028a53 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -467,7 +467,7 @@ def format_previous_runtime_memory_tag( sequence_started_at, (int, float), ) or sequence_started_at <= 0: - return "" + return "" if now is None: now = time.time() @@ -481,7 +481,7 @@ def format_previous_runtime_memory_tag( TypeError, ValueError, ): - return "" + return "" from runtime.L1_memory_utils import ( format_user_idle_seconds, @@ -492,10 +492,10 @@ def format_previous_runtime_memory_tag( ) if not elapsed_text: - return "" + return "" return ( - "" ) @@ -537,7 +537,7 @@ def rename_runtime_memory_for_followup( prompt[:opening_index] + previous_opening_tag + prompt[opening_index + len(opening_tag):closing_index] - + "" + + "" + prompt[closing_index + len(closing_tag):] ) @@ -736,6 +736,7 @@ def build_followup_system_prompt( ) -> str: from utils.context.context_exports import ( + build_current_runtime_context, build_session_actions_history_context, strip_actions_history_context, ) @@ -844,6 +845,16 @@ def build_followup_system_prompt( context.runtime_turn_interruption_reason = "" context.runtime_turn_interruption_quote = "" + current_runtime_context = build_current_runtime_context( + user_message=initial_user_request, + sequence_started_at=sequence_started_at, + ) + + if current_runtime_context: + sections.append( + current_runtime_context + ) + if current_actions_history_context: sections.append( current_actions_history_context diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index d947c3c3..7ce93afb 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -24,14 +24,14 @@ "tags:", "body:", "anchor_fact_ids:", - "absorbed_fact_ids:", + "facts_ids:", "
      ", "You must fulfill title, summary, tags, and body so the report is valid for runtime processing.", - "When the report consolidates facts from , classify each referenced F id as either anchor or absorbed.", - "anchor_fact_ids are important atomic facts that must remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", - "absorbed_fact_ids are details represented by the report and are hidden from while the report exists.", + "When the report consolidates facts from , list every referenced F id in facts_ids.", + "anchor_fact_ids are the most important/key facts among facts_ids. They remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", + "facts_ids is the full set of L4 facts attached to the report, including every anchor_fact_ids entry. Non-anchor facts_ids are represented by the report and are hidden from while the report exists.", "Use exact ids copied from the [ id: ... ] suffixes. Do not list unrelated ids and never use PF pending ids.", - "A fact id must not be present in both arrays. Leave both arrays empty when the report does not consolidate L4 facts.", + "anchor_fact_ids must be a subset of facts_ids. Leave both arrays empty when the report does not consolidate L4 facts.", "DO NOT add any other text while making a report. Do not notify or acknowledge saving; immediately emit only the marker." ], "close_tag": true diff --git a/contracts/update_delayed_memory.json b/contracts/update_delayed_memory.json index c676b95c..a3110a80 100644 --- a/contracts/update_delayed_memory.json +++ b/contracts/update_delayed_memory.json @@ -11,17 +11,18 @@ }, "rules": [ "Use this marker only after the user explicitly asks to update an existing delayed memory report or explicitly accepts a proposal to extend one.", - "The update is additive. It may append new tags, new body text, new anchor_fact_ids, and/or new absorbed_fact_ids. It never replaces title, summary, or existing body content.", + "The update is additive. It may append new tags, new body text, new anchor_fact_ids, and/or new facts_ids. It never replaces title, summary, or existing body content.", "Emit the fulfilled marker with one JSON object inside it:", "", - "{\"tags\":[\"tag\"],\"body\":\"new text\",\"anchor_fact_ids\":[\"F1\"],\"absorbed_fact_ids\":[\"F2\"]}", + "{\"tags\":[\"tag\"],\"body\":\"new text\",\"anchor_fact_ids\":[\"F1\"],\"facts_ids\":[\"F1\",\"F2\"]}", "", "Replace id with the 6-character report id shown in .", - "The JSON object may contain any subset of tags, body, anchor_fact_ids, absorbed_fact_ids; at least one field must be present and non-empty.", + "The JSON object may contain any subset of tags, body, anchor_fact_ids, facts_ids; at least one field must be present and non-empty.", "Use exact F ids copied from . F means a committed fact; never use PF pending ids here.", - "anchor_fact_ids remain visible in and receive a [ delayed_memory_id: ... ] suffix while the report exists.", - "absorbed_fact_ids are represented by the report and are hidden from while the report exists.", - "A fact id must not be placed in both arrays. If a fact is important enough to remain directly visible, classify it as an anchor.", + "facts_ids is the full set of L4 facts attached to the report. Include every anchor_fact_ids entry in facts_ids.", + "anchor_fact_ids are the most important/key facts among facts_ids. They remain visible in and receive a [ delayed_memory_id: ... ] suffix while the report exists.", + "Non-anchor facts_ids are represented by the report and are hidden from while the report exists.", + "anchor_fact_ids must be a subset of facts_ids.", "Do not add any other text while updating a report. Immediately emit only the marker." ], "close_tag": true diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 564cae6c..57dcef43 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -34,7 +34,7 @@ from utils.actions.save_delayed_memory_utils import ( collect_anchor_fact_report_ids, collect_long_term_fact_ids_from_reports, - normalize_delayed_memory_fact_roles, + normalize_delayed_memory_fact_ids, normalize_long_term_fact_ids, ) from utils.long_term_facts_file_store import ( @@ -516,6 +516,7 @@ def remap_delayed_memory_l4_fact_ids( return { "changed": False, "report_ids": [], + "removed_report_refs": [], "file_errors": [], } @@ -524,6 +525,7 @@ def remap_delayed_memory_l4_fact_ids( return { "changed": False, "report_ids": [], + "removed_report_refs": [], "file_errors": [], } @@ -531,57 +533,70 @@ def remap_delayed_memory_l4_fact_ids( appended_reports = get_appended_delayed_memory_report(context) changed_reports = {} + removed_report_refs = [] for report_id, report in list(reports.items()): if not isinstance(report, dict): continue - current_anchor_ids, current_absorbed_ids = ( - normalize_delayed_memory_fact_roles( + current_anchor_ids, current_fact_ids = ( + normalize_delayed_memory_fact_ids( report.get("anchor_fact_ids", []), - report.get("absorbed_fact_ids", []), - report.get("long_term_facts_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) ) - removed_anchor = any( - fact_id in removed_ids + removed_anchor_ids = [ + fact_id for fact_id in current_anchor_ids - ) - removed_absorbed = any( - fact_id in removed_ids - for fact_id in current_absorbed_ids - ) + if fact_id in removed_ids + ] + removed_fact_ids_for_report = [ + fact_id + for fact_id in current_fact_ids + if fact_id in removed_ids + ] + removed_anchor = bool(removed_anchor_ids) + removed_fact = bool(removed_fact_ids_for_report) - if not removed_anchor and not removed_absorbed: + if not removed_anchor and not removed_fact: continue + removed_report_refs.append({ + "report_id": str(report_id or "").strip(), + "anchor_fact_ids": removed_anchor_ids, + "facts_ids": removed_fact_ids_for_report, + }) + next_anchor_ids = [ fact_id for fact_id in current_anchor_ids if fact_id not in removed_ids ] - next_absorbed_ids = [ + next_fact_ids = [ fact_id - for fact_id in current_absorbed_ids + for fact_id in current_fact_ids if fact_id not in removed_ids ] if removed_anchor: next_anchor_ids.extend(replacement_ids) - if removed_absorbed: - next_absorbed_ids.extend(replacement_ids) + if removed_fact: + next_fact_ids.extend(replacement_ids) - next_anchor_ids, next_absorbed_ids = ( - normalize_delayed_memory_fact_roles( + next_anchor_ids, next_fact_ids = ( + normalize_delayed_memory_fact_ids( next_anchor_ids, - next_absorbed_ids, + next_fact_ids, ) ) updated_report = { **report, "anchor_fact_ids": next_anchor_ids, - "absorbed_fact_ids": next_absorbed_ids, + "facts_ids": next_fact_ids, } + updated_report.pop("absorbed_fact_ids", None) updated_report.pop("long_term_facts_ids", None) reports[report_id] = updated_report changed_reports[report_id] = updated_report @@ -606,6 +621,198 @@ def remap_delayed_memory_l4_fact_ids( return { "changed": bool(changed_reports), "report_ids": sorted(changed_reports), + "removed_report_refs": sorted( + removed_report_refs, + key=lambda item: item["report_id"], + ), + "file_errors": file_errors, + } + + +def normalize_l4_fact_restore_report_refs(value) -> list[dict]: + if not isinstance(value, dict): + return [] + + refs = value.get("delayed_memory_report_refs") + if not isinstance(refs, list): + return [] + + clean_refs = [] + seen = set() + for ref in refs: + if not isinstance(ref, dict): + continue + + report_id = str(ref.get("report_id") or "").strip() + if not report_id: + continue + + anchor_fact_ids = normalize_long_term_fact_ids( + ref.get("anchor_fact_ids", []) + ) + fact_ids = normalize_long_term_fact_ids( + ref.get("facts_ids", []) + ) + if not anchor_fact_ids and not fact_ids: + continue + + dedupe_key = ( + report_id, + tuple(anchor_fact_ids), + tuple(fact_ids), + ) + if dedupe_key in seen: + continue + + seen.add(dedupe_key) + clean_refs.append({ + "report_id": report_id, + "anchor_fact_ids": anchor_fact_ids, + "facts_ids": fact_ids, + }) + + return clean_refs + + +def build_l4_deleted_fact_restore_meta(delayed_memory_change: dict) -> dict: + refs = normalize_l4_fact_restore_report_refs({ + "delayed_memory_report_refs": ( + delayed_memory_change.get("removed_report_refs") + if isinstance(delayed_memory_change, dict) + else [] + ), + }) + if not refs: + return {} + + return { + "version": 1, + "delayed_memory_report_refs": refs, + } + + +def restore_delayed_memory_l4_fact_refs( + context, + *, + fact_id: str, + restore_meta: dict, +) -> dict: + normalized_fact_ids = normalize_long_term_fact_ids([fact_id]) + if not normalized_fact_ids: + return { + "changed": False, + "report_ids": [], + "missing_report_ids": [], + "file_errors": [], + } + + target_id = normalized_fact_ids[0] + refs = [ + ref + for ref in normalize_l4_fact_restore_report_refs(restore_meta) + if ( + target_id in ref.get("anchor_fact_ids", []) + or target_id in ref.get("facts_ids", []) + ) + ] + if not refs: + return { + "changed": False, + "report_ids": [], + "missing_report_ids": [], + "file_errors": [], + } + + reports = getattr(context, "delayed_memory_reports", None) + if not isinstance(reports, dict): + return { + "changed": False, + "report_ids": [], + "missing_report_ids": [ + ref["report_id"] + for ref in refs + ], + "file_errors": [], + } + + from utils.brain_client_utils import get_appended_delayed_memory_report + + appended_reports = get_appended_delayed_memory_report(context) + changed_reports = {} + missing_report_ids = [] + + for ref in refs: + report_id = ref["report_id"] + report = reports.get(report_id) + if not isinstance(report, dict): + missing_report_ids.append(report_id) + continue + + current_anchor_ids, current_fact_ids = ( + normalize_delayed_memory_fact_ids( + report.get("anchor_fact_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), + ) + ) + next_anchor_ids = list(current_anchor_ids) + next_fact_ids = list(current_fact_ids) + + if ( + target_id in ref.get("anchor_fact_ids", []) + and target_id not in next_anchor_ids + ): + next_anchor_ids.append(target_id) + if ( + target_id in ref.get("facts_ids", []) + and target_id not in next_fact_ids + ): + next_fact_ids.append(target_id) + + next_anchor_ids, next_fact_ids = ( + normalize_delayed_memory_fact_ids( + next_anchor_ids, + next_fact_ids, + ) + ) + if ( + next_anchor_ids == current_anchor_ids + and next_fact_ids == current_fact_ids + ): + continue + + updated_report = { + **report, + "anchor_fact_ids": next_anchor_ids, + "facts_ids": next_fact_ids, + } + updated_report.pop("absorbed_fact_ids", None) + updated_report.pop("long_term_facts_ids", None) + reports[report_id] = updated_report + changed_reports[report_id] = updated_report + + if report_id in appended_reports: + appended_reports[report_id] = { + **updated_report, + "id": report_id, + } + + file_errors = [] + if changed_reports and bool( + getattr(context, "delayed_memory_file_store_enabled", False) + ): + from utils.delayed_memory_file_store import persist_delayed_memory_reports + + file_errors = persist_delayed_memory_reports(changed_reports) + + if changed_reports: + refresh_runtime_l4_archived_fact_ids(context) + + return { + "changed": bool(changed_reports), + "report_ids": sorted(changed_reports), + "missing_report_ids": sorted(set(missing_report_ids)), "file_errors": file_errors, } @@ -1439,13 +1646,30 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: context, store, ) + delayed_memory_change = remap_delayed_memory_l4_fact_ids( + context, + removed_fact_ids=[target_id], + replacement_fact_ids=[], + ) + restore_meta = build_l4_deleted_fact_restore_meta( + delayed_memory_change + ) + deleted_fact_for_restore = ( + { + **deleted_fact, + "_restore_meta": restore_meta, + } + if restore_meta + else deleted_fact + ) await log_memory_event( context, level=L4_LOG_LEVEL, message="L4 fact deleted", details=json.dumps( { - "fact": deleted_fact, + "fact": deleted_fact_for_restore, + "delayed_memory_change": delayed_memory_change, "revision": store.get("revision"), "total_facts": len(store.get("facts") or []), }, @@ -1454,12 +1678,7 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: ), event="fact_deleted", tag_suffix="DELETED", - deleted_fact=deleted_fact, - ) - delayed_memory_change = remap_delayed_memory_l4_fact_ids( - context, - removed_fact_ids=[target_id], - replacement_fact_ids=[], + deleted_fact=deleted_fact_for_restore, ) await emit_l4_memory_update( context, @@ -1471,6 +1690,11 @@ async def delete_l4_memory_fact(context, fact_id: str) -> bool: async def restore_l4_memory_fact(context, fact) -> bool: + restore_meta = ( + fact.get("_restore_meta", {}) + if isinstance(fact, dict) + else {} + ) store, changed = restore_l4_fact_to_store( ensure_runtime_l4_state(context), fact, @@ -1495,11 +1719,21 @@ async def restore_l4_memory_fact(context, fact) -> bool: context, store, ) + delayed_memory_change = restore_delayed_memory_l4_fact_refs( + context, + fact_id=restored_fact["id"], + restore_meta=restore_meta, + ) await emit_l4_memory_update( context, change={ "restored_ids": [restored_fact["id"]], "changed": True, + "delayed_memory_report_ids": ( + delayed_memory_change.get("report_ids", []) + ), }, ) + if delayed_memory_change.get("changed"): + await emit_delayed_memory_reference_update(context) return True diff --git a/runtime/stream.py b/runtime/stream.py index b6b5d50c..5ee72002 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -48,6 +48,7 @@ RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, + RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, @@ -154,6 +155,7 @@ def __init__( self.marker_repetition_aborted = False self.action_guard_rejected_aborted = False self.context_limit_recovery_armed = False + self.started_active_memory_action_ids = [] self.started_delayed_memory_action_ids = [] self.started_update_l4_facts_action_ids = [] self.confirmed_action_guard_names = set() @@ -1256,6 +1258,25 @@ def get_runtime_action_display_id( return action_id + if action.name == RUNTIME_ACTION_SAVE_ACTIVE_MEMORY: + if self.started_active_memory_action_ids: + return self.started_active_memory_action_ids.pop(0) + + sequence = int( + getattr( + self.context, + "runtime_active_memory_action_sequence", + 0, + ) + or 0 + ) + 1 + self.context.runtime_active_memory_action_sequence = sequence + + return build_runtime_action_id( + RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, + sequence, + ) + if action.name in { RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, @@ -1827,6 +1848,40 @@ async def emit_started_runtime_actions( "text": display_text, "close_tag": has_close_tag, } + elif action.name == RUNTIME_ACTION_SAVE_ACTIVE_MEMORY: + current_sequence = int( + getattr( + self.context, + "runtime_active_memory_action_sequence", + 0, + ) + or 0 + ) + next_sequence = current_sequence + 1 + self.context.runtime_active_memory_action_sequence = ( + next_sequence + ) + action_id = build_runtime_action_id( + RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, + next_sequence, + ) + self.started_active_memory_action_ids.append( + action_id + ) + + payload = { + "type": "runtime_action", + "runtime_message_id": self.stream.message_id, + "action": "save_active_memory", + "id": action_id, + "status": "started", + "display_name": display_name, + "text": display_text, + "close_tag": has_close_tag, + } + + if action.payload and not has_close_tag: + payload["payload"] = action.payload elif action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: pending_ids = getattr( self.context, diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 09938d47..6a7019cf 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -388,6 +388,7 @@ def test_apply_runtime_action_calls_records_save_active_memory(self): context.runtime_action_events, [ { + "id": "save_active_memory_001", "name": "save_active_memory", "payload": "remind later", } @@ -468,14 +469,26 @@ def test_apply_runtime_action_calls_emits_save_active_memory_bubble(self): context.emitter.events[0]["active_memory"], context.active_memory_records[0], ) + self.assertEqual( + context.emitter.events[0]["id"], + "save_active_memory_001", + ) + self.assertRegex( + context.emitter.events[0]["active_memory_id"], + r"^[a-z0-9]{6}$", + ) self.assertEqual( context.emitter.events[1], { "type": "runtime_action", "action": "save_active_memory", + "id": "save_active_memory_001", "status": "completed", "display_name": "SAVE_ACTIVE_MEMORY", "close_tag": False, + "active_memory_id": ( + context.emitter.events[0]["active_memory_id"] + ), }, ) @@ -499,6 +512,84 @@ def test_apply_runtime_action_calls_emits_save_active_memory_bubble(self): tool_results, ) + def test_apply_runtime_action_calls_emits_one_bubble_per_saved_active_memory(self): + + Emitter = FakeEmitter + + Context = FakeContext + + context = Context() + context.emitter = Emitter() + context.timestamp = "2026-06-20T10:00:00" + context.session_id = "test-session" + context.turn_number = 3 + + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload="first reminder", + ), + RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload="second reminder", + ), + ), + ) + ) + + self.assertEqual( + applied_count, + 2, + ) + self.assertEqual( + len(context.active_memory_records), + 2, + ) + self.assertEqual( + [ + event.get("id") + for event in context.emitter.events + ], + [ + "save_active_memory_001", + "save_active_memory_001", + "save_active_memory_002", + "save_active_memory_002", + ], + ) + self.assertEqual( + [ + event.get("text") + for event in context.emitter.events + if not event.get("status") + ], + [ + "SAVE_ACTIVE_MEMORY: first reminder", + "SAVE_ACTIVE_MEMORY: second reminder", + ], + ) + self.assertEqual( + len({ + event["active_memory_id"] + for event in context.emitter.events + if event.get("active_memory_id") + }), + 2, + ) + self.assertEqual( + [ + event.get("id") + for event in context.runtime_action_events + ], + [ + "save_active_memory_001", + "save_active_memory_002", + ], + ) + def test_save_active_memory_replaces_model_runtime_suffixes(self): @@ -709,6 +800,7 @@ def test_apply_runtime_action_calls_skips_exact_active_memory_copy(self): context.runtime_action_events, [ { + "id": "save_active_memory_001", "name": "save_active_memory", "payload": "remember cuckoo", }, @@ -720,6 +812,7 @@ def test_apply_runtime_action_calls_skips_exact_active_memory_copy(self): { "type": "runtime_action", "action": "save_active_memory", + "id": "save_active_memory_001", "display_name": "SAVE_ACTIVE_MEMORY", "text": "SAVE_ACTIVE_MEMORY: remember cuckoo", "payload": "remember cuckoo", @@ -728,6 +821,7 @@ def test_apply_runtime_action_calls_skips_exact_active_memory_copy(self): { "type": "runtime_action", "action": "save_active_memory", + "id": "save_active_memory_001", "status": "completed", "display_name": "SAVE_ACTIVE_MEMORY", "close_tag": False, @@ -780,6 +874,7 @@ def test_apply_runtime_action_calls_skips_active_memory_copy_from_runtime_memory context.runtime_action_events, [ { + "id": "save_active_memory_001", "name": "save_active_memory", "payload": "remember cuckoo", }, @@ -791,6 +886,7 @@ def test_apply_runtime_action_calls_skips_active_memory_copy_from_runtime_memory { "type": "runtime_action", "action": "save_active_memory", + "id": "save_active_memory_001", "display_name": "SAVE_ACTIVE_MEMORY", "text": "SAVE_ACTIVE_MEMORY: remember cuckoo", "payload": "remember cuckoo", @@ -799,6 +895,7 @@ def test_apply_runtime_action_calls_skips_active_memory_copy_from_runtime_memory { "type": "runtime_action", "action": "save_active_memory", + "id": "save_active_memory_001", "status": "completed", "display_name": "SAVE_ACTIVE_MEMORY", "close_tag": False, diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 0541054f..98dd425e 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -138,7 +138,7 @@ def test_parses_delayed_memory_content_payload(self): ), "pinned": False, "anchor_fact_ids": [], - "absorbed_fact_ids": [], + "facts_ids": [], "created_session_id": "session-1", "created_time": "2026-06-29T12:00:00", }, @@ -164,7 +164,7 @@ def test_parses_long_term_fact_ids_for_delayed_memory_report(self): ) self.assertEqual( - report_value["absorbed_fact_ids"], + report_value["facts_ids"], [ "F1", "F2", @@ -173,7 +173,7 @@ def test_parses_long_term_fact_ids_for_delayed_memory_report(self): self.assertNotIn("long_term_facts_ids", report_value) - def test_parses_anchor_and_absorbed_fact_ids_for_delayed_memory_report(self): + def test_parses_anchor_and_facts_ids_for_delayed_memory_report(self): report = parse_delayed_memory_content_payload( ( @@ -182,15 +182,38 @@ def test_parses_anchor_and_absorbed_fact_ids_for_delayed_memory_report(self): "tags: social\n" "body: Reusable summary.\n" "anchor_fact_ids: F1, F1\n" - "absorbed_fact_ids: F2, F3, F1" + "facts_ids: F1, F2, F3" ) ) report_value = next(iter(report.values())) self.assertEqual(report_value["anchor_fact_ids"], ["F1"]) self.assertEqual( - report_value["absorbed_fact_ids"], - ["F2", "F3"], + report_value["facts_ids"], + ["F1", "F2", "F3"], + ) + + def test_parses_json_array_fact_ids_for_delayed_memory_report(self): + + report = parse_delayed_memory_content_payload( + ( + "title: Architecture context\n" + "summary: Consolidated architecture details.\n" + "tags: architecture, protocol\n" + "body: Reusable summary.\n" + 'anchor_fact_ids: ["F1", "F5", "F13"]\n' + 'facts_ids: ["F1", "F5", "F13", "F25", "F26"]' + ) + ) + report_value = next(iter(report.values())) + + self.assertEqual( + report_value["anchor_fact_ids"], + ["F1", "F5", "F13"], + ) + self.assertEqual( + report_value["facts_ids"], + ["F1", "F5", "F13", "F25", "F26"], ) def test_update_delayed_memory_json_accepts_single_non_empty_field(self): @@ -245,11 +268,12 @@ def test_build_report_keeps_only_existing_l4_fact_ids(self): report_value = report["abc123"] self.assertEqual( - report_value["absorbed_fact_ids"], + report_value["facts_ids"], [ "F1", ], ) + self.assertNotIn("absorbed_fact_ids", report_value) self.assertNotIn("long_term_facts_ids", report_value) @@ -1775,7 +1799,7 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): ( '{"tags":["social","context"],' '"anchor_fact_ids":["F1"],' - '"absorbed_fact_ids":["F2"],' + '"facts_ids":["F1","F2"],' '"body":"Additional durable context."}\n' ) ) @@ -1809,7 +1833,8 @@ def test_stream_filter_buffers_update_delayed_memory_header_payload(self): "anchor_fact_ids": [ "F1", ], - "absorbed_fact_ids": [ + "facts_ids": [ + "F1", "F2", ], "body": "Additional durable context.", @@ -1824,7 +1849,7 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): "\n" '{"tags":["social","context"],' '"anchor_fact_ids":["F1"],' - '"absorbed_fact_ids":["F99"],' + '"facts_ids":["F1","F99"],' '"body":"Additional durable context."}\n' "" ), @@ -1858,7 +1883,8 @@ def test_extracts_update_delayed_memory_block_with_header_id(self): "anchor_fact_ids": [ "F1", ], - "absorbed_fact_ids": [ + "facts_ids": [ + "F1", "F99", ], "body": "Additional durable context.", @@ -1886,7 +1912,9 @@ def test_update_delayed_memory_appends_only_supported_fields(self): "anchor_fact_ids": [ "F1", ], - "absorbed_fact_ids": [], + "facts_ids": [ + "F1", + ], "pinned": False, }, } @@ -1915,7 +1943,7 @@ def test_update_delayed_memory_appends_only_supported_fields(self): "a1b2c3\n" + json.dumps({ "tags": ["context", "social"], - "absorbed_fact_ids": ["F2", "F99"], + "facts_ids": ["F2", "F99"], "body": "Additional body.", }) ), @@ -1940,8 +1968,9 @@ def test_update_delayed_memory_appends_only_supported_fields(self): ], ) self.assertEqual( - report["absorbed_fact_ids"], + report["facts_ids"], [ + "F1", "F2", ], ) diff --git a/tests/runtime_actions/test_skill_marker_semantics.py b/tests/runtime_actions/test_skill_marker_semantics.py index 89ef5f87..a4b93718 100644 --- a/tests/runtime_actions/test_skill_marker_semantics.py +++ b/tests/runtime_actions/test_skill_marker_semantics.py @@ -18,6 +18,7 @@ from utils.session_actions_history import ( compact_session_action_history_since, format_session_action_marker_names, + upsert_session_action_marker_history_since, ) @@ -208,6 +209,52 @@ def test_counter_groups_only_identical_marker_payloads(self): ), ) + def test_active_memory_marker_history_keeps_payloads_separate(self): + counter = RuntimeActionCounter() + counter.record([ + RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload="remember tea", + ), + RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload="remember coffee", + ), + ]) + + context = FakeContext() + context.runtime_current_turn_id = "turn-1" + + self.assertTrue( + upsert_session_action_marker_history_since( + context, + 0, + counter.marker_actions(), + ) + ) + self.assertEqual( + [ + item["text"] + for item in context.runtime_session_action_history + ], + [ + "SAVE_ACTIVE_MEMORY - remember tea", + "SAVE_ACTIVE_MEMORY - remember coffee", + ], + ) + self.assertTrue( + all( + item.get("runtime_session_action_preserve_separate") + for item in context.runtime_session_action_history + ) + ) + self.assertFalse( + compact_session_action_history_since( + context, + 0, + ) + ) + def test_skill_marker_ui_contract_keeps_rows_separate_and_uncounted(self): logger_source = LOGGER_JS.read_text(encoding="utf-8") runtime_source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") diff --git a/tests/runtime_actions/test_update_l4_facts.py b/tests/runtime_actions/test_update_l4_facts.py index eeff4f0b..859378e0 100644 --- a/tests/runtime_actions/test_update_l4_facts.py +++ b/tests/runtime_actions/test_update_l4_facts.py @@ -153,9 +153,13 @@ async def test_runtime_action_updates_l4_in_background(self): replacement_id = facts[0]["id"] self.assertEqual( - context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], + context.delayed_memory_reports["abc123"]["facts_ids"], [replacement_id], ) + self.assertNotIn( + "absorbed_fact_ids", + context.delayed_memory_reports["abc123"], + ) self.assertNotIn( "long_term_facts_ids", context.delayed_memory_reports["abc123"], diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 027fd8a3..e1b70e10 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -108,10 +108,22 @@ def _assert_latest_request_payload( system_prompt.index( expected_followup_message ), + system_prompt.index( + "" + ), system_prompt.index( "" ), ) + test_case.assertIn( + f"user_message: {user_input}", + system_prompt, + ) test_case.assertIn( f"INITIAL_SEQUENCE_INSTRUCTION: {user_input}", system_prompt, @@ -342,6 +354,16 @@ async def test_current_sequence_starts_with_original_user_message(self): context=context, ) + self.assertIn( + "\n" + "user_message: keep <this> in delayed memory\n" + "", + prompt, + ) + self.assertLess( + prompt.index(""), + ) self.assertIn( "\n" "INITIAL_SEQUENCE_INSTRUCTION: keep <this> in delayed memory ( 10s ago )\n" @@ -558,14 +580,14 @@ async def test_previous_runtime_memory_tag_tracks_elapsed_sequence_time(self): sequence_started_at=1000.0, now=1150.0, ), - "", + "", ) self.assertEqual( format_previous_runtime_memory_tag( sequence_started_at=1000.0, now=1185.0, ), - "", + "", ) async def test_followup_runtime_memory_tag_uses_sequence_started_at(self): @@ -591,8 +613,8 @@ async def test_followup_runtime_memory_tag_uses_sequence_started_at(self): self.assertIn( ( - "" - "state" + "" + "state" ), prompt, ) @@ -609,8 +631,8 @@ async def test_followup_renames_runtime_memory_block(self): ) self.assertIn( - "\nactive_topic: test\n" - "", + "\nactive_topic: test\n" + "", prompt, ) self.assertNotIn( @@ -2706,6 +2728,10 @@ async def fake_run_brain_stream(**kwargs): ) self.assertLess( prompt.index(idle_instruction), + prompt.index(""), ) self.assertLess( @@ -2785,8 +2811,8 @@ async def test_idle_followup_prompt_contains_tool_result_and_frozen_context(self ) self.assertIn( ( - "" - "frozen state" + "" + "frozen state" ), prompt, ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index f0f37716..9ce8a22e 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -1058,27 +1058,39 @@ async def collect(context): "list_skills", ) self.assertEqual( - context.runtime_session_action_history[-1]["text"], - ( - "WEB_SEARCH - Latest astronomical news 2026, " - "SAVE_ACTIVE_MEMORY - astronomical news tracker, " - "LIST_SKILLS" - ), + [ + item["text"] + for item in context.runtime_session_action_history + ], + [ + "WEB_SEARCH - Latest astronomical news 2026", + "SAVE_ACTIVE_MEMORY - astronomical news tracker", + "LIST_SKILLS", + ], ) self.assertEqual( - context.runtime_session_action_history[-1]["parts"], [ - { - "text": "WEB_SEARCH", - "detail": "Latest astronomical news 2026", - }, - { - "text": "SAVE_ACTIVE_MEMORY", - "detail": "astronomical news tracker", - }, - { - "text": "LIST_SKILLS", - }, + item["parts"] + for item in context.runtime_session_action_history + ], + [ + [ + { + "text": "WEB_SEARCH", + "detail": "Latest astronomical news 2026", + }, + ], + [ + { + "text": "SAVE_ACTIVE_MEMORY", + "detail": "astronomical news tracker", + }, + ], + [ + { + "text": "LIST_SKILLS", + }, + ], ], ) @@ -1533,26 +1545,35 @@ def test_session_history_adds_count_to_every_marker_part(self): ) self.assertEqual( - context.runtime_session_action_history[0]["parts"], [ - { - "text": "WEB_SEARCH", - "detail": "latest news", - }, - { - "text": "SAVE_ACTIVE_MEMORY", - "detail": "remember coffee", - }, - { - "text": "IDLE", - "detail": "5s", - }, - { - "text": "APPEND_SKILL: wildcards", - }, - { - "text": "APPEND_SKILL: file_manager", - }, + item["parts"] + for item in context.runtime_session_action_history + ], + [ + [ + { + "text": "WEB_SEARCH", + "detail": "latest news", + }, + ], + [ + { + "text": "SAVE_ACTIVE_MEMORY", + "detail": "remember coffee", + }, + ], + [ + { + "text": "IDLE", + "detail": "5s", + }, + { + "text": "APPEND_SKILL: wildcards", + }, + { + "text": "APPEND_SKILL: file_manager", + }, + ], ], ) @@ -1666,7 +1687,10 @@ def test_payload_distinct_active_memory_history_uses_separate_parts(self): ) self.assertEqual( - context.runtime_session_action_history[0]["parts"], + [ + item["parts"][0] + for item in context.runtime_session_action_history + ], [ { "text": "SAVE_ACTIVE_MEMORY", diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index db644762..d33d7360 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -69,7 +69,7 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): storage_source, ) self.assertIn( - "long_term_facts_ids", + "facts_ids", storage_source, ) self.assertIn( diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index 111cc8a7..d211cb91 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -34,7 +34,8 @@ def build_report( "anchor_fact_ids": [ "F1", ], - "absorbed_fact_ids": [ + "facts_ids": [ + "F1", "F2", ], "created_session_id": "session-a", @@ -89,11 +90,16 @@ def test_persist_and_load_direct_report_shape(self): ], ) self.assertEqual( - payload["absorbed_fact_ids"], + payload["facts_ids"], [ + "F1", "F2", ], ) + self.assertNotIn( + "absorbed_fact_ids", + payload, + ) reports, warnings = ( load_delayed_memory_reports_from_files( @@ -124,8 +130,9 @@ def test_persist_and_load_direct_report_shape(self): ], ) self.assertEqual( - reports["48ggds"]["absorbed_fact_ids"], + reports["48ggds"]["facts_ids"], [ + "F1", "F2", ], ) @@ -227,7 +234,9 @@ def test_local_storage_reports_override_file_fallback_by_id(self): browser_report = self.build_report( title="Browser title", ) - browser_report["absorbed_fact_ids"] = [] + browser_report["facts_ids"] = [ + "F1", + ] browser_reports = { "48ggds": browser_report, } @@ -252,8 +261,9 @@ def test_local_storage_reports_override_file_fallback_by_id(self): ], ) self.assertEqual( - merged["48ggds"]["absorbed_fact_ids"], + merged["48ggds"]["facts_ids"], [ + "F1", "F2", ], ) @@ -291,6 +301,58 @@ def test_websocket_sync_merges_browser_primary_with_file_fallback(self): "File only", ) + def test_websocket_sync_treats_incoming_report_refs_as_authoritative(self): + + existing_report = self.build_report( + title="File title", + ) + existing_report["anchor_fact_ids"] = [ + "F1", + ] + existing_report["facts_ids"] = [ + "F1", + "F2", + ] + incoming_report = self.build_report( + title="Browser title", + ) + incoming_report["anchor_fact_ids"] = [] + incoming_report["facts_ids"] = [ + "F2", + ] + context = SimpleNamespace( + delayed_memory_reports={ + "48ggds": existing_report, + "a1b2c3": self.build_report( + title="File only", + ), + }, + ) + + apply_delayed_memory_reports( + context, + { + "delayed_memory_reports": { + "48ggds": incoming_report, + }, + }, + ) + + self.assertEqual( + context.delayed_memory_reports["48ggds"]["anchor_fact_ids"], + [], + ) + self.assertEqual( + context.delayed_memory_reports["48ggds"]["facts_ids"], + [ + "F2", + ], + ) + self.assertEqual( + context.delayed_memory_reports["a1b2c3"]["title"], + "File only", + ) + def test_file_payload_accepts_example_aliases(self): payload = build_delayed_memory_file_payload( diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index 7da21dee..f62c2e18 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,8 +47,8 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-2", source) - self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-17", source) + self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) + self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-20", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index ab14f7f6..6372d251 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -20,6 +20,7 @@ def test_deleted_fact_card_exposes_full_payload_and_restore(self): self.assertIn('"payload"', source) self.assertIn('"restore"', source) self.assertIn('api.requestFactRestore(', source) + self.assertIn('window.handleL4LoggerMemoryRestoreResult', source) self.assertIn('window.handleL4MemoryRestoreResult', source) def test_extraction_and_merge_request_response_cards_are_paired(self): @@ -58,18 +59,29 @@ def test_restore_message_round_trip_is_registered(self): socket_source = SOCKET_EVENTS_JS.read_text(encoding="utf-8") self.assertIn('type: "l4_memory_restore_fact"', runtime_source) + self.assertIn('_restore_meta: restoreMeta', runtime_source) self.assertIn('requestFactRestore,', runtime_source) self.assertIn('"l4_memory_restore_result"', socket_source) + self.assertIn('handleSocketL4MemoryRestoreResult', socket_source) + self.assertIn('window.handleL4LoggerMemoryRestoreResult', socket_source) + + def test_delete_marks_local_l4_store_deleted_before_sync(self): + runtime_source = RUNTIME_L4_JS.read_text(encoding="utf-8") + + self.assertIn("function deleteFactLocally(", runtime_source) + self.assertIn("deleted_fact_ids: deletedFactIds", runtime_source) + self.assertIn("deleteFactLocally(id)", runtime_source) + self.assertIn("syncLongTermMemoryToRuntime();", runtime_source) def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=memory-hover-sync-2', source) - self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-restore-1', source) + self.assertIn('/static/css/runtime-memory.css?v=memory-hover-sync-9', source) + self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-delete-local-1', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) - self.assertIn('/static/js/logger/log-entries.js?v=l4-response-visibility-1', source) - self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=l4-restore-meta-1', source) + self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-2', source) if __name__ == "__main__": diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 063c414f..2684dffe 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1166,7 +1166,7 @@ def test_delayed_report_anchor_stays_visible_with_report_suffix(self): "abc123": { "title": "Social context", "anchor_fact_ids": ["F1"], - "absorbed_fact_ids": ["F2"], + "facts_ids": ["F1", "F2"], }, } @@ -1204,10 +1204,11 @@ def test_anchor_visibility_wins_over_absorbed_reference_in_other_report(self): "abc123": { "title": "Identity", "anchor_fact_ids": ["F1"], + "facts_ids": ["F1"], }, "def456": { "title": "Social", - "absorbed_fact_ids": ["F1"], + "facts_ids": ["F1"], }, } @@ -1242,7 +1243,7 @@ async def test_delete_l4_fact_cleans_delayed_memory_fact_arrays(self): "abc123": { "title": "Social context", "anchor_fact_ids": ["F1"], - "absorbed_fact_ids": ["F2"], + "facts_ids": ["F1", "F2"], }, } context.delayed_memory_file_store_enabled = False @@ -1253,15 +1254,19 @@ async def test_delete_l4_fact_cleans_delayed_memory_fact_arrays(self): [], ) self.assertEqual( - context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], + context.delayed_memory_reports["abc123"]["facts_ids"], ["F2"], ) self.assertTrue(await delete_l4_memory_fact(context, "F2")) self.assertEqual( - context.delayed_memory_reports["abc123"]["absorbed_fact_ids"], + context.delayed_memory_reports["abc123"]["facts_ids"], [], ) + self.assertNotIn( + "absorbed_fact_ids", + context.delayed_memory_reports["abc123"], + ) self.assertNotIn( "long_term_facts_ids", context.delayed_memory_reports["abc123"], @@ -1275,6 +1280,150 @@ async def test_delete_l4_fact_cleans_delayed_memory_fact_arrays(self): 2, ) + async def test_delete_l4_fact_logs_report_refs_for_restore(self): + logger = CaptureMemoryLogger() + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=logger, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [{ + "id": "F1", + "key": "project.restore.test", + "value": "Restore the report links.", + }], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Primary report", + "anchor_fact_ids": ["F1"], + "facts_ids": ["F1"], + }, + "def456": { + "title": "Related report", + "facts_ids": ["F1"], + }, + "ghi789": { + "title": "Unrelated report", + "facts_ids": [], + }, + } + context.delayed_memory_file_store_enabled = False + + self.assertTrue(await delete_l4_memory_fact(context, "F1")) + + deleted_fact = logger.logs[0]["deleted_fact"] + restore_meta = deleted_fact["_restore_meta"] + self.assertEqual( + restore_meta["delayed_memory_report_refs"], + [ + { + "report_id": "abc123", + "anchor_fact_ids": ["F1"], + "facts_ids": ["F1"], + }, + { + "report_id": "def456", + "anchor_fact_ids": [], + "facts_ids": ["F1"], + }, + ], + ) + self.assertEqual( + json.loads(logger.logs[0]["details"])["fact"]["_restore_meta"], + restore_meta, + ) + + async def test_restore_l4_fact_restores_delayed_memory_report_refs(self): + logger = CaptureMemoryLogger() + emitter = FakeEmitter() + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=logger, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "project.restore.test", + "value": "Restore the report links.", + }, + { + "id": "F2", + "key": "project.restore.keep", + "value": "Keep this report link.", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Primary report", + "anchor_fact_ids": ["F1"], + "facts_ids": ["F1", "F2"], + }, + "def456": { + "title": "Related report", + "facts_ids": ["F1"], + }, + } + context.delayed_memory_file_store_enabled = False + + self.assertTrue(await delete_l4_memory_fact(context, "F1")) + deleted_fact = logger.logs[0]["deleted_fact"] + self.assertEqual( + context.delayed_memory_reports["abc123"]["anchor_fact_ids"], + [], + ) + self.assertEqual( + context.delayed_memory_reports["abc123"]["facts_ids"], + ["F2"], + ) + self.assertEqual( + context.delayed_memory_reports["def456"]["facts_ids"], + [], + ) + + self.assertTrue(await restore_l4_memory_fact(context, deleted_fact)) + + self.assertEqual( + [ + fact["id"] + for fact in context.runtime_long_term_memory_store["facts"] + ], + ["F2", "F1"], + ) + self.assertNotIn( + "_restore_meta", + context.runtime_long_term_memory_store["facts"][-1], + ) + self.assertEqual( + context.delayed_memory_reports["abc123"]["anchor_fact_ids"], + ["F1"], + ) + self.assertEqual( + context.delayed_memory_reports["abc123"]["facts_ids"], + ["F1", "F2"], + ) + self.assertEqual( + context.delayed_memory_reports["def456"]["facts_ids"], + ["F1"], + ) + self.assertTrue(any( + event.get("type") == "delayed_memory_store_snapshot" + for event in emitter.events + )) + self.assertTrue(any( + event.get("change", {}).get("restored_ids") == ["F1"] + and event.get("change", {}).get("delayed_memory_report_ids") + == ["abc123", "def456"] + for event in emitter.events + if event.get("type") == "l4_memory_update" + )) + def test_delayed_report_fact_ids_hide_only_from_brain_context(self): context = RuntimeContext( websocket=None, @@ -1430,12 +1579,16 @@ async def test_saved_delayed_report_hides_linked_facts_on_next_context(self): context.delayed_memory_reports[ "abc123" ][ - "absorbed_fact_ids" + "facts_ids" ], [ "F1", ], ) + self.assertNotIn( + "absorbed_fact_ids", + context.delayed_memory_reports["abc123"], + ) self.assertNotIn( "long_term_facts_ids", context.delayed_memory_reports["abc123"], diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index f6efd78b..f0d59ac5 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -111,11 +111,11 @@ def test_win95_keeps_reference_and_citation_hits_consistent(self): def test_cache_versions_are_bumped_for_citation_sync_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-2", source) + self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-18", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-11", source) - self.assertIn("/static/js/think-citations.js?v=think-citations-4", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-15", source) + self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) self.assertIn("/static/js/chat.js?v=memory-reference-sync-2", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index f2090598..769e89a4 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -87,6 +87,231 @@ def test_memory_rows_expose_keys_and_ids_for_reference_matching(self): css_source, ) + def test_open_delayed_report_dispatches_avatar_active_state(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn( + '"jin:delayed-memory-report-active"', + source, + ) + self.assertIn( + "function dispatchDelayedMemoryReportAvatarHighlight(", + source, + ) + self.assertIn( + "dispatchDelayedMemoryReportAvatarHighlight(\n delayedMemoryModalReport,\n true", + source, + ) + self.assertIn( + "dispatchDelayedMemoryReportAvatarHighlight(\n delayedMemoryModalReport,\n false", + source, + ) + self.assertIn( + "let activeDelayedMemoryReportId = \"\";", + source, + ) + self.assertIn( + "function setActiveDelayedMemoryReportRow(", + source, + ) + self.assertIn( + '"runtime-memory-delayed-row-active"', + source, + ) + self.assertIn( + ".runtime-memory-delayed-row-active", + css_source, + ) + self.assertIn( + "pointer-events: none;", + css_source, + ) + active_rule_start = css_source.index( + ".runtime-memory-delayed-row-active {" + ) + active_rule_end = css_source.index( + "}", + active_rule_start, + ) + active_rule = css_source[ + active_rule_start:active_rule_end + ] + self.assertNotIn( + "background", + active_rule, + ) + self.assertNotIn( + "box-shadow", + active_rule, + ) + + def test_modal_l4_delete_cleans_local_report_refs_before_sync(self): + source = RUNTIME_JS.read_text(encoding="utf-8") + + self.assertIn( + "function removeLongTermFactIdFromDelayedMemoryReports(", + source, + ) + self.assertIn( + "anchor_fact_ids: nextAnchorFactIds", + source, + ) + self.assertIn( + "facts_ids: nextFactIds", + source, + ) + self.assertIn( + "removeLongTermFactIdFromDelayedMemoryReports(\n factId", + source, + ) + self.assertIn( + "deleteLongTermMemoryFact: deleteLongTermMemoryFactAndRender", + source, + ) + + def test_delayed_report_fact_chip_hover_targets_avatar_l4_dash(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "function dispatchLongTermFactAvatarHover(", + source, + ) + self.assertIn( + 'buildAvatarMemoryHoverId(\n "l4",', + source, + ) + self.assertIn( + 'item.addEventListener("mouseenter"', + source, + ) + self.assertIn( + 'item.addEventListener("mouseleave"', + source, + ) + self.assertIn( + 'item.addEventListener("focus"', + source, + ) + self.assertIn( + 'item.addEventListener("blur"', + source, + ) + + def test_delayed_memory_fact_ids_render_inline_with_fact_titles(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + runtime_source = RUNTIME_JS.read_text(encoding="utf-8") + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn( + 'function appendDelayedMemoryFactIdField(', + source, + ) + self.assertIn( + 'function setDelayedMemoryModalAnchorFactId(', + source, + ) + self.assertIn( + '"delayed-memory-modal-value delayed-memory-modal-fact-ids"', + source, + ) + self.assertIn( + 'item.title =', + source, + ) + self.assertIn( + 'function sortDelayedMemoryFactIdsByNumber(', + source, + ) + self.assertIn( + 'fieldName === "facts_ids"', + source, + ) + self.assertIn( + '"delayed-memory-modal-fact-id-anchor"', + source, + ) + self.assertIn( + 'configureRuntimeMemoryDeleteHold(', + source, + ) + self.assertIn( + 'deleteLongTermMemoryFact(', + source, + ) + self.assertIn( + 'removeDelayedMemoryFactIdFromModal(', + source, + ) + self.assertIn( + 'item.addEventListener("dblclick"', + source, + ) + self.assertIn( + ': !isAnchorFactId', + source, + ) + self.assertIn( + 'return `${key}: ${value}`;', + source, + ) + self.assertIn( + 'l4Memory.getFacts()', + source, + ) + self.assertIn( + 'function setDelayedMemoryReportAnchorFactIds(', + runtime_source, + ) + self.assertIn( + 'anchor_fact_ids:', + runtime_source, + ) + self.assertIn( + 'setDelayedMemoryReportAnchorFactIds,', + runtime_source, + ) + self.assertIn( + '.delayed-memory-modal-fact-ids', + css_source, + ) + self.assertIn( + 'flex-wrap: wrap;', + css_source, + ) + self.assertIn( + 'cursor: pointer;', + css_source, + ) + self.assertIn( + 'color: rgba(244, 244, 245, 0.46);', + css_source, + ) + self.assertIn( + 'text-decoration: none;', + css_source, + ) + self.assertIn( + '.delayed-memory-modal-fact-id-anchor', + css_source, + ) + self.assertIn( + 'color: rgba(255, 255, 255, 0.86);', + css_source, + ) + self.assertIn( + '0 0 2px rgba(255, 255, 255, 0.64)', + css_source, + ) + self.assertNotIn( + 'text-decoration: underline', + css_source, + ) + self.assertNotIn( + 'inset 0 0 0 1px rgba(244, 244, 245', + css_source, + ) + @unittest.skipUnless( shutil.which("node"), "node is required for the browser-side reference matcher test", @@ -143,15 +368,15 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=memory-hover-sync-2', + '/static/css/runtime-memory.css?v=memory-hover-sync-9', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-18', + '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26', source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=runtime-facade-17', + '/static/js/runtime/runtime.js?v=runtime-facade-20', source, ) self.assertIn( diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 90f11c20..0318b5c1 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -39,9 +39,24 @@ def test_l4_facts_are_rendered_as_reference_aware_memory_dashes(self): self.assertIn('kind === "l4"', source) self.assertIn("id,\n key,", source) self.assertIn("referenceAliases: record.referenceAliases", source) + self.assertIn("dot: record.archived", source) + self.assertIn( + '"data-delayed-memory-fact-ids":', + source, + ) + self.assertIn( + '"data-delayed-memory-anchor-fact-ids":', + source, + ) + self.assertIn('"data-l4-fact-ids":', source) + self.assertIn("applyDelayedMemoryFactLinkGlow()", source) + self.assertIn("is-delayed-memory-linked-hit", source) css_source = AVATAR_CSS.read_text(encoding="utf-8") self.assertIn(".jin-avatar-memory-dash-l4", css_source) + self.assertIn(".jin-avatar-memory-dash.is-memory-dot", css_source) + self.assertIn(".is-delayed-memory-linked-hit", css_source) + self.assertIn("@keyframes jin-avatar-memory-absorb-dot", css_source) self.assertIn("rgba(147, 197, 253, 0.30)", css_source) def test_l4_ring_is_appended_inside_delayed_and_active_rings(self): @@ -57,15 +72,43 @@ def test_l4_ring_is_appended_inside_delayed_and_active_rings(self): self.assertLess(l4_index, delayed_index) self.assertLess(delayed_index, active_index) + def test_open_delayed_report_keeps_avatar_fact_links_highlighted(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertIn( + '"jin:delayed-memory-report-active"', + source, + ) + self.assertIn( + "let delayedMemoryReportActiveState = null;", + source, + ) + self.assertIn( + "function getActiveAvatarMemoryHoverIds()", + source, + ) + self.assertIn( + "window.addEventListener(DELAYED_MEMORY_REPORT_ACTIVE_EVENT", + source, + ) + self.assertIn( + "getFocusedMemoryDashNodes(svg)", + source, + ) + self.assertIn( + "is-delayed-memory-linked-hit", + source, + ) + def test_avatar_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/css/runtime-avatar.css?v=memory-rings-4", + "/static/css/runtime-avatar.css?v=memory-rings-6", source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-11", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-15", source, ) diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 25355f0c..a94ab639 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -216,6 +216,53 @@ stroke-width 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); } +.jin-avatar-memory-dash circle { + transition: + fill-opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), + r 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.jin-avatar-memory-dash.is-memory-dot .jin-avatar-memory-dash-arc { + animation: jin-avatar-memory-absorb-dash 0.58s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +} + +.jin-avatar-memory-dash.is-memory-dot .jin-avatar-memory-dot { + animation: jin-avatar-memory-absorb-dot 0.58s cubic-bezier(0.22, 0.61, 0.36, 1) both; + fill-opacity: var(--jin-avatar-memory-dot-opacity, 0.26); + r: var(--jin-avatar-memory-dot-radius, 1.6px); +} + +@keyframes jin-avatar-memory-absorb-dash { + 0% { + stroke-opacity: var(--jin-avatar-memory-dot-opacity, 0.26); + } + + 42% { + stroke-opacity: 0.14; + } + + 100% { + stroke-opacity: 0; + } +} + +@keyframes jin-avatar-memory-absorb-dot { + 0% { + fill-opacity: 0; + r: 0.35px; + } + + 56% { + fill-opacity: var(--jin-avatar-memory-dot-opacity, 0.26); + r: 1.1px; + } + + 100% { + fill-opacity: var(--jin-avatar-memory-dot-opacity, 0.26); + r: var(--jin-avatar-memory-dot-radius, 1.6px); + } +} + .jin-avatar-memory-dash-active { filter: brightness(1.16) @@ -237,7 +284,8 @@ } .jin-avatar-memory-dash.is-runtime-cited, -.jin-avatar-memory-dash.is-memory-reference-hit { +.jin-avatar-memory-dash.is-memory-reference-hit, +.jin-avatar-memory-dash.is-delayed-memory-linked-hit { filter: brightness(1.72) saturate(1.65) @@ -247,12 +295,20 @@ drop-shadow(0 0 38px var(--jin-avatar-memory-glow-far, rgba(34, 211, 238, 0.18))); } -.jin-avatar-memory-dash.is-runtime-cited path, -.jin-avatar-memory-dash.is-memory-reference-hit path { +.jin-avatar-memory-dash:not(.is-memory-dot).is-runtime-cited path, +.jin-avatar-memory-dash:not(.is-memory-dot).is-memory-reference-hit path, +.jin-avatar-memory-dash:not(.is-memory-dot).is-delayed-memory-linked-hit path { stroke-opacity: 1 !important; stroke-width: var(--jin-avatar-memory-hover-width, 2px); } +.jin-avatar-memory-dash.is-memory-dot.is-runtime-cited circle, +.jin-avatar-memory-dash.is-memory-dot.is-memory-reference-hit circle, +.jin-avatar-memory-dash.is-memory-dot.is-delayed-memory-linked-hit circle { + fill-opacity: 1 !important; + r: var(--jin-avatar-memory-hover-dot-radius, 2.45px); +} + .jin-avatar-orbit.is-memory-hover-hit, .jin-avatar-counter-orbit.is-memory-hover-hit, .jin-avatar-memory-dash.is-memory-hover-hit { @@ -264,11 +320,16 @@ drop-shadow(0 0 28px var(--jin-avatar-memory-glow-far, rgba(52, 211, 222, 0.30))); } -.jin-avatar-memory-dash.is-memory-hover-hit path { +.jin-avatar-memory-dash:not(.is-memory-dot).is-memory-hover-hit path { stroke-opacity: 1 !important; stroke-width: calc(var(--jin-avatar-memory-hover-width, 2px) + 0.45px); } +.jin-avatar-memory-dash.is-memory-dot.is-memory-hover-hit circle { + fill-opacity: 1 !important; + r: var(--jin-avatar-memory-hover-dot-radius, 2.45px); +} + .jin-avatar-center-glow-fill, .jin-avatar-center-soft, .jin-avatar-center-core, diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 8e4baf72..456fe206 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -44,6 +44,20 @@ outline: none; } +.runtime-memory-delayed-row-active { + position: relative; + z-index: 60; + pointer-events: none; +} + +.runtime-memory-delayed-row-active .runtime-memory-key, +.runtime-memory-delayed-row-active .runtime-memory-value { + color: rgba(244, 244, 245, 0.94); + text-shadow: + 0 0 5px rgba(240, 253, 250, 0.32), + 0 0 18px rgba(45, 212, 191, 0.18); +} + .runtime-memory-line:not(.runtime-memory-user-idle):hover .runtime-memory-key, .runtime-memory-line:not(.runtime-memory-user-idle):hover .runtime-memory-value, .runtime-memory-line:not(.runtime-memory-user-idle):focus-visible .runtime-memory-key, @@ -160,6 +174,51 @@ color: rgba(244, 244, 245, 0.86); } +.delayed-memory-modal-fact-ids { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px 8px; + white-space: normal; +} + +.delayed-memory-modal-fact-id { + display: inline-flex; + align-items: center; + max-width: 100%; + color: rgba(244, 244, 245, 0.46); + cursor: pointer; + transition: + opacity 0.18s ease, + color 0.18s ease, + text-shadow 0.18s ease; + text-decoration: none; +} + +.delayed-memory-modal-fact-id:hover, +.delayed-memory-modal-fact-id:focus-visible { + color: rgba(244, 244, 245, 0.64); + outline: none; + text-decoration: none; +} + +.delayed-memory-modal-fact-id-anchor { + color: rgba(255, 255, 255, 0.86); + text-shadow: + 0 0 2px rgba(255, 255, 255, 0.64), + 0 0 5px rgba(255, 255, 255, 0.42), + 0 0 9px rgba(241, 245, 249, 0.26); +} + +.delayed-memory-modal-fact-id-anchor:hover, +.delayed-memory-modal-fact-id-anchor:focus-visible { + color: rgba(255, 255, 255, 0.94); + text-shadow: + 0 0 2px rgba(255, 255, 255, 0.74), + 0 0 6px rgba(255, 255, 255, 0.50), + 0 0 10px rgba(241, 245, 249, 0.30); +} + .delayed-memory-modal-section { margin-top: 18px; } diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index a96c2140..05add8f8 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -2486,6 +2486,9 @@ function appendLog( return logDiv; } +window.handleL4LoggerMemoryRestoreResult = + handleL4MemoryRestoreResult; + window.handleL4MemoryRestoreResult = handleL4MemoryRestoreResult; diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 3e030c63..254f3899 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -7,6 +7,8 @@ const AVATAR_EVENT = "jin:runtime-avatar-snapshot"; const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = "jin:think-runtime-citation-highlight"; const MEMORY_ROW_AVATAR_HOVER_EVENT = "jin:memory-row-avatar-hover"; + const DELAYED_MEMORY_REPORT_ACTIVE_EVENT = + "jin:delayed-memory-report-active"; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = "jin:memory-reference-highlight"; const MEMORY_REFERENCE_ALIAS_DATASET_KEY = @@ -221,6 +223,102 @@ node.appendChild(title); } + function normalizeL4FactIds(value) { + const source = + Array.isArray(value) + ? value + : [value]; + const factIds = []; + const seen = new Set(); + + source.forEach((item) => { + if (Array.isArray(item)) { + normalizeL4FactIds(item).forEach((factId) => { + if (seen.has(factId)) { + return; + } + + seen.add(factId); + factIds.push(factId); + }); + return; + } + + const text = + String(item || "").trim(); + + if (text.startsWith("[") && text.endsWith("]")) { + try { + const parsed = JSON.parse(text); + + if (Array.isArray(parsed)) { + normalizeL4FactIds(parsed).forEach((factId) => { + if (seen.has(factId)) { + return; + } + + seen.add(factId); + factIds.push(factId); + }); + return; + } + } catch (_error) { + // Fall through to token parsing. + } + } + + text + .split(/[\s,;]+/) + .map(candidate => ( + String(candidate || "") + .trim() + .replace(/^["'\[]+|["'\]]+$/g, "") + .toUpperCase() + )) + .forEach((candidate) => { + if ( + !/^F[1-9]\d*$/.test(candidate) + || seen.has(candidate) + ) { + return; + } + + seen.add(candidate); + factIds.push(candidate); + }); + }); + + return factIds; + } + + function serializeL4FactIds(value) { + const factIds = + normalizeL4FactIds(value); + + return factIds.length + ? factIds.join(",") + : null; + } + + function addL4FactIds(target, value) { + normalizeL4FactIds(value) + .forEach(factId => target.add(factId)); + } + + function l4FactIdSetsIntersect(left, right) { + if (!left || !right || !left.size || !right.size) { + return false; + } + + for (const factId of left) { + if (right.has(factId)) { + return true; + } + } + + return false; + } + function countOccurrences(text, words) { const source = String(text || "").toLowerCase(); @@ -674,6 +772,10 @@ String(report.title || "").trim(); const summary = String(report.summary || "").trim(); + const anchorFactIds = + normalizeL4FactIds(report.anchor_fact_ids); + const factIds = + normalizeL4FactIds(report.facts_ids); if (!id || !title) { return null; @@ -684,6 +786,8 @@ title, summary, pinned: Boolean(report.pinned), + anchorFactIds, + factIds, avatarMemoryHoverId: buildAvatarMemoryHoverId( "delayed", @@ -709,7 +813,9 @@ const l4Memory = window.JinRuntime && window.JinRuntime.l4Memory; const facts = - l4Memory && typeof l4Memory.getFacts === "function" + l4Memory && typeof l4Memory.getFactsWithArchiveState === "function" + ? l4Memory.getFactsWithArchiveState() + : l4Memory && typeof l4Memory.getFacts === "function" ? l4Memory.getFacts() : []; @@ -727,6 +833,11 @@ const key = String(fact.key || "").trim(); const value = String(fact.value || fact.content || "").trim(); const lineText = `${key}: ${value}`.trim(); + const l4FactIds = + normalizeL4FactIds([ + id, + fact.source_fact_ids, + ]); if (!id || !key || !value) { return null; @@ -737,6 +848,12 @@ key, value, text: lineText, + l4FactIds, + archived: + Boolean( + fact.archived + || fact.hidden_from_context + ), avatarMemoryHoverId: buildAvatarMemoryHoverId( "l4", @@ -777,6 +894,11 @@ const endAngle = options.angle + arcDegrees / 2; const color = options.color; const glowColor = options.glowColor || color; + const isDot = Boolean(options.dot); + const dotRadius = + isDot + ? Math.max(layout.strokeWidth * 1.45, 1.35) + : 0; const classNames = [ "jin-avatar-memory-dash", `jin-avatar-memory-dash-${options.kind}`, @@ -786,6 +908,14 @@ classNames.push("is-memory-pinned"); } + if (options.archived) { + classNames.push("is-memory-archived"); + } + + if (isDot) { + classNames.push("is-memory-dot"); + } + const dashGroup = createSvgElement("g", { class: classNames.join(" "), "data-avatar-memory-hover-id": options.avatarMemoryHoverId || null, @@ -793,7 +923,12 @@ "data-runtime-line-text": options.citationText || null, "data-runtime-line-identity": options.citationIdentity || null, "data-delayed-memory-id": options.delayedMemoryId || null, + "data-delayed-memory-fact-ids": + serializeL4FactIds(options.delayedMemoryFactIds), + "data-delayed-memory-anchor-fact-ids": + serializeL4FactIds(options.delayedMemoryAnchorFactIds), "data-l4-fact-id": options.l4FactId || null, + "data-l4-fact-ids": serializeL4FactIds(options.l4FactIds), }); setAvatarMemoryReferenceAliases( @@ -804,7 +939,8 @@ setMemoryDashGlowVariables( dashGroup, glowColor, - layout.strokeWidth + 0.75 + layout.strokeWidth + 0.75, + dotRadius ); appendTitle( @@ -813,6 +949,7 @@ ); dashGroup.appendChild(createSvgElement("path", { + class: isDot ? "jin-avatar-memory-dash-arc" : null, d: describeArcPath(layout.radius, startAngle, endAngle), fill: "none", stroke: color, @@ -821,10 +958,32 @@ "stroke-linecap": "round", })); + if (isDot) { + const dotPoint = polarPoint(layout.radius, options.angle); + + dashGroup.style.setProperty( + "--jin-avatar-memory-dot-opacity", + Number(options.opacity || 0.28).toFixed(2) + ); + dashGroup.appendChild(createSvgElement("circle", { + class: "jin-avatar-memory-dot", + cx: dotPoint.x.toFixed(3), + cy: dotPoint.y.toFixed(3), + r: dotRadius.toFixed(2), + fill: color, + "fill-opacity": options.opacity, + })); + } + parent.appendChild(dashGroup); } - function setMemoryDashGlowVariables(dashGroup, glowColor, hoverWidth) { + function setMemoryDashGlowVariables( + dashGroup, + glowColor, + hoverWidth, + dotRadius + ) { const glowRgb = hexToRgb(glowColor); dashGroup.style.setProperty( @@ -843,6 +1002,17 @@ "--jin-avatar-memory-hover-width", `${Number(hoverWidth || 2).toFixed(2)}px` ); + + if (dotRadius) { + dashGroup.style.setProperty( + "--jin-avatar-memory-dot-radius", + `${Number(dotRadius).toFixed(2)}px` + ); + dashGroup.style.setProperty( + "--jin-avatar-memory-hover-dot-radius", + `${Number(dotRadius + 0.85).toFixed(2)}px` + ); + } } function getMemoryRingAnimation(records, kind) { @@ -946,13 +1116,16 @@ arcDegrees, color, glowColor: L4_MEMORY_RING_COLOR, - opacity: 0.52, + opacity: record.archived ? 0.26 : 0.52, + archived: record.archived, + dot: record.archived, avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.key), citationText: record.citationText, citationIdentity: record.citationIdentity, l4FactId: record.id, + l4FactIds: record.l4FactIds, referenceAliases: record.referenceAliases, title: `L4 ${record.id} · ${record.key}: ${record.value}`, } @@ -982,6 +1155,8 @@ citationKey: normalizeRuntimeCitationIdentity(record.id), delayedMemoryId: record.id, + delayedMemoryFactIds: record.factIds, + delayedMemoryAnchorFactIds: record.anchorFactIds, referenceAliases: record.referenceAliases, title: `${record.title}${record.summary ? `: ${record.summary}` : ""}`, } @@ -1043,6 +1218,8 @@ ); } + applyDelayedMemoryFactLinkGlow(); + return true; } @@ -1499,8 +1676,9 @@ let currentRenderedSnapshotIndex = null; const activeThinkRuntimeCitationSources = new Map(); let memoryRowAvatarHoverState = null; + let delayedMemoryReportActiveState = null; - function normalizeMemoryRowAvatarHoverDetail(detail) { + function normalizeAvatarMemoryFocusDetail(detail) { if (!detail || detail.active !== true) { return null; } @@ -1513,6 +1691,135 @@ : null; } + function normalizeMemoryRowAvatarHoverDetail(detail) { + return normalizeAvatarMemoryFocusDetail(detail); + } + + function getActiveAvatarMemoryHoverIds() { + return new Set([ + memoryRowAvatarHoverState + ? memoryRowAvatarHoverState.avatarMemoryHoverId + : "", + delayedMemoryReportActiveState + ? delayedMemoryReportActiveState.avatarMemoryHoverId + : "", + ].filter(Boolean)); + } + + function getAvatarDashFactIdSet(node, datasetKey) { + return new Set( + normalizeL4FactIds( + node && node.dataset + ? node.dataset[datasetKey] + : "" + ) + ); + } + + function getFocusedMemoryDashNodes(svg) { + const hoverIds = + getActiveAvatarMemoryHoverIds(); + + if (!hoverIds.size) { + return []; + } + + return Array.from( + svg.querySelectorAll(".jin-avatar-memory-dash") + ).filter((node) => ( + hoverIds.has(node.dataset.avatarMemoryHoverId) + )); + } + + function collectDelayedMemoryLinkedL4FactIds(svg) { + const factIds = new Set(); + + Array.from( + svg.querySelectorAll( + ".jin-avatar-memory-dash-delayed.is-memory-pinned" + ) + ).forEach((node) => { + addL4FactIds( + factIds, + node.dataset.delayedMemoryFactIds + ); + }); + + getFocusedMemoryDashNodes(svg) + .filter(node => ( + node.classList.contains( + "jin-avatar-memory-dash-delayed" + ) + )) + .forEach((node) => { + addL4FactIds( + factIds, + node.dataset.delayedMemoryFactIds + ); + }); + + return factIds; + } + + function collectHoveredL4FactIds(svg) { + const factIds = new Set(); + + getFocusedMemoryDashNodes(svg) + .filter(node => ( + node.classList.contains( + "jin-avatar-memory-dash-l4" + ) + )) + .forEach((node) => { + addL4FactIds( + factIds, + node.dataset.l4FactIds + ); + }); + + return factIds; + } + + function applyDelayedMemoryFactLinkGlow() { + const svg = avatarRoot.querySelector("svg"); + + if (!svg) { + return; + } + + const delayedLinkedFactIds = + collectDelayedMemoryLinkedL4FactIds(svg); + const hoveredL4FactIds = + collectHoveredL4FactIds(svg); + + Array.from( + svg.querySelectorAll(".jin-avatar-memory-dash-l4") + ).forEach((node) => { + node.classList.toggle( + "is-delayed-memory-linked-hit", + l4FactIdSetsIntersect( + getAvatarDashFactIdSet(node, "l4FactIds"), + delayedLinkedFactIds + ) + ); + }); + + Array.from( + svg.querySelectorAll(".jin-avatar-memory-dash-delayed") + ).forEach((node) => { + node.classList.toggle( + "is-delayed-memory-linked-hit", + l4FactIdSetsIntersect( + getAvatarDashFactIdSet( + node, + "delayedMemoryAnchorFactIds" + ), + hoveredL4FactIds + ) + ); + }); + } + function applyMemoryRowAvatarHoverGlow() { const svg = avatarRoot.querySelector("svg"); @@ -1520,13 +1827,17 @@ return; } + const activeHoverIds = + getActiveAvatarMemoryHoverIds(); + svg.querySelectorAll( ".jin-avatar-orbit, .jin-avatar-counter-orbit, .jin-avatar-memory-dash" ).forEach((node) => { const matched = Boolean( - memoryRowAvatarHoverState - && node.dataset.avatarMemoryHoverId - === memoryRowAvatarHoverState.avatarMemoryHoverId + node.dataset.avatarMemoryHoverId + && activeHoverIds.has( + node.dataset.avatarMemoryHoverId + ) ); node.classList.toggle( @@ -1534,6 +1845,8 @@ matched ); }); + + applyDelayedMemoryFactLinkGlow(); } function normalizeThinkRuntimeCitationHighlightDetail(detail) { @@ -1730,8 +2043,21 @@ snapshot && snapshot.total_diff, lines.map(line => [line.key, line.value, line.status, line.key_status, line.value_status].join("␟")).join("␞"), activeMemoryRecords.map(record => record.text).join("␞"), - delayedMemoryRecords.map(record => [record.id, record.title, record.summary, record.pinned].join("␟")).join("␞"), - l4MemoryRecords.map(record => [record.id, record.key, record.value].join("␟")).join("␞"), + delayedMemoryRecords.map(record => [ + record.id, + record.title, + record.summary, + record.pinned, + record.anchorFactIds.join(","), + record.factIds.join(","), + ].join("␟")).join("␞"), + l4MemoryRecords.map(record => [ + record.id, + record.key, + record.value, + record.archived ? "archived" : "visible", + record.l4FactIds.join(","), + ].join("␟")).join("␞"), ].join("␝"); } @@ -2082,6 +2408,15 @@ applyMemoryRowAvatarHoverGlow(); }); + window.addEventListener(DELAYED_MEMORY_REPORT_ACTIVE_EVENT, (event) => { + delayedMemoryReportActiveState = + normalizeAvatarMemoryFocusDetail( + event && event.detail || {} + ); + + applyMemoryRowAvatarHoverGlow(); + }); + window.addEventListener(THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT, (event) => { const detail = event && event.detail || {}; const sourceId = diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 4a286c45..37a01006 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -284,6 +284,125 @@ return readStore().facts; } + function getArchivedFactIdSet() { + const reports = + storage && typeof storage.readDelayedMemoryReports === "function" + ? storage.readDelayedMemoryReports() + : {}; + + if ( + !reports + || typeof reports !== "object" + || Array.isArray(reports) + ) { + return new Set(); + } + + const archivedIds = new Set(); + const anchorIds = new Set(); + + Object.values(reports).forEach((report) => { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return; + } + + normalizeList(report.anchor_fact_ids).forEach((rawId) => { + const factId = normalizeFactId(rawId, false); + if (factId) { + anchorIds.add(factId); + } + }); + + [ + report.facts_ids, + report.absorbed_fact_ids, + report.long_term_facts_ids, + ].forEach((rawIds) => { + normalizeList(rawIds).forEach((rawId) => { + const factId = normalizeFactId(rawId, false); + if (factId) { + archivedIds.add(factId); + } + }); + }); + }); + + anchorIds.forEach((factId) => { + archivedIds.delete(factId); + }); + + return archivedIds; + } + + function getArchivedFactIds() { + return Array.from(getArchivedFactIdSet()) + .sort((left, right) => String(left).localeCompare(String(right))); + } + + function isArchivedFact(factId) { + const normalizedId = + normalizeFactId(factId, false); + + return Boolean( + normalizedId + && getArchivedFactIdSet().has(normalizedId) + ); + } + + function factMatchesArchivedIds(fact, archivedIds) { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + || !archivedIds + || archivedIds.size < 1 + ) { + return false; + } + + return [ + fact.id, + ...normalizeList(fact.source_fact_ids), + ].some((rawId) => { + const factId = + normalizeFactId(rawId, false); + + return Boolean( + factId + && archivedIds.has(factId) + ); + }); + } + + function getVisibleFacts() { + const archivedIds = + getArchivedFactIdSet(); + + return getFacts().filter((fact) => ( + !factMatchesArchivedIds(fact, archivedIds) + )); + } + + function getFactsWithArchiveState() { + const archivedIds = + getArchivedFactIdSet(); + + return getFacts().map((fact) => { + const archived = + factMatchesArchivedIds(fact, archivedIds); + + return { + ...fact, + archived, + hidden_from_context: archived, + }; + }); + } + function getPendingFacts() { return readStore().pending_facts; } @@ -319,6 +438,61 @@ return sendIfOpen(buildStoreSyncPayload()); } + function deleteFactLocally(factId) { + const id = + normalizeFactId(factId, false); + + if (!id) { + return false; + } + + const store = + readStore(); + const facts = + (Array.isArray(store.facts) ? store.facts : []) + .filter(fact => fact && fact.id !== id); + const deletedFactIds = + normalizeList(store.deleted_fact_ids) + .map(rawId => normalizeFactId(rawId, false)) + .filter(Boolean); + const removedFact = + facts.length !== (Array.isArray(store.facts) ? store.facts.length : 0); + + if (!deletedFactIds.includes(id)) { + deletedFactIds.push(id); + } + + if (!removedFact && store.deleted_fact_ids.includes(id)) { + return false; + } + + writeStore({ + ...store, + facts, + deleted_fact_ids: deletedFactIds, + revision: Math.max( + 0, + Math.floor(normalizeNumber(store.revision, 0)) + ) + 1, + updated_at: new Date().toISOString(), + }); + + if ( + window.JinRuntime.runtime + && window.JinRuntime.runtime.renderRuntimeMemorySnapshot + ) { + window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); + } + if ( + window.JinRuntime.avatar + && typeof window.JinRuntime.avatar.refresh === "function" + ) { + window.JinRuntime.avatar.refresh(); + } + + return true; + } + function applyFactsMemoryRecordsUpdate(payload) { const records = payload && Array.isArray(payload.records) ? payload.records @@ -385,6 +559,12 @@ ) { window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); } + if ( + window.JinRuntime.avatar + && typeof window.JinRuntime.avatar.refresh === "function" + ) { + window.JinRuntime.avatar.refresh(); + } return store; } @@ -393,10 +573,14 @@ if (!id) { return false; } - return sendIfOpen({ + const sent = sendIfOpen({ type: "l4_memory_delete_fact", fact_id: id, }); + if (sent && deleteFactLocally(id)) { + syncLongTermMemoryToRuntime(); + } + return sent; } function requestFactRestore(fact) { @@ -407,9 +591,23 @@ if (!normalized) { return false; } + const restoreMeta = + fact + && typeof fact === "object" + && !Array.isArray(fact) + && fact._restore_meta + && typeof fact._restore_meta === "object" + ? fact._restore_meta + : null; + return sendIfOpen({ type: "l4_memory_restore_fact", - fact: normalized, + fact: restoreMeta + ? { + ...normalized, + _restore_meta: restoreMeta, + } + : normalized, }); } @@ -461,6 +659,10 @@ writeStore, normalizeStore, getFacts, + getArchivedFactIds, + isArchivedFact, + getVisibleFacts, + getFactsWithArchiveState, getPendingFacts, buildFactsMemorySyncPayload, buildStoreSyncPayload, @@ -468,6 +670,7 @@ syncLongTermMemoryToRuntime, applyFactsMemoryRecordsUpdate, applyServerUpdate, + deleteFactLocally, requestFactDelete, requestFactRestore, maybeSendIdleTick, diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index c7639bb9..6677ff03 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -12,6 +12,7 @@ let deleteRuntimeMemoryLine = null; let getDelayedMemoryReports = null; let setDelayedMemoryReportPinned = null; + let setDelayedMemoryReportAnchorFactIds = null; let getFactsMemoryFields = null; let deleteFactsMemoryField = null; let getLongTermMemoryFacts = null; @@ -23,6 +24,8 @@ const MEMORY_DELETE_HOLD_MS = 1500; const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = "jin:think-runtime-citation-highlight"; const MEMORY_ROW_AVATAR_HOVER_EVENT = "jin:memory-row-avatar-hover"; + const DELAYED_MEMORY_REPORT_ACTIVE_EVENT = + "jin:delayed-memory-report-active"; const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; const buildCitationRecordIdentity = @@ -55,6 +58,7 @@ let delayedMemoryModalContent = null; let delayedMemoryModalPinButton = null; let delayedMemoryModalReport = null; + let activeDelayedMemoryReportId = ""; const runtimeDiffHistory = { diffs: [], @@ -1563,6 +1567,33 @@ ); } + function dispatchDelayedMemoryReportAvatarHighlight( + report, + active + ) { + const avatarMemoryHoverId = + buildAvatarMemoryHoverId( + "delayed", + report && report._storage_key + ); + + window.dispatchEvent( + new CustomEvent( + DELAYED_MEMORY_REPORT_ACTIVE_EVENT, + { + detail: active && avatarMemoryHoverId + ? { + active: true, + avatarMemoryHoverId, + } + : { + active: false, + }, + } + ) + ); + } + function dispatchRuntimeMemoryLineAvatarHover( row, active @@ -1584,6 +1615,28 @@ ); } + function dispatchLongTermFactAvatarHover( + factId, + active + ) { + const avatarMemoryHoverId = + buildAvatarMemoryHoverId( + "l4", + factId + ); + + dispatchMemoryRowAvatarHover( + active && avatarMemoryHoverId + ? { + active: true, + avatarMemoryHoverId, + } + : { + active: false, + } + ); + } + function clearRuntimeMemoryLineAvatarHover() { dispatchRuntimeMemoryLineAvatarHover( null, @@ -2316,6 +2369,20 @@ row.dataset.memoryHighlightSortIndex = String(index); + const reportId = + normalizeDelayedMemoryReportId( + report._storage_key + ); + + if ( + reportId + && reportId === activeDelayedMemoryReportId + ) { + row.classList.add( + "runtime-memory-delayed-row-active" + ); + } + if (Boolean(report.pinned)) { row.classList.add( "runtime-memory-delayed-row-pinned" @@ -2356,12 +2423,12 @@ row.title; row.dataset.delayedMemoryId = normalizeRuntimeCitationIdentity( - report._storage_key + reportId || report._storage_key ); row.dataset.avatarMemoryHoverId = buildAvatarMemoryHoverId( "delayed", - report._storage_key + reportId || report._storage_key ); setMemoryReferenceAliases( row, @@ -2439,6 +2506,268 @@ .trim(); } + function normalizeDelayedMemoryTooltipText(value) { + return normalizeDelayedMemoryDisplayText(value) + .replace(/\s+/g, " "); + } + + function normalizeDelayedMemoryFactId(value) { + const raw = + normalizeDelayedMemoryDisplayText(value) + .replace(/^["']|["']$/g, ""); + + const match = + raw.match(/^F([1-9]\d*)$/i); + + return match + ? `F${match[1]}` + : raw; + } + + function normalizeDelayedMemoryFactIds(value) { + const candidates = + Array.isArray(value) + ? value + : normalizeDelayedMemoryDisplayText(value) + .match(/\bF[1-9]\d*\b/gi) || []; + + const seen = + new Set(); + + return candidates + .map(item => normalizeDelayedMemoryFactId(item)) + .filter((factId) => { + if (!factId || seen.has(factId)) { + return false; + } + + seen.add(factId); + return true; + }); + } + + function getDelayedMemoryFactIdNumber(factId) { + const match = + String(factId || "").match(/^F([1-9]\d*)$/); + + return match + ? Number(match[1]) + : Number.POSITIVE_INFINITY; + } + + function sortDelayedMemoryFactIdsByNumber(factIds) { + return [...factIds].sort((left, right) => { + const leftNumber = + getDelayedMemoryFactIdNumber(left); + const rightNumber = + getDelayedMemoryFactIdNumber(right); + + if (leftNumber !== rightNumber) { + return leftNumber - rightNumber; + } + + return String(left).localeCompare( + String(right) + ); + }); + } + + function isDelayedMemoryFactIdField(key) { + return [ + "anchor_fact_ids", + "facts_ids", + "absorbed_fact_ids", + "long_term_facts_ids", + ].includes( + String(key || "").trim() + ); + } + + function appendDelayedMemoryFactLookupEntry( + lookup, + fact + ) { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + ) { + return; + } + + const factId = + normalizeDelayedMemoryFactId(fact.id); + + if (factId && !lookup.has(factId)) { + lookup.set( + factId, + fact + ); + } + + (Array.isArray(fact.source_fact_ids) ? fact.source_fact_ids : []) + .forEach((sourceFactId) => { + const normalizedSourceId = + normalizeDelayedMemoryFactId(sourceFactId); + + if (normalizedSourceId && !lookup.has(normalizedSourceId)) { + lookup.set( + normalizedSourceId, + fact + ); + } + }); + } + + function getDelayedMemoryFactLookup() { + const lookup = + new Map(); + + getLongTermMemoryFactRecords() + .forEach(fact => appendDelayedMemoryFactLookupEntry( + lookup, + fact + )); + + const l4Memory = + window.JinRuntime + && window.JinRuntime.l4Memory; + + const allFacts = + l4Memory && typeof l4Memory.getFacts === "function" + ? l4Memory.getFacts() + : []; + + (Array.isArray(allFacts) ? allFacts : []) + .forEach(fact => appendDelayedMemoryFactLookupEntry( + lookup, + fact + )); + + return lookup; + } + + function buildDelayedMemoryFactIdTitle( + factId, + factLookup + ) { + const fact = + factLookup.get(factId); + + if (!fact) { + return `Fact ${factId}`; + } + + const key = + normalizeDelayedMemoryTooltipText(fact.key); + const value = + normalizeDelayedMemoryTooltipText( + fact.value || fact.content + ); + + if (key && value) { + return `${key}: ${value}`; + } + + return key || value || `Fact ${factId}`; + } + + function removeDelayedMemoryFactIdFromModal(factId) { + const normalizedFactId = + normalizeDelayedMemoryFactId(factId); + + if ( + !normalizedFactId + || !delayedMemoryModalContent + ) { + return; + } + + Array.from( + delayedMemoryModalContent.querySelectorAll( + ".delayed-memory-modal-fact-id" + ) + ).forEach((item) => { + if (item.dataset.delayedMemoryFactId !== normalizedFactId) { + return; + } + + const list = + item.closest( + ".delayed-memory-modal-fact-ids" + ); + const row = + item.closest( + ".delayed-memory-modal-field" + ); + + item.remove(); + + if (list && list.childElementCount < 1 && row) { + row.remove(); + } + }); + } + + function setDelayedMemoryModalAnchorFactId( + factId, + anchor + ) { + const normalizedFactId = + normalizeDelayedMemoryFactId(factId); + + if ( + !normalizedFactId + || !delayedMemoryModalReport + || typeof setDelayedMemoryReportAnchorFactIds !== "function" + ) { + return false; + } + + const currentAnchorIds = + new Set( + normalizeDelayedMemoryFactIds( + delayedMemoryModalReport.anchor_fact_ids + ) + ); + const hadAnchor = + currentAnchorIds.has(normalizedFactId); + + if (anchor) { + currentAnchorIds.add(normalizedFactId); + } else { + currentAnchorIds.delete(normalizedFactId); + } + + if (hadAnchor === currentAnchorIds.has(normalizedFactId)) { + return false; + } + + const nextAnchorIds = + sortDelayedMemoryFactIdsByNumber( + Array.from(currentAnchorIds) + ); + const updatedReport = + setDelayedMemoryReportAnchorFactIds( + delayedMemoryModalReport._storage_key, + nextAnchorIds + ); + + if ( + !updatedReport + || typeof updatedReport !== "object" + || Array.isArray(updatedReport) + ) { + return false; + } + + openDelayedMemoryReportModal( + updatedReport + ); + + return true; + } + function padDelayedMemoryDatePart(value) { return String(value).padStart( 2, @@ -2595,12 +2924,52 @@ pinned ? "Unpin delayed memory" : "Pin delayed memory"; } + function setActiveDelayedMemoryReportRow( + reportId + ) { + const nextId = + normalizeDelayedMemoryReportId(reportId); + + activeDelayedMemoryReportId = + isDelayedMemoryReportId(nextId) + ? nextId + : ""; + + if (!runtimeMemoryText) { + return; + } + + Array.from( + runtimeMemoryText.querySelectorAll( + ".runtime-memory-delayed-row" + ) + ).forEach((row) => { + row.classList.toggle( + "runtime-memory-delayed-row-active", + Boolean( + activeDelayedMemoryReportId + && normalizeDelayedMemoryReportId( + row.dataset.delayedMemoryId + ) === activeDelayedMemoryReportId + ) + ); + }); + } + function closeDelayedMemoryReportModal() { if (!delayedMemoryModal) { return; } + dispatchDelayedMemoryReportAvatarHighlight( + delayedMemoryModalReport, + false + ); + setActiveDelayedMemoryReportRow( + "" + ); + delayedMemoryModal.classList.add( "hidden" ); @@ -2777,16 +3146,12 @@ }); } - function appendDelayedMemoryModalField(parent, label, value) { - const normalizedValue = - Array.isArray(value) - ? value - .map((item) => normalizeDelayedMemoryDisplayText(item)) - .filter(Boolean) - .join(", ") - : normalizeDelayedMemoryDisplayText(value); - - if (!normalizedValue) { + function appendDelayedMemoryModalFieldNode( + parent, + label, + valueNode + ) { + if (!valueNode) { return; } @@ -2805,6 +3170,32 @@ key.textContent = label; + row.appendChild( + key + ); + + row.appendChild( + valueNode + ); + + parent.appendChild( + row + ); + } + + function appendDelayedMemoryModalField(parent, label, value) { + const normalizedValue = + Array.isArray(value) + ? value + .map((item) => normalizeDelayedMemoryDisplayText(item)) + .filter(Boolean) + .join(", ") + : normalizeDelayedMemoryDisplayText(value); + + if (!normalizedValue) { + return; + } + const text = document.createElement("div"); @@ -2814,16 +3205,151 @@ text.textContent = normalizedValue; - row.appendChild( - key - ); - - row.appendChild( + appendDelayedMemoryModalFieldNode( + parent, + label, text ); + } - parent.appendChild( - row + function appendDelayedMemoryFactIdField( + parent, + label, + value, + anchorFactIds = new Set() + ) { + const fieldName = + String(label || "").trim(); + const normalizedFactIds = + normalizeDelayedMemoryFactIds(value); + const factIds = + fieldName === "facts_ids" + ? sortDelayedMemoryFactIdsByNumber(normalizedFactIds) + : normalizedFactIds; + + if (!factIds.length) { + if (Array.isArray(value) && value.length < 1) { + appendDelayedMemoryModalField( + parent, + label, + "[]" + ); + } + + return; + } + + const factLookup = + getDelayedMemoryFactLookup(); + + const list = + document.createElement("div"); + + list.className = + "delayed-memory-modal-value delayed-memory-modal-fact-ids"; + + factIds.forEach((factId) => { + const item = + document.createElement("span"); + const isAnchorFactId = + anchorFactIds.has(factId); + + const title = + buildDelayedMemoryFactIdTitle( + factId, + factLookup + ); + + item.className = + "delayed-memory-modal-fact-id"; + item.classList.toggle( + "delayed-memory-modal-fact-id-anchor", + isAnchorFactId + ); + item.textContent = + factId; + item.title = + title; + item.dataset.delayedMemoryFactId = + factId; + item.setAttribute( + "aria-label", + `${factId}: ${title}` + ); + item.setAttribute( + "tabindex", + "0" + ); + + item.addEventListener("mouseenter", () => { + dispatchLongTermFactAvatarHover( + factId, + true + ); + }); + + item.addEventListener("mouseleave", () => { + dispatchLongTermFactAvatarHover( + factId, + false + ); + }); + + item.addEventListener("focus", () => { + dispatchLongTermFactAvatarHover( + factId, + true + ); + }); + + item.addEventListener("blur", () => { + dispatchLongTermFactAvatarHover( + factId, + false + ); + }); + + item.addEventListener("dblclick", (event) => { + event.preventDefault(); + event.stopPropagation(); + + setDelayedMemoryModalAnchorFactId( + factId, + fieldName === "anchor_fact_ids" + ? false + : !isAnchorFactId + ); + }); + + configureRuntimeMemoryDeleteHold( + item, + () => { + if (typeof deleteLongTermMemoryFact !== "function") { + return; + } + + const deleted = + deleteLongTermMemoryFact( + factId + ); + + if (deleted !== false) { + removeDelayedMemoryFactIdFromModal( + factId + ); + } + } + ); + + list.appendChild( + item + ); + }); + + appendDelayedMemoryModalFieldNode( + parent, + label, + list ); } @@ -2873,6 +3399,13 @@ } function appendDelayedMemoryModalExtraFields(parent, report) { + const anchorFactIds = + new Set( + normalizeDelayedMemoryFactIds( + report && report.anchor_fact_ids + ) + ); + const shownKeys = new Set([ "_storage_key", @@ -2894,8 +3427,21 @@ return; } + if (isDelayedMemoryFactIdField(key)) { + appendDelayedMemoryFactIdField( + parent, + key, + value, + anchorFactIds + ); + return; + } + const normalizedValue = - typeof value === "object" + Array.isArray(value) && value.length < 1 + ? "[]" + : typeof value === "object" + && !Array.isArray(value) ? JSON.stringify( value, null, @@ -2923,6 +3469,9 @@ delayedMemoryModalReport = { ...resolvedReport, }; + setActiveDelayedMemoryReportRow( + delayedMemoryModalReport._storage_key + ); updateDelayedMemoryModalPinState( delayedMemoryModalReport ); @@ -2998,6 +3547,11 @@ delayedMemoryModal.classList.add( "flex" ); + + dispatchDelayedMemoryReportAvatarHighlight( + delayedMemoryModalReport, + true + ); } function buildFactsMemoryLine(record) { @@ -3500,6 +4054,8 @@ deleteRuntimeMemoryLine = options.deleteRuntimeMemoryLine || null; getDelayedMemoryReports = options.getDelayedMemoryReports || null; setDelayedMemoryReportPinned = options.setDelayedMemoryReportPinned || null; + setDelayedMemoryReportAnchorFactIds = + options.setDelayedMemoryReportAnchorFactIds || null; getFactsMemoryFields = options.getFactsMemoryFields || null; deleteFactsMemoryField = options.deleteFactsMemoryField || null; getLongTermMemoryFacts = options.getLongTermMemoryFacts || null; diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index a17ffec6..5783a9a5 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1119,11 +1119,45 @@ const factIds = []; source.forEach(function (item) { - String(item || "") + if (Array.isArray(item)) { + normalizeLongTermFactIds(item).forEach(function (factId) { + if (!seen.has(factId)) { + seen.add(factId); + factIds.push(factId); + } + }); + return; + } + + const text = + String(item || "").trim(); + + if (text.startsWith("[") && text.endsWith("]")) { + try { + const parsed = JSON.parse(text); + + if (Array.isArray(parsed)) { + normalizeLongTermFactIds(parsed).forEach(function (factId) { + if (!seen.has(factId)) { + seen.add(factId); + factIds.push(factId); + } + }); + return; + } + } catch (_error) { + // Fall through to token parsing. + } + } + + text .split(/[\s,;]+/) .forEach(function (candidate) { const factId = - String(candidate || "").trim().toUpperCase(); + String(candidate || "") + .trim() + .replace(/^["'\[]+|["'\]]+$/g, "") + .toUpperCase(); if ( !/^F[1-9]\d*$/.test(factId) @@ -1221,20 +1255,14 @@ normalizeLongTermFactIds( report.anchor_fact_ids ), - absorbed_fact_ids: + facts_ids: normalizeLongTermFactIds( [ - ...(Array.isArray(report.absorbed_fact_ids) - ? report.absorbed_fact_ids - : []), - ...(Array.isArray(report.long_term_facts_ids) - ? report.long_term_facts_ids - : []), + report.anchor_fact_ids, + report.facts_ids, + report.absorbed_fact_ids, + report.long_term_facts_ids, ] - ).filter( - factId => !normalizeLongTermFactIds( - report.anchor_fact_ids - ).includes(factId) ), created_session_id: String(report.created_session_id || "").trim(), diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index c426a649..58646530 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -568,18 +568,12 @@ memoryView.init({ deleteRuntimeMemoryLine: deleteRuntimeMemoryLineAndRender, getDelayedMemoryReports: readDelayedMemoryReports, setDelayedMemoryReportPinned, + setDelayedMemoryReportAnchorFactIds, + removeLongTermFactIdFromDelayedMemoryReports, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, - getLongTermMemoryFacts: () => ( - l4Memory && l4Memory.getFacts - ? l4Memory.getFacts() - : [] - ), - deleteLongTermMemoryFact: (factId) => ( - l4Memory && l4Memory.requestFactDelete - ? l4Memory.requestFactDelete(factId) - : false - ), + getLongTermMemoryFacts: getVisibleLongTermMemoryFacts, + deleteLongTermMemoryFact: deleteLongTermMemoryFactAndRender, getDisplayMode: () => runtimeMemoryDisplayMode, setDisplayMode: (value) => { runtimeMemoryDisplayMode = value; @@ -603,6 +597,19 @@ function refreshRuntimeAvatar() { } } +function getVisibleLongTermMemoryFacts() { + if ( + l4Memory + && typeof l4Memory.getVisibleFacts === "function" + ) { + return l4Memory.getVisibleFacts(); + } + + return l4Memory && typeof l4Memory.getFacts === "function" + ? l4Memory.getFacts() + : []; +} + function setDelayedMemoryPinnedOnAvatar( reportId, pinned @@ -896,6 +903,193 @@ function setDelayedMemoryReportPinned( } +function setDelayedMemoryReportAnchorFactIds( + reportId, + anchorFactIds +) { + + const normalizedId = + String(reportId || "").trim().toLowerCase(); + const reports = + readDelayedMemoryReports(); + const report = + reports[normalizedId]; + + if ( + !/^[a-z0-9]{6}$/.test(normalizedId) + || !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return false; + } + + reports[normalizedId] = { + ...report, + anchor_fact_ids: + Array.isArray(anchorFactIds) + ? anchorFactIds + : [], + }; + + writeDelayedMemoryReports( + reports + ); + + const updatedReports = + readDelayedMemoryReports(); + const updatedReport = + updatedReports[normalizedId]; + + renderRuntimeMemorySnapshot(); + refreshRuntimeAvatar(); + + if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { + window.syncDelayedMemoryReportsToRuntime(); + } + + return updatedReport + ? { + ...updatedReport, + _storage_key: normalizedId, + } + : false; + +} + + +function normalizeLongTermFactId( + value +) { + + const match = + String(value || "").trim().match(/^F([1-9]\d*)$/i); + + return match + ? `F${Number(match[1])}` + : ""; + +} + + +function withoutLongTermFactId( + values, + factId +) { + + const source = + Array.isArray(values) + ? values + : []; + + return source.filter((value) => ( + normalizeLongTermFactId(value) !== factId + )); + +} + + +function removeLongTermFactIdFromDelayedMemoryReports( + factId +) { + + const normalizedFactId = + normalizeLongTermFactId(factId); + + if (!normalizedFactId) { + return false; + } + + const reports = + readDelayedMemoryReports(); + let changed = false; + + Object.entries(reports).forEach(([reportId, report]) => { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return; + } + + const nextAnchorFactIds = + withoutLongTermFactId( + report.anchor_fact_ids, + normalizedFactId + ); + const nextFactIds = + withoutLongTermFactId( + report.facts_ids, + normalizedFactId + ); + + if ( + nextAnchorFactIds.length === (report.anchor_fact_ids || []).length + && nextFactIds.length === (report.facts_ids || []).length + && !report.absorbed_fact_ids + && !report.long_term_facts_ids + ) { + return; + } + + const updatedReport = { + ...report, + anchor_fact_ids: nextAnchorFactIds, + facts_ids: nextFactIds, + }; + updatedReport.pop( + "absorbed_fact_ids" + ); + updatedReport.pop( + "long_term_facts_ids" + ); + reports[reportId] = updatedReport; + changed = true; + }); + + if (!changed) { + return false; + } + + writeDelayedMemoryReports( + reports + ); + + if (runtimeMemoryDisplayMode === "delayed") { + renderRuntimeMemorySnapshot(); + } + refreshRuntimeAvatar(); + + if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { + window.syncDelayedMemoryReportsToRuntime(); + } + + return true; + +} + + +function deleteLongTermMemoryFactAndRender( + factId +) { + + const deleted = + l4Memory && l4Memory.requestFactDelete + ? l4Memory.requestFactDelete(factId) + : false; + + if (deleted !== false) { + removeLongTermFactIdFromDelayedMemoryReports( + factId + ); + } + + return deleted; + +} + + function appendDelayedMemoryReports( reports ) { @@ -955,6 +1149,8 @@ function buildDelayedMemoryAvatarLayoutSignature( reportId, report.title, report.summary, + (report.anchor_fact_ids || []).join(","), + (report.facts_ids || []).join(","), ]; } ) @@ -1250,17 +1446,14 @@ window.JinRuntime.runtime = { removeActiveMemoryRecordById: removeActiveMemoryRecordByIdAndRender, getDelayedMemoryReports: readDelayedMemoryReports, setDelayedMemoryReportPinned, + setDelayedMemoryReportAnchorFactIds, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, getLongTermMemoryFacts() { - return l4Memory && l4Memory.getFacts - ? l4Memory.getFacts() - : []; + return getVisibleLongTermMemoryFacts(); }, deleteLongTermMemoryFact(factId) { - return l4Memory && l4Memory.requestFactDelete - ? l4Memory.requestFactDelete(factId) - : false; + return deleteLongTermMemoryFactAndRender(factId); }, replaceDelayedMemoryReports: replaceDelayedMemoryReportsAndRender, appendDelayedMemoryReports, diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index 40b472b1..53e32ed0 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -109,11 +109,43 @@ function normalizeDelayedMemoryFactIds( const source = Array.isArray(value) ? value - : String(value || "").split(/[;,\s]+/); + : [value]; const seen = new Set(); + const candidates = []; - return source - .map(item => String(item || "").trim().toUpperCase()) + source.forEach(function (item) { + if (Array.isArray(item)) { + normalizeDelayedMemoryFactIds(item).forEach( + factId => candidates.push(factId) + ); + return; + } + + const text = + String(item || "").trim(); + + if (text.startsWith("[") && text.endsWith("]")) { + try { + const parsed = JSON.parse(text); + + if (Array.isArray(parsed)) { + normalizeDelayedMemoryFactIds(parsed).forEach( + factId => candidates.push(factId) + ); + return; + } + } catch (_error) { + // Fall through to token parsing. + } + } + + text.split(/[;,\s]+/).forEach( + candidate => candidates.push(candidate) + ); + }); + + return candidates + .map(item => String(item || "").trim().replace(/^["'\[]+|["'\]]+$/g, "").toUpperCase()) .filter( factId => { if (!/^F[1-9]\d*$/.test(factId) || seen.has(factId)) { @@ -140,7 +172,7 @@ function parseDelayedMemoryReportPayload( } const fieldPattern = - /^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|absorbed_fact_ids|long_term_facts_ids)[^\S\r\n]*:[^\S\r\n]*(.*)$/gim; + /^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|facts_ids|absorbed_fact_ids|long_term_facts_ids)[^\S\r\n]*:[^\S\r\n]*(.*)$/gim; const matches = []; let match = fieldPattern.exec(text); @@ -186,6 +218,7 @@ function parseDelayedMemoryReportPayload( .trim() : [ "anchor_fact_ids", + "facts_ids", "absorbed_fact_ids", "long_term_facts_ids", ].includes(field.name) @@ -219,13 +252,13 @@ function parseDelayedMemoryReportPayload( normalizeDelayedMemoryFactIds( fields.anchor_fact_ids ); - const anchorFactIdSet = - new Set(anchorFactIds); - const absorbedFactIds = + const factsIds = normalizeDelayedMemoryFactIds([ + ...anchorFactIds, + ...normalizeDelayedMemoryFactIds(fields.facts_ids), ...normalizeDelayedMemoryFactIds(fields.absorbed_fact_ids), ...normalizeDelayedMemoryFactIds(fields.long_term_facts_ids), - ]).filter(factId => !anchorFactIdSet.has(factId)); + ]); return { [key]: { @@ -240,7 +273,7 @@ function parseDelayedMemoryReportPayload( body: String(fields.body || "").trim(), anchor_fact_ids: anchorFactIds, - absorbed_fact_ids: absorbedFactIds, + facts_ids: factsIds, created_session_id: String(window.jinRuntimeSessionId || websocketClientId || "").trim(), created_time: diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index dd0844f7..d5418fc2 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -52,12 +52,12 @@ function handleL4MemoryUpdate( } -function handleL4MemoryRestoreResult( +function handleSocketL4MemoryRestoreResult( data ) { - if (typeof window.handleL4MemoryRestoreResult === "function") { - window.handleL4MemoryRestoreResult( + if (typeof window.handleL4LoggerMemoryRestoreResult === "function") { + window.handleL4LoggerMemoryRestoreResult( data ); } @@ -261,7 +261,7 @@ registerSocketMessageHandler( registerSocketMessageHandler( "l4_memory_restore_result", - handleL4MemoryRestoreResult + handleSocketL4MemoryRestoreResult ); [ diff --git a/ui/static/js/think-citations.js b/ui/static/js/think-citations.js index 460f47b2..5f100484 100644 --- a/ui/static/js/think-citations.js +++ b/ui/static/js/think-citations.js @@ -577,13 +577,18 @@ if ( !l4Memory - || typeof l4Memory.getFacts !== "function" + || ( + typeof l4Memory.getFacts !== "function" + && typeof l4Memory.getVisibleFacts !== "function" + ) ) { return []; } const facts = - l4Memory.getFacts(); + typeof l4Memory.getVisibleFacts === "function" + ? l4Memory.getVisibleFacts() + : l4Memory.getFacts(); if (!Array.isArray(facts)) { return []; diff --git a/ui/templates/index.html b/ui/templates/index.html index 9d798c6f..7353dbbb 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,10 +14,10 @@ - + - + @@ -499,16 +499,16 @@ } | tojson }} - + - + - - - + + + @@ -516,17 +516,17 @@ - - + + - + - + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index 724577af..b756d9b1 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -55,6 +55,7 @@ from .save_delayed_memory_utils import ( collect_anchor_fact_report_ids, collect_long_term_fact_ids_from_reports, + normalize_delayed_memory_fact_ids, normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, parse_delayed_memory_content_payload, @@ -100,6 +101,7 @@ "normalize_runtime_action_names", "collect_anchor_fact_report_ids", "collect_long_term_fact_ids_from_reports", + "normalize_delayed_memory_fact_ids", "normalize_delayed_memory_fact_roles", "normalize_long_term_fact_ids", "parse_delayed_memory_content_payload", diff --git a/utils/actions/active_memory_actions.py b/utils/actions/active_memory_actions.py index 4071f285..fb29e1ae 100644 --- a/utils/actions/active_memory_actions.py +++ b/utils/actions/active_memory_actions.py @@ -5,6 +5,9 @@ get_runtime_action_display_name, runtime_action_has_close_tag, ) +from .active_memory_utils import ( + collect_active_memory_slot_ids, +) from utils.tool_results import ( TOOL_RESULT_KIND_ACTIVE_MEMORY, record_runtime_tool_result, @@ -67,6 +70,7 @@ async def apply_save_active_memory_actions( *, log_runtime, with_action_context, + action_display_ids=None, ): from utils.brain_client_utils import ( save_active_memory_runtime_record, @@ -75,6 +79,14 @@ async def apply_save_active_memory_actions( saved_active_memory_texts = [] save_active_memory_results = [] + resolved_action_display_ids = ( + action_display_ids + if isinstance( + action_display_ids, + dict, + ) + else {} + ) if not save_active_memory_actions: return saved_active_memory_texts @@ -84,16 +96,24 @@ async def apply_save_active_memory_actions( "[RUNTIME ACTION] save_active_memory requested" ) - for active_memory_text in ( - normalize_active_memory_runtime_payload( + for action in save_active_memory_actions: + if not action.payload: + continue + + active_memory_text = normalize_active_memory_runtime_payload( action.payload ) - for action in save_active_memory_actions - if action.payload - ): if not active_memory_text: continue + action_display_id = str( + resolved_action_display_ids.get( + id(action), + "", + ) + or "" + ).strip() + records_before = list( getattr( context, @@ -129,6 +149,7 @@ async def apply_save_active_memory_actions( ( active_memory_text, active_memory_line, + action_display_id, ) ) @@ -177,10 +198,22 @@ async def apply_save_active_memory_actions( ) if emit is not None: - for active_memory_text, active_memory_line in save_active_memory_results: + for ( + active_memory_text, + active_memory_line, + action_display_id, + ) in save_active_memory_results: display_name = get_runtime_action_display_name( RUNTIME_ACTION_SAVE_ACTIVE_MEMORY ) + active_memory_ids = collect_active_memory_slot_ids( + active_memory_line + ) + active_memory_id = ( + sorted(active_memory_ids)[0] + if active_memory_ids + else "" + ) event = { "type": "runtime_action", "action": "save_active_memory", @@ -195,13 +228,19 @@ async def apply_save_active_memory_actions( ), } + if action_display_id: + event["id"] = action_display_id + + if active_memory_id: + event["active_memory_id"] = active_memory_id + if active_memory_line: event["active_memory"] = active_memory_line await emit(with_action_context( event )) - await emit(with_action_context({ + completed_event = { "type": "runtime_action", "action": "save_active_memory", "status": "completed", @@ -209,7 +248,17 @@ async def apply_save_active_memory_actions( "close_tag": runtime_action_has_close_tag( RUNTIME_ACTION_SAVE_ACTIVE_MEMORY ), - })) + } + + if action_display_id: + completed_event["id"] = action_display_id + + if active_memory_id: + completed_event["active_memory_id"] = active_memory_id + + await emit(with_action_context( + completed_event + )) return saved_active_memory_texts diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index cf30c64c..45081d19 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -989,6 +989,27 @@ def accept_runtime_action_once_per_message( ) or "" ).strip() + if ( + not action_display_id + and action.name == RUNTIME_ACTION_SAVE_ACTIVE_MEMORY + ): + active_memory_action_sequence = int( + getattr( + context, + "runtime_active_memory_action_sequence", + 0, + ) + or 0 + ) + 1 + context.runtime_active_memory_action_sequence = ( + active_memory_action_sequence + ) + action_display_id = build_runtime_action_id( + RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, + active_memory_action_sequence, + ) + action_display_ids[id(action)] = action_display_id + if action_display_id: action_event["id"] = action_display_id runtime_turn_id = str( @@ -1552,6 +1573,7 @@ def accept_runtime_action_once_per_message( save_active_memory_actions, log_runtime=log_runtime, with_action_context=with_action_context, + action_display_ids=action_display_ids, ) saved_delayed_memory_reports = await apply_save_delayed_memory_actions( diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 0ab78afb..a2072ad1 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -8,7 +8,7 @@ DELAYED_MEMORY_FIELD_RE = re.compile( r"(?im)^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|" - r"absorbed_fact_ids|long_term_facts_ids)" + r"facts_ids|absorbed_fact_ids|long_term_facts_ids)" r"[^\S\r\n]*:[^\S\r\n]*(.*)$", ) @@ -19,10 +19,30 @@ def normalize_long_term_fact_ids(value) -> list[str]: candidates = [] for item in source: + if isinstance(item, list): + candidates.extend( + normalize_long_term_fact_ids(item) + ) + continue + + text = str(item or "").strip() + + if text.startswith("[") and text.endswith("]"): + try: + parsed = json.loads(text) + except json.JSONDecodeError: + parsed = None + + if isinstance(parsed, list): + candidates.extend( + normalize_long_term_fact_ids(parsed) + ) + continue + candidates.extend( re.split( r"[,;\s]+", - str(item or ""), + text, ) ) @@ -33,7 +53,9 @@ def normalize_long_term_fact_ids(value) -> list[str]: fact_id = str( candidate or "" - ).strip().upper() + ).strip().strip( + "\"'[]" + ).upper() if ( not fact_id @@ -50,33 +72,56 @@ def normalize_long_term_fact_ids(value) -> list[str]: return fact_ids -def normalize_delayed_memory_fact_roles( +def normalize_delayed_memory_fact_ids( anchor_fact_ids=None, - absorbed_fact_ids=None, + facts_ids=None, + legacy_absorbed_fact_ids=None, legacy_long_term_fact_ids=None, ) -> tuple[list[str], list[str]]: """Normalize delayed-memory L4 references. - Legacy ``long_term_facts_ids`` are treated as absorbed references. A fact - can only have one role inside a report; anchor wins when an id appears in - both lists so an important fact can never be hidden by the same report. + ``facts_ids`` is the full set of L4 facts represented by the report. + ``anchor_fact_ids`` is a visible, important subset and is always folded + into ``facts_ids``. Legacy absorbed/long-term fields are treated as + non-anchor report facts for compatibility with older reports. """ anchor_ids = normalize_long_term_fact_ids( anchor_fact_ids or [] ) - absorbed_ids = normalize_long_term_fact_ids([ + fact_ids = normalize_long_term_fact_ids([ + *anchor_ids, + *normalize_long_term_fact_ids( + facts_ids or [] + ), *normalize_long_term_fact_ids( - absorbed_fact_ids or [] + legacy_absorbed_fact_ids or [] ), *normalize_long_term_fact_ids( legacy_long_term_fact_ids or [] ), ]) + + return anchor_ids, fact_ids + + +def normalize_delayed_memory_fact_roles( + anchor_fact_ids=None, + absorbed_fact_ids=None, + legacy_long_term_fact_ids=None, +) -> tuple[list[str], list[str]]: + """Compatibility wrapper returning legacy non-anchor absorbed facts.""" + + anchor_ids, fact_ids = normalize_delayed_memory_fact_ids( + anchor_fact_ids, + facts_ids=None, + legacy_absorbed_fact_ids=absorbed_fact_ids, + legacy_long_term_fact_ids=legacy_long_term_fact_ids, + ) anchor_set = set(anchor_ids) absorbed_ids = [ fact_id - for fact_id in absorbed_ids + for fact_id in fact_ids if fact_id not in anchor_set ] @@ -86,7 +131,7 @@ def normalize_delayed_memory_fact_roles( def collect_long_term_fact_ids_from_reports( reports, ) -> set[str]: - """Compatibility name: return only facts absorbed by delayed memory.""" + """Compatibility name: return facts represented by delayed memory.""" if not isinstance(reports, dict): return set() @@ -97,12 +142,13 @@ def collect_long_term_fact_ids_from_reports( if not isinstance(report, dict): continue - _anchor_ids, absorbed_ids = normalize_delayed_memory_fact_roles( + _anchor_ids, report_fact_ids = normalize_delayed_memory_fact_ids( report.get("anchor_fact_ids", []), - report.get("absorbed_fact_ids", []), - report.get("long_term_facts_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) - fact_ids.update(absorbed_ids) + fact_ids.update(report_fact_ids) return fact_ids @@ -125,10 +171,11 @@ def collect_anchor_fact_report_ids( if not normalized_report_id: continue - anchor_ids, _absorbed_ids = normalize_delayed_memory_fact_roles( + anchor_ids, _fact_ids = normalize_delayed_memory_fact_ids( report.get("anchor_fact_ids", []), - report.get("absorbed_fact_ids", []), - report.get("long_term_facts_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) for fact_id in anchor_ids: @@ -202,6 +249,7 @@ def parse_delayed_memory_content_payload( ).strip() elif field_name in { "anchor_fact_ids", + "facts_ids", "absorbed_fact_ids", "long_term_facts_ids", }: @@ -243,10 +291,11 @@ def parse_delayed_memory_content_payload( if tag.strip() ] - anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + anchor_fact_ids, fact_ids = normalize_delayed_memory_fact_ids( fields.get("anchor_fact_ids", []), - fields.get("absorbed_fact_ids", []), - fields.get("long_term_facts_ids", []), + fields.get("facts_ids", []), + legacy_absorbed_fact_ids=fields.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=fields.get("long_term_facts_ids", []), ) report_id = generate_delayed_memory_report_id( @@ -273,7 +322,7 @@ def parse_delayed_memory_content_payload( ).strip(), "pinned": False, "anchor_fact_ids": anchor_fact_ids, - "absorbed_fact_ids": absorbed_fact_ids, + "facts_ids": fact_ids, "created_session_id": str( created_session_id or "" diff --git a/utils/actions/update_delayed_memory_utils.py b/utils/actions/update_delayed_memory_utils.py index e9c03280..f6bf8dcc 100644 --- a/utils/actions/update_delayed_memory_utils.py +++ b/utils/actions/update_delayed_memory_utils.py @@ -4,7 +4,7 @@ from .delayed_memory_utils import is_delayed_memory_report_id from .save_delayed_memory_utils import ( - normalize_delayed_memory_fact_roles, + normalize_delayed_memory_fact_ids, normalize_long_term_fact_ids, ) @@ -13,6 +13,7 @@ "tags", "body", "anchor_fact_ids", + "facts_ids", "absorbed_fact_ids", }) @@ -80,21 +81,23 @@ def parse_update_delayed_memory_payload(payload: str) -> dict: if "anchor_fact_ids" in raw_update else [] ) - requested_absorbed_ids = ( - normalize_long_term_fact_ids(raw_update.get("absorbed_fact_ids", [])) - if "absorbed_fact_ids" in raw_update - else [] - ) - anchor_ids, absorbed_ids = normalize_delayed_memory_fact_roles( + requested_facts_ids = normalize_long_term_fact_ids([ + *normalize_long_term_fact_ids(raw_update.get("facts_ids", [])), + *normalize_long_term_fact_ids(raw_update.get("absorbed_fact_ids", [])), + ]) + anchor_ids, facts_ids = normalize_delayed_memory_fact_ids( requested_anchor_ids, - requested_absorbed_ids, + requested_facts_ids, ) if "anchor_fact_ids" in raw_update and anchor_ids: update["anchor_fact_ids"] = anchor_ids - if "absorbed_fact_ids" in raw_update and absorbed_ids: - update["absorbed_fact_ids"] = absorbed_ids + if ( + ("facts_ids" in raw_update or "absorbed_fact_ids" in raw_update) + and facts_ids + ): + update["facts_ids"] = facts_ids if len(update) == 1: return {} diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 872b9916..e14844bd 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -113,7 +113,7 @@ def get_brain_runtime_config(): parse_delayed_memory_content_payload, parse_idle_seconds, normalize_jin_color_payload, - normalize_delayed_memory_fact_roles, + normalize_delayed_memory_fact_ids, normalize_long_term_fact_ids, refresh_active_memory_runtime_metadata, strip_active_memory_runtime_metadata, @@ -331,11 +331,12 @@ def build_delayed_memory_report( report_id ) - requested_anchor_fact_ids, requested_absorbed_fact_ids = ( - normalize_delayed_memory_fact_roles( + requested_anchor_fact_ids, requested_facts_ids = ( + normalize_delayed_memory_fact_ids( value.get("anchor_fact_ids", []), - value.get("absorbed_fact_ids", []), - value.get("long_term_facts_ids", []), + value.get("facts_ids", []), + legacy_absorbed_fact_ids=value.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=value.get("long_term_facts_ids", []), ) ) available_l4_fact_ids = get_runtime_l4_fact_ids( @@ -346,20 +347,20 @@ def build_delayed_memory_report( for fact_id in requested_anchor_fact_ids if fact_id in available_l4_fact_ids ] - absorbed_fact_ids = [ + facts_ids = [ fact_id - for fact_id in requested_absorbed_fact_ids + for fact_id in requested_facts_ids if fact_id in available_l4_fact_ids ] - anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + anchor_fact_ids, facts_ids = normalize_delayed_memory_fact_ids( anchor_fact_ids, - absorbed_fact_ids, + facts_ids, ) enriched_report[report_id] = { **value, "anchor_fact_ids": anchor_fact_ids, - "absorbed_fact_ids": absorbed_fact_ids, + "facts_ids": facts_ids, "pinned": bool(value.get("pinned", False)), "created_session_id": ( str( @@ -434,6 +435,7 @@ def build_delayed_memory_report( ) ), } + enriched_report[report_id].pop("absorbed_fact_ids", None) enriched_report[report_id].pop("long_term_facts_ids", None) return enriched_report @@ -2061,36 +2063,41 @@ def update_delayed_memory_report( requested_anchor_fact_ids = normalize_long_term_fact_ids( update.get("anchor_fact_ids", []) ) - requested_absorbed_fact_ids = normalize_long_term_fact_ids( - update.get("absorbed_fact_ids", []) + requested_facts_ids = normalize_long_term_fact_ids( + [ + *normalize_long_term_fact_ids(update.get("facts_ids", [])), + *normalize_long_term_fact_ids(update.get("absorbed_fact_ids", [])), + ] ) accepted_anchor_fact_ids = [ fact_id for fact_id in requested_anchor_fact_ids if fact_id in available_l4_fact_ids ] - accepted_absorbed_fact_ids = [ + accepted_facts_ids = [ fact_id - for fact_id in requested_absorbed_fact_ids + for fact_id in requested_facts_ids if fact_id in available_l4_fact_ids ] - current_anchor_fact_ids, current_absorbed_fact_ids = ( - normalize_delayed_memory_fact_roles( + current_anchor_fact_ids, current_facts_ids = ( + normalize_delayed_memory_fact_ids( report.get("anchor_fact_ids", []), - report.get("absorbed_fact_ids", []), - report.get("long_term_facts_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) ) - next_anchor_fact_ids, next_absorbed_fact_ids = ( - normalize_delayed_memory_fact_roles( + next_anchor_fact_ids, next_facts_ids = ( + normalize_delayed_memory_fact_ids( [ *current_anchor_fact_ids, *accepted_anchor_fact_ids, ], [ - *current_absorbed_fact_ids, - *accepted_absorbed_fact_ids, + *current_facts_ids, + *accepted_facts_ids, + *accepted_anchor_fact_ids, ], ) ) @@ -2113,7 +2120,7 @@ def update_delayed_memory_report( if ( next_tags == list(report.get("tags", []) or []) and next_anchor_fact_ids == current_anchor_fact_ids - and next_absorbed_fact_ids == current_absorbed_fact_ids + and next_facts_ids == current_facts_ids and next_body == current_body ): return build_delayed_memory_failure_result( @@ -2126,9 +2133,10 @@ def update_delayed_memory_report( **report, "tags": next_tags, "anchor_fact_ids": next_anchor_fact_ids, - "absorbed_fact_ids": next_absorbed_fact_ids, + "facts_ids": next_facts_ids, "body": next_body, } + updated_report.pop("absorbed_fact_ids", None) updated_report.pop("long_term_facts_ids", None) reports[report_id] = updated_report @@ -2968,4 +2976,3 @@ def has_zero_diff_stall_alert( ) ) - diff --git a/utils/context/context_exports.py b/utils/context/context_exports.py index 9839d882..c326f735 100644 --- a/utils/context/context_exports.py +++ b/utils/context/context_exports.py @@ -20,6 +20,7 @@ from .session_actions import ( _is_current_sequence_action, _normalize_session_action_history_item, + build_current_runtime_context, build_session_actions_history_context, format_session_action_age, strip_actions_history_context, @@ -52,6 +53,7 @@ "build_previous_chat_messages_context", "build_previous_chat_messages_context_text", "build_runtime_xml", + "build_current_runtime_context", "build_session_actions_history_context", "build_tool_results_context", "crop_recent_message_text", diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index d6a9d893..65bc6229 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -464,6 +464,39 @@ def build_session_actions_history_context( ) +def build_current_runtime_context( + *, + user_message: str = "", + sequence_started_at=None, +) -> str: + + message_text = str( + user_message + or "" + ).strip() + + if not message_text: + return "" + + elapsed_suffix = "" + + if isinstance( + sequence_started_at, + (int, float), + ) and sequence_started_at > 0: + elapsed_suffix = ( + " ( " + f"{format_session_action_age(time.time() - float(sequence_started_at))}" + " ago )" + ) + + return ( + f"\n" + f"user_message: {escape(message_text)}\n" + "" + ) + + def strip_actions_history_context( system_prompt: str, ) -> str: @@ -475,6 +508,7 @@ def strip_actions_history_context( for tag_name in ( "SESSION_ACTIONS_HISTORY", + "CURRENT_RUNTIME", "CURRENT_SEQUENCE", "CURRENT_ACTIONS_HISTORY", "SEQUENCE_ORIGIN_REQUEST", diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index e4e1732d..85f647c3 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -10,7 +10,7 @@ is_delayed_memory_report_id, ) from utils.actions.save_delayed_memory_utils import ( - normalize_delayed_memory_fact_roles, + normalize_delayed_memory_fact_ids, ) @@ -138,10 +138,11 @@ def normalize_delayed_memory_report( or created_time, limit=MAX_DELAYED_MEMORY_TIME_CHARS, ) - anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + anchor_fact_ids, facts_ids = normalize_delayed_memory_fact_ids( report.get("anchor_fact_ids", []), - report.get("absorbed_fact_ids", []), - report.get("long_term_facts_ids", []), + report.get("facts_ids", []), + legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), + legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) return normalized_id, { @@ -159,7 +160,7 @@ def normalize_delayed_memory_report( ), "pinned": bool(report.get("pinned", False)), "anchor_fact_ids": anchor_fact_ids, - "absorbed_fact_ids": absorbed_fact_ids, + "facts_ids": facts_ids, "created_session_id": _clean_text( report.get("created_session_id", "") or report.get("session", ""), @@ -250,18 +251,23 @@ def merge_delayed_memory_reports( ).intersection( primary_reports ): - anchor_fact_ids, absorbed_fact_ids = normalize_delayed_memory_fact_roles( + anchor_fact_ids, facts_ids = normalize_delayed_memory_fact_ids( [ *fallback_reports[report_id].get("anchor_fact_ids", []), *primary_reports[report_id].get("anchor_fact_ids", []), ], [ + *fallback_reports[report_id].get("facts_ids", []), + *primary_reports[report_id].get("facts_ids", []), + ], + legacy_absorbed_fact_ids=[ *fallback_reports[report_id].get("absorbed_fact_ids", []), *primary_reports[report_id].get("absorbed_fact_ids", []), ], ) reports[report_id]["anchor_fact_ids"] = anchor_fact_ids - reports[report_id]["absorbed_fact_ids"] = absorbed_fact_ids + reports[report_id]["facts_ids"] = facts_ids + reports[report_id].pop("absorbed_fact_ids", None) reports[report_id].pop("long_term_facts_ids", None) return reports @@ -325,8 +331,8 @@ def build_delayed_memory_file_payload( "anchor_fact_ids": clean_report[ "anchor_fact_ids" ], - "absorbed_fact_ids": clean_report[ - "absorbed_fact_ids" + "facts_ids": clean_report[ + "facts_ids" ], "appended_times": clean_report["appended_times"], "append_streak": clean_report["append_streak"], diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index 843c2a8e..ddead0fa 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1086,6 +1086,10 @@ def _build_session_action_marker_detail( "UPDATE_DELAYED_MEMORY", } +SEPARATE_SESSION_ACTION_MARKER_ITEMS = { + "SAVE_ACTIVE_MEMORY", +} + def _unique_session_action_values( values, @@ -1587,6 +1591,107 @@ def format_session_action_marker_names( ) +def _build_session_action_marker_history_items( + formatted_marker_parts, + *, + created_at, + runtime_turn_id: str = "", + jin_message_content: str = "", +) -> list[dict]: + + normalized_parts = _normalize_session_action_display_parts( + formatted_marker_parts + ) + items = [] + grouped_parts = [] + + def append_item( + parts, + *, + preserve_separate: bool = False, + ) -> None: + + normalized_item_parts = ( + _normalize_session_action_display_parts( + parts + ) + ) + text = ", ".join( + formatted_part + for formatted_part in ( + _format_session_action_display_part( + part + ) + for part in normalized_item_parts + ) + if formatted_part + ) + + if not text: + return + + item = { + "text": text, + "created_at": created_at, + "parts": normalized_item_parts, + "runtime_session_action_marker_item": True, + } + + if preserve_separate: + item["runtime_session_action_preserve_separate"] = True + + if runtime_turn_id: + item["runtime_turn_id"] = runtime_turn_id + + normalized_jin_message_content = _normalize_jin_message_content( + jin_message_content + ) + + if normalized_jin_message_content: + item["jin_message_content"] = normalized_jin_message_content + + items.append( + item + ) + + def flush_grouped_parts() -> None: + + if not grouped_parts: + return + + append_item( + grouped_parts + ) + grouped_parts.clear() + + for part in normalized_parts: + action_name = str( + part.get( + "text", + "", + ) + or "" + ).strip().upper() + + if action_name in SEPARATE_SESSION_ACTION_MARKER_ITEMS: + flush_grouped_parts() + append_item( + [ + part, + ], + preserve_separate=True, + ) + continue + + grouped_parts.append( + part + ) + + flush_grouped_parts() + + return items + + def _normalize_jin_message_content( value, ) -> str: @@ -1725,10 +1830,20 @@ def replace_session_action_history_since( del history[safe_start_index:] - record_session_action_history( - context, - formatted_marker_names, - display_parts=formatted_marker_parts, + runtime_turn_id = get_current_action_sequence_turn_id( + context + ) + marker_items = _build_session_action_marker_history_items( + formatted_marker_parts, + created_at=time.time(), + runtime_turn_id=runtime_turn_id, + ) + + if not marker_items: + return + + history.extend( + marker_items ) @@ -1821,14 +1936,6 @@ def upsert_session_action_marker_history_since( dict, ) else time.time() - item = { - "text": formatted_marker_names, - "created_at": created_at, - "parts": _normalize_session_action_display_parts( - formatted_marker_parts - ), - "runtime_session_action_marker_item": True, - } previous_jin_message_content = _normalize_jin_message_content( previous_item.get( "jin_message_content", @@ -1841,22 +1948,52 @@ def upsert_session_action_marker_history_since( else "" ) - if previous_jin_message_content: - item["jin_message_content"] = previous_jin_message_content - runtime_turn_id = get_current_action_sequence_turn_id( context ) + marker_items = _build_session_action_marker_history_items( + formatted_marker_parts, + created_at=created_at, + runtime_turn_id=runtime_turn_id, + jin_message_content=previous_jin_message_content, + ) - if runtime_turn_id: - item["runtime_turn_id"] = runtime_turn_id + if not marker_items: + return False if marker_index is None: - history.append( - item + history.extend( + marker_items ) else: - history[marker_index] = item + marker_indexes = [ + index + for index in range( + safe_start_index, + len(history), + ) + if isinstance( + history[index], + dict, + ) + and history[index].get( + "runtime_session_action_marker_item" + ) is True + ] + insert_index = marker_indexes[0] + + for index in reversed( + marker_indexes + ): + del history[index] + + for item in reversed( + marker_items + ): + history.insert( + insert_index, + item, + ) if len(history) > MAX_SESSION_ACTION_HISTORY_ITEMS: del history[:-MAX_SESSION_ACTION_HISTORY_ITEMS] diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index b885ce28..032a7181 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -131,10 +131,10 @@ def apply_delayed_memory_reports( ) ) - context.delayed_memory_reports = merge_delayed_memory_reports( - incoming_reports, - existing_reports, - ) + context.delayed_memory_reports = { + **existing_reports, + **incoming_reports, + } from runtime.L4_memory import ( refresh_runtime_l4_archived_fact_ids, From b5a14a1ba7a8ce29f11de75bde12e29a0c8cfc61 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:48:41 +0300 Subject: [PATCH 025/168] chore: live avatar - update live avatar refresh behavior - cleanup default background image --- ...ayed_memory_pinned_glow_client_contract.py | 2 +- tests/test_l4_logger_cards_client_contract.py | 2 +- ..._memory_citation_layers_client_contract.py | 2 +- ...t_memory_reference_sync_client_contract.py | 2 +- ..._runtime_avatar_l4_ring_client_contract.py | 42 +- ui/static/images/states/default.jpg | Bin 276894 -> 277898 bytes ui/static/js/runtime/runtime-avatar.js | 391 ++++++++++++++++++ ui/static/js/runtime/runtime-l4-memory.js | 33 +- ui/static/js/runtime/runtime.js | 149 +++---- ui/templates/index.html | 6 +- 10 files changed, 526 insertions(+), 103 deletions(-) diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index f62c2e18..b8c83163 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -48,7 +48,7 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) - self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-20", source) + self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-22", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index 6372d251..c1567c63 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -77,7 +77,7 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn('/static/css/runtime-memory.css?v=memory-hover-sync-9', source) - self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-delete-local-1', source) + self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-delete-local-2', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) self.assertIn('/static/js/logger/log-entries.js?v=l4-restore-meta-1', source) diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index f0d59ac5..bb2e81a5 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -114,7 +114,7 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-15", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-17", source) self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) self.assertIn("/static/js/chat.js?v=memory-reference-sync-2", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 769e89a4..fee565f4 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -376,7 +376,7 @@ def test_cache_versions_are_bumped(self): source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=runtime-facade-20', + '/static/js/runtime/runtime.js?v=runtime-facade-22', source, ) self.assertIn( diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 0318b5c1..57652b4e 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -7,6 +7,7 @@ AVATAR_JS = ( ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" ) +RUNTIME_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" AVATAR_CSS = ROOT / "ui" / "static" / "css" / "runtime-avatar.css" INDEX_HTML = ROOT / "ui" / "templates" / "index.html" @@ -49,6 +50,17 @@ def test_l4_facts_are_rendered_as_reference_aware_memory_dashes(self): source, ) self.assertIn('"data-l4-fact-ids":', source) + self.assertIn('"data-avatar-memory-angle": options.angle', source) + self.assertIn('class: "jin-avatar-center"', source) + self.assertIn("function syncMemorySignalLayer(kind", source) + self.assertIn("function syncL4MemoryArchiveState()", source) + self.assertIn("function syncDelayedMemoryState()", source) + self.assertIn("function syncActiveMemoryState()", source) + self.assertIn("function syncL4MemoryState()", source) + self.assertIn("setL4MemoryDashArchivedState(", source) + self.assertIn("syncActiveMemoryState,", source) + self.assertIn("syncDelayedMemoryState,", source) + self.assertIn("syncL4MemoryState,", source) self.assertIn("applyDelayedMemoryFactLinkGlow()", source) self.assertIn("is-delayed-memory-linked-hit", source) @@ -72,6 +84,34 @@ def test_l4_ring_is_appended_inside_delayed_and_active_rings(self): self.assertLess(l4_index, delayed_index) self.assertLess(delayed_index, active_index) + def test_memory_ring_changes_can_sync_without_avatar_refresh(self): + source = RUNTIME_JS.read_text(encoding="utf-8") + + self.assertIn( + "function syncDelayedMemoryStateToAvatar()", + source, + ) + self.assertIn( + "function syncActiveMemoryStateToAvatar()", + source, + ) + self.assertIn( + "if (!syncActiveMemoryStateToAvatar()) {\n refreshRuntimeAvatar();", + source, + ) + self.assertIn( + "if (!syncDelayedMemoryStateToAvatar()) {\n refreshRuntimeAvatar();", + source, + ) + self.assertNotIn( + "function buildDelayedMemoryAvatarLayoutSignature(", + source, + ) + self.assertNotIn( + "function syncDelayedMemoryPinsToAvatar(", + source, + ) + def test_open_delayed_report_keeps_avatar_fact_links_highlighted(self): source = AVATAR_JS.read_text(encoding="utf-8") @@ -108,7 +148,7 @@ def test_avatar_cache_versions_are_bumped(self): source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-15", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-17", source, ) diff --git a/ui/static/images/states/default.jpg b/ui/static/images/states/default.jpg index c0b22bdd6ffefb2abc5667e30e743bb789d2065c..5cd10f723534cd018f9cc4d63ddf2664417def98 100644 GIT binary patch delta 258873 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

      &%^va)1yzo(jI!iYi*(ueNjkk15q{jEx<=vP*< za+UbZxvYQ#x$SbZ%_A6GVhIK_Ph0Y+5vZglhO=Nd%)szyrU7(nU`&Sbdhi8lBpOc4z&iqD*j z^PBKEAa~3Kg2)m zu3rpr`{VV6;uj|vr{A`Hfu9&`ibY@Y?X_Bg94V8j+)d z5(wE5kv{5J`6(Nzti+yyDUMA~$4e7#oUMqYzYLbpJ&f-*#b zVxob6JF9s`cM_3izA2(DljpDsHJp)7!IotX&jK{+SfW=$dEN)=anon~aWUB&6&g<4 zy$Ws3EpKgL<8KelVtK0KOC%sNQE?!}Doa8TaMUO(c*cu8cM5gjUB@G{qwETp$os@X z-vhE)WlFN0zM+h&Z90&t8;n)4^R7yo9;D09q7|9KYqMQ)n6F<8`9riSyUozkPe#mW z+@x7PmTBckfn<{v&YnUZsg1p}Xm{JZV|%H-!CO$TlKWUMWW!c=rlb8zcb0(M+2?wX zZ_MO}p7rRs{E9=?(PY1y_gI}>qh6H>BhqsR*W8v<$3%B zZTvjJeX(If@SC_R006un%r9g&=e=?N*|ku0I#{yeo0&5ofiFHoo|6Dz)uf|4ZZ@gO zQA-NLnZkixI%Hc1mZb5ppm7&Pwz5DI9AIrJ@RWZDZg@u(^yTx^MuZjga)y!c7jj}xA+gVliuLES? zD%R{Qh98n0B`+2mEC3(Re0z+0d%s~Clf3mtF_h#^|{GExNePcU%b`#?X^n;gP zY@~u2qIvFNg4d65^WMswDLu1zET~cCNS|1pftV9HTHXM6)Yj7Gjmc4)c9EPY`{r{x zC|-}K#dPZiQ5Ad3!79omudTf032~}ES)WZ>b5r+fBE>4U5=+f6d3m0iH{tX&4~y|; zErgRWKOgUVA-pfNZK?j|m4r&G#0S{&Ad?e}PzoAXId$(5$>L_3pAI|yk(?|-=`Oc>Ic@UPd1)- z^5w<3wve4|iHF;IC$1;`$@u3s19s_FLG8&&fqe$E2HRadtDg?sG3^4(3=3oZrF(K_ znhSgI-*Fvrt*M#_$?#c|Yih727}J#T7A6aQ%M;azlee@72cEJ*KdV6!VWxtV(tfaCk_ynMSE>Q~)U;4hxH31pAAAPk2Rtk}}7^3BeSdhL9Kjdp`|x<>etVB&nRn^&MUS57p5K{N98B^P?F zU2dJl-!O>azroI`)yYoxm8j=TavTCe_8i<2o27vqduoy4o`)<$l@5wP$>1zlm@1 z9n*7ZD16;GeSoNFyt&TcJZF^epQ?+A%7 zmtT&582C#4Rri1oNDN~#&tNCnzHuai5}bu_ zJU7ql>mLP6$4YTV;^gIxVl?x(o)StGyA8%3#Jde znNar$0`AIv&B%R|g?@&wn{F1@MCyLXuO0cPIAiIHtwcP7V-tzY@;?%N<%)pbb>Ps{ z*?pH^YUb{~ZeWxaT`u3uynno9QX$B78Q|r;;^niHw5a$dWTrV)$Md`VbH)1_p^ky8 zb^4i2r0FuCFo9;d5d;y);wA|p_>3}p{2P+>g{Dlq?1imG9q`i)a%!K0EcK&gKsjU1 z6-gj8I#+|?rt9M~B3{TFOq_k7V$^I6;wMSb>Qxl-d@GW{{bA<~VVfM^{xVTZG}^)8 zz%iVe54%Mtt1xi}M4~^k3pps#K&yJUDedq{jgyDGeEw9sMOMZ8HLi$w#eA^k>_Sbo z4?N6F`OVGT7}fJW0mGdBEg-2?d=f@0smt{~n8EPZR7fCWm3+XNoldW9t2CwGwk5|+ zH#$~wFGYm?L7U&SNlrg_1yYNNMUI&G&W(5B5!=C z!nWh~vBs^(@AQ5|o+&=|>fXt-1D+d46mRN@pQ4U;58mtD`thT9s*~sP%D1K6jqpD= zBAyqQFlKf?jmNqiD(x@jY?!VkT)kWH!m8-5zt2qd?ed7Mcec;1Qra-KiI=9>b}&d3 z$N?`)tF&#Wh>%%>o`pF2d$^gAVyUZ;)t*F2E5U|tA;i@mB=rsZQ!-*zV!1T@DovwX zyEct%nF{q#8I19VjiF;Jk_;7=#^-!2{EZk-$LGr0Mdia4qDO@?D7r$TTY>7+urpQt z)B1D+?|cz!hq{qsMv?#&qXgo2Jd@StRAApZtu1V@p7(1no?6zf? zN=dWYWOziY`6I?G))hDXRP%DPmd*^uBa8&98oy%pj{t~NLDw({{4ZmmBFD<5`c=TC&p?3=uGa%Ly%Y*DM({^IW!jbw!z zX4LtXwjvJb`plhgq!oQgo!96%uquqX!RTpLkq7DxA$Tfx#Lh|y&9Qb+1lu?`iDWvd z-4R3b_LdCNICSoqK6dyHHGRb&MC{c*$noI|7DnU)0|xjb=Yt)F9Y-}TdSi!zFhv5G zC~}zBAXVuG)jwp;Rg(lDno;RACK7m_Hd@_ak?)VFR%tm<;?M9EP$JHo z$~sgkYnA#AY_?@r-1Gf&1q0;iyvG@tidNuq%(G~%xTR#~U1F787k62_mq_u5vVrPp z{-fg>nawq|{Xnuf*lOP1nT8T~dKsu>(@=Jo8phos;JbMgE6JnzzSuu1EcNUR}=J{cR4J_$>ef50_R2 z7cSoB>`Yy=yRx(OQLsBByx|p!pqv%x6s2s07CY*5PO~I94yI<$+d&guDl@WCnaia) zA!_El5?vHoXd^Fu;Zmc$0IqcCz^bwtFIJsw*o(H zsOk<)H^6;)0qoD+gb>@xG`oS>va|#__H_SaImOjBT6qYVh1O)f+=}tfE_*7@F<^7a zwmX^Kp?Y>d{EGZTvo!*h>=+&<`1xte^(=ig2`}kJqs!3MZ6${)I;Y?j&Cy}QD6t;r zl^nh-OnaO=bc}z#Og!o*+HX|*RpDRleuKB~o;EnLqJTRX@uFWObB=H1)aBznH}my$5*8R`r_0O&YyC7@ z4`gNv1nyYHMJj-X12o`WhC&)JO|EpiV!+cR_e)Yt=RDHOG2_}8(eXg?-Nc9y|08&J z&Oww{iB!JqAXprKSVf)}HwNCN0p7W49V-`QVrX1^>?+h~UbXQ4yGXC%p zxz`q3L6IJDk_Y*>-eLGgiP^H4;hIWK@EqxS8BXBkbF_jM-vOUkPPCTEnUvIc1n8km5z7?)u=6;R(ljTH9a|<37cog9sarqj4%V4aiGh z&l-j<1)Md%dm-a?zuvyX#q*I;$3E)bKh=AUq5AVo?%=bkr}-Dz&-eL#|32|?*YWAe z!kq^nFC3rUiP{wvL2XYMObD^o3(v*eRekrusPK|iHQDaVnKjAbyU{3&03UZ%Y0BY% z%BH1XEOu&^H`&snau3@35mpUu{WFD1H4 zYEAK4oA!5Pr9%YF6f)bV&ZKjdjh-c2HYn1@L0Wv-9=6JLVi` zW7}3{ZDL_Ly_{e`mY#qIsIXg8u?MyD#_Olg-Pe&?k)v2|LK&xCfG+^if2nTZUdJgZTm zXVj)hpb&SBf>HP_U{1Eo6PrQvOmuq~W9_Sw*p797eR8Vd)u3&#q^Irtw}`S=dIEXsXtvy z)Rv!5Qr%r!8`U0Pt8xn83krt+bDVMD(yb!K*9RZ=ot)nBxqky>5ial?bHFclw#WI# zb*0keH{}+EK0RU&`R`ph%cm^ptT-J$QU)#HaJjr$#?&TX-d>y)*T!iq293FKIIbcS zNh~g6)jhUKuQ%6~wA`EuAdCJFj?OcX?e}Zrc2!YrQ8lZry-J1JEmb2zjo5n>v1$_| z+8RYsdyBm#w%Tn~t)c|6wN^xok{C(xd-8wsD(~;_eV=o#bDhrtrRrzj-h6aD=b=fX zuf2Arw-|S)CFCX7c7nNLxbnTIQWyubW%Vm@8qQzemLGFHi~4lGgJl`mY;--5q`{hXe?EgPthNV z-D;C@SIdk}g&uWt*=Smo$j=h-J->4q%mD7Rn!r3MS%&s>qxFi7ml%uLAzfHjU8H>; z-OPI1m;1tO()Ll$WX=5i)vrAgflkqdDT7{a#pmkx#(oS$PFci1iim5^S3u{Y;6US! zqzI+%Z4RdeHv0Jr3ptuFNeoFFg&Et9eu|JvD}46@U8Zkr|du_aa zlqOJ2WeIVNZf8;raPIc|?WGfAA6sE09<*V>6D*JKGR-WXpQxv~PlJX930NkAbZ?s` z{oM!g3uX|@sPh6c8``amv!eMyrX%Aj{A{ux82IiMW4DP0xJY@dS}rMH;c$uEfha}f zMiu15(ra*J=?2Jss{u&d(PM93uK*f>Cg9d@L0&?IJ6oVZuF3TMKXcdwVM>8dB8>Db zQOzvktkeR^OtC<_5*V$>cs0bsDI?)g-kH%I0UKcpg3n8v2^G~WOceOXek|G=+AMcc zG{JKLZOg^AY#gqiZV^*F|D*8w+zk^tnfyu^+eb)Vt*Cv-vnt|4CeS5N800tJbs+dh zaNDWZdDppdg#?fCKMk%#VB9~evk!pqZtQ)vSsb9lVOemAdhaXenPriEXWvw*I)ITj??qU~TTd-4nv z^R~h=ZanSvTbN^v=~kM_oR}$8qKUYKQ&ml{Y{D)6d$@6hPT&9C0@tz{TrjGv7`Q`j zp~o|@v!~Bt(`wyj5}zB`h0N8oNoM{?_vBadehLSURr#7$c-ae9NZ3;Y$+mcrVHyAh zfAmL{Knw^hA=vPcckCWT_nqrAzgigS!Sfx+8PYx$hE?^c!{vA!`*h;}Swllrh!9(= z*Il0N{gna4uArWkg1fFav&Jrq*7q-Jm(*^e_*z5$bAm}1&)VPR8B7kD+9pvy&K*Z8zSrU}88+ViSzM zzT1OHz&@BT(mPZ*N>uCKeNG2#k^2@M%SdOmNtw>ie9c3}GPYRcg*>xy7Kh%ik0|Sk z=@togaThj6FR)zYis+mrZH2pnOjp?ia-gRC%l^wx@r?c=6gPNV-5Nj?b<@%7Ho0yt z3uPwjITX7Rt-Q|cm(_(oczSuWs?P`e`D((z z|F7zMyK)c0_}BOimh>T;x^&CJ<_3T8x7|0wbAbq5NOEfF+NXCA1+xAH(5cK6i-`va zyNO7QE>bW6SsuT5MnUPJIE&+)n)wXU{FyayJ9cXi{4uM)bcBX;T-(J`wW+f$i~N*i z!`5;v*Ki}f%uq!M(5p@iH#DeV6&B~3>Ds?%ch_fY;`4bg=F~^XRTur^9sB32qHxam zAC|Glm(0?;#HTDBOKZ+3{aeF*mOs(Y(UM4$wf^EHX*P(l=5UqhEI?bla-{q|g_bp? zVP|hvW9zLq(aHN19+^TImVeoLuf~{#^ zV5S$P&=+=omAR$mvvLsQiprNy-H%U{9NP_h2FJsMO?7GjmMhY~_Q%J9{<=dIh~5a@ z{Tr{SI9R^0(p$cZe#ODUqPsx(-dvorEVX$)Qw8|B0rO$|AV@TiKRiQjyH0yubTtqbL&I~m4uLQ;+vpg_astW{nC1VOvB1S&rD@#x#j{xtU*l;R&+YywuZj`&g2GKe@A*xJ zAcyvOXL-T55?RI~_rM=o*va-5C$7lg>tDW#JC!V<+$OJlseEW4W#elL3h+XDtYq1GDIuBA9tBJJQz8GZ0&ss|mu_3T7jx6fX?PMo z8yRSP{N6oxOYx04`jy+MGO_E**6#5nvILrlN}o(|4I`ZvCC^|6d|eY%ZLL$DZ3T!x znOAH?I;(DDWk0pa-!W!={$8E4)g#|mm*QG&#vH0bqsp}*R1-uQzutA=HTYO$Ref0} z)aKpJ?Kw}GUpDm5W#S>0!gZqGX~!Y&?_u=vHO0i6*!1eA4E&Q1)W3f0WH?PXwk*?W z=X)9f8#Ld?Unr=kd`erGaX`%+pQyu(!jb5M$2)a?KR!5x!Widh74xa4aRm&Ooz*!8 zW(Zkty6qS(HPqMul-D*c^3AQs98~P|jqH@V)YnOw$aGaVWc#MbO%a`KkxY# zu+%##29O0nDig%!DSrun(7OHv;9i z882-&a2IPRn3_<}6#}!f)UtJ6>_1Fcr>Gs(K*UYfdCvVuac_Pj@l`w6zpWO#+LjZ0 z^3Yq`8?>DDqU4 zqUwL%NK-Kc{-AylE9mb7@>Gx*r;yQN9jTqU5#$1}hEY_}`)Y|cv_F6C-Z)LgKKp4N z>>@|!{;KD18SkW0n|yueHTi9nlt}%HC!c0=&Da=3v3WdCWLft#MY4=~70X{IKD$q_ znwXhmF4Tv6Dr+qM?a`P&dRp`!g+ib#SYmW^!KIRH=}7j8v?=#E5gLt%t0MLl0&{Dm z>A%(XRKTn)nB+Qdt_&P+Z|^?jMut>hJ+WE)G*fZ2eT%9ro;$Bi0BXDVxr?$LSu-qOiy63@Z< z*-mrEk0yBBi3$X4?}4HUjJ`M7TPBgSWwYv03C*?#3672&G{);WzeJsrCGWkyl|?Xy zT!T5fWzusxi(OB1cX#T($@Zw`#`Uzy*WXg9IngsRkngj^%Gx>W4NyWx#T>sxP7$r- z%skbu*vyCQyS`5ys@ld(WbS+xcK_p}LN9DWTr1lSPdQN&7(OAR4#XN?yn~=1wp)K9 z?slF>{zs8Wx>BRWhKBt-A*FBiHO^Wk#FmohV4nN8YMw(Sj;h`yPF$~_EAtdg&=1J> z59#Q|&*7&lz9`UrEoOOF@LRBehKAzaZw<-q-G2`EpR_F9i zqhl>rjsMX>gR5VcJ_R6}9wc(yLqb7zCi;~4!%)qCX8zXaeZ5)J1KEO6pgikVT zg#nrQQ1i}@6aV_u>Qx1jv6@r{29=80S$5HwA%dh*ZjB-h$}j-MhGf%W6nN?eQ#tRT z?fbE6#V>xdoGeJu@2n^-n37^}$%d)1cbJNDCV$dGBGqC{GsVvp#oJXc-}_pO=X5_F zcI5b~uLV<7d{sMIlA#eWC4KyB5a109U>WhsW-`x<(q)~EKE2}({=m{AAD^jDZ3rs9 zliyj`=Ut|b`q8E!{}5Dl{e25NmV&Hl7`5k1{&cI=Gw{u-)^U+3U+ialEpk`Y0UHYA zAip^{z_;G7m5>uB?fRmr(+ja>j~I)FDxqLA^bi)E zz0YQVR~lI;tGT2b0`6Xk!M3n-SZ*^P4yvD5>DEdW@Mx`|%9F@PFJ8X>(K$Dyt=vm( z{MPCv0oOEbJAH~q>XSTJU*VDk8$jlHzDAiBC(X5cEj4Xy-g1si>pf6=x%Nok7KM6VN ztpwhV$!Hy8aVSN@*sO7ZLoVY>Rrbwo7}Lr&Oxey>2?FdbHX~ReTi3*v3C&Q@{01@E zGRY98wU!ud5GV5Ud4XM^jF@U?#zN-?tjn`ZOB%KsI=?de5!J1AQQWR)Nd-<6d9ju zLmwZXTHi7Dtl#mIsxiBxXtmKnfBWmMVs%VcQgA-j()24XeAXB^delA!Fnv9*Kcr#< zXi4Ip?X1HGl~#PDWE*b2;TtG&!-!ol!7IL|7_w}fDO)1o`OcT!NrQ?j{ar?3#_Eu( z$$=5RKN+??0v3KusiOx5kMDSa>9X##?t$KCRXBUNKt81{*56=g+qZv8DMamR$>fwx z`;!p9U{X3u#h;ReZFEmSe8Ki`r@60jX&%AmFU+L&&dns=Sa?*W<%?K@YZ{D7XjKr3 zmq|x!UONS1AmGsTDmTTUUf*lk@T>h z>A_-iW*%%wFaSF%d`)y}kVXk_Z_B==aQBpckhUzDmS;{TCdLd)t8)Q$d{hmpMts{R zB?o(2mwauL+b5>E$THyK#v2^CUpRZ(H|Ti9+N!=Sq41;aC?B{_>GI*(QaFtAJgm|m+5ze&KWy#&!l3I=OcC>Kv&8yO zg#mM(#DgAhp;o9j1|;j$*>HAe?k6EAXlB&=TPgdPM9@92u0r+FW68SCyYC$7r8IL? zz>1%)-#GG@sMAch(>nyoSkX1U(w@&>haFYEmA}moj`wx|g7uo`ppWa-QJ+F`8gx+A zy+sWf-&SNrR`P{zRKM+IvkXs?aBmF|^}XvdV6<6f5^tI$k!9HqvCv;Hw$GLQ@rXzA z3i?zM_@hpCu4irfxX<_>U&5JcpMltrkK&M*n=vG2km*Uyo45yK3mzwGV-Y?b=djgX zuw~xwf9J|!=%av@g2+oeT&H)4%)Q1I(XzKGx@H!7SRF*1C)a())01H-#ZGLBmdmYvAKWOt>~AxA_}ypBplB+#Z&DwO;dzp73I2hZszoJVoU{k9 zhcR3Ft%Pg$5DK}Ein5If|4}HRVA%5r=CE+Nf=k&;gNGxRYEq|8W!V@>@BZ;C{lY+l z9mG88r#mry8=7#Jat(v_YsdAFu8*~C4~%yybq^;UfBUM;r)G=PJ3-4iw`JZ?ERd^0 zStX#e>Vwa5m6O-7u41|lD1%@Pl?s5r3P=9y`Jy$6APZc_q+xaY+?*tAZ-xmwCU?s_ zWBo)v_koCK^DMr+5>V)oB{;&JD_Z-j`J2D2DJwe#D!%b0!{`)%!R5Rx?^`m*Vhi)N z+eU4U-plzMv((q+60$8~o{6@|8xA-BG>+9kQ>=!G6J#LsJH@vbs5&{@-K*!8#i`1- zTBlwIKRA^x<8P(P1SVo(;WZ~Pftj$#Yd{1{>FD&h6B1v0qNqY7g#AIF6th_piC!mK z`G0Jm7&iC(wrqGJYMY4=XYU z3ytq6I$i|~RY2^X6R)z4&iRM_oU5^e<}(SKOBAynyPPnZ+2xb)sgDq0 zYwoGy^iROdSA=8h`4Q*D=Lzr3WxmYB$XpnM9q)-RIa0Z~y+RIl%4-6eV;73uCk%CU zpFd?UUC@~}=(~r&;E~LE1W}a7|IZbO; zZTfGPLBwp@Wf8)*&MHNg9)Y8kxz%EA(|E#S-Yjm?Vm544Al;0uKz6x-p(d^9KJATr zVfR1jw(;C)`hxV54Q5BWf{{v&OZw?gTZ92PIydGK|RZak$ z^68g;bl)rXJCd9mP#V81sdrOVU(5i2qFHr*LWZKEDNHJ@N*TKOByJzKN< zGWyj!itBGFz4fj4{ClGdb(AJG|q3#D}t| z=pq7lULaJmy`$LJl$CF-sJc$P*$Tt`#Ug6lcabWpxus>>lV!w})Q|doOvVMD2#S*7UwXMzC&5|$`*?$fi*44J^t7q^si+0%t z)B3u${-Lsn}mdiM*hKrAJ07=WWtIXvJJ;& zY*v9^f|f2LfS+FnMyHSa1GKKZiXiWpLn&$a9D7=xY4H{sXxE?q|h zd8?QcQ(zPQcNZ=av37S){X}@|_=1;VBBv6Z{Wxn87>;dm#i1V<_cA@w%e2$P-zc%g>`AqcMX2Pkfs=%;(ID+-nV3dT z7V8AQh_M-_=kngcUf$*e*(wd^nM`-Uxc@y|y`xD@!%Zy}o`WcAf9_iV!In=8B|W#zV@nQCz1LLJ#}c?;gDhC1ekM-M+-{Mm z)(*K>iAmo*d=$Ub8CFfvOugf7s?Aol+6(EUJUEvtt;Zlx5<#6_jgvM7wqwk3nukq` zd$wp5xm+I;-5Spsf!BdeVr;V&P_C)QNh^xbmCi29*8%yg>T@$~;MAv;V`vkPRx|&O z3X#^pcim}Rao!d%!Tg_G=+>I5F;3H4?L#)v%hGIF@55y;?|FxwVinHj_47X_cZ(4& zFf*5MKC}w#?$V_iS8;tG!%*FkR+Z-Y>RCrLO{KA!i+($Ytc=sK z1l;-@_Jc7tv*?m0 z+`cMU8N+%7m*qJzh&;@xH`FRS*?cpnGV8~?#h#d%xwg{sAGhVEbpvzw>&gTR zKvo$>(;o1FHLt&Gs?aN$uH< zFX^={VGw6LUEFdjc*1Fc&0NI--F*x7PmHM%@)fn64Z3@BNzc>44FZD9RL3qNj7b0I zaZ>l<7hi76+Mnb(ZLMbKS3uZ&q)|xBS_}E|?|g56@=DaJ-ZmZmb&n^UjWB>$RZ*Ex zf{k$`Y)l^}S6K_F!aClMMV4*J-i4=H@8sfm&5$@=pIR#O-n-dlzry({k5XK}oKq9* z#(VEf5~LfhX#s=^RqoajRR_=eG&^m z)_x%l3AzI?$dZtC7N^SlenYg)V$|Km;CYoI7{7;zo%mEVpp;=XX<-nY)gCzJQM6|K z=JaB4c4W*ais@VU>;+-bwW{VQ=&CH-buI$9dD1oRZKe*XP!fLz8S?{kW(mOZPTSZF zTx6x|y<0O2O+h9q#r(@$7R->zq`6&~D}F=A-w5MW_{ppy9?ho9D+JPgGcBxLYm#9B z{PMZzr=?f(u@7*fKbC-y?S?t@4ula8jjFn~(XPp7x74dFXLV&NMmuee(AK|Sptfsg zcI1b25bQ$wTrKav^{!EW9)i)HC>IJ~c6xZTTSINX3&uj@gtRKc|D!;KxVHU{47o}t zX4ixk<=+|$^uq-V+4$mCwK|$6^`CUS*XmQ;mwCJw@-Ltp?;)FHn*%FF>x|9LILwZo z_`a%7brH1^KiHe*-aV9V7Hro|!y{rAtmU4Vf1+mqfnG*dY$2yrtkH4D>EwJe0q>ZWNjvqSyu7ajp;NtAlGDvY7>2+ns7FduZVU1fVSJ8vNg&`x& zHdVp{=kMpa3Ulk$bJIt~J@}IP$A$kr_~-3A;-hYI?s&8HE7-)}UgWQ}$^PdF1&>5L z_O>Bup#n9o;P_78Ca9G{6f{*b-JIDxl~z>t7}%BOBL`q*c3$Q{T|wYh~5-7aDWN zy$`di+J6&nX+l$;|K3~MAlq9{T01Wva5`B=Jm>{|sCEaKSi1uzL6v%0wRU$dtG;7* zg-w3-^dX2THLxX@UMSe|q)0!-f_PZ<_ohFlAMhUq=wb-nlVaAsi971I8=mrr0m7Y+ zE)iAvRhc{6zkB(Vzz8GBq4XA=emSHz8e15wq9$)=Jc$}Td4m&2hE7$qP6E>di=h9; zgPiAvw+=5wj*MK2;0ka;{Bfo~cMNF!#zbbJ| zZ?+ko^xf1N=)L*GCC>CL4t3De?;3Lw4$`utwRr==5QO3n&JRnzf-7bg^fzt3?@=}; zMAr+!niG3vw@e)mH3_-c1&ob1c=lo4C`m5>D~*ClRSnrr^ZmKG1G_mqLr<%aNzvsm zJU~rNrLOfrWY9ns#;s7wK9>;`$w? z6oST_)u-cDgUjk`=G+x^&8Kcyl^b@3H#33RxUp@MTIB%l4xhUdSBGzRl~t~u>=66T z*278_*Bi7oX3x(~5A#TqaI(I3(Rv9y?zcF;RPD&G8tGMgqS?-dP(gnsF(>kL7Qu?_ zo)W_Mc`vqAyHk+HeVj>tWtD+>yP;hqgl&;)T{^HGEGj&`>{_u?3zn6<ebbFEo2GX*S*+dR+z-KQHp)(KFfyy*3w;YUJ z+GI>SIEKbLI5+IY*m%8m&u8w@j2O#w)jYZVp)`}?7t5E&k4Kt8F&r=G;$fy=eD%_u z?vnbC`~IWY^%;raacjktGtuI|9mt*Tcn5 zFqM1M*VL4Yv+x-bRgpK^ZPj){|Gfw}wke^C5Z2(~t`b#u1$aXUv&6cD3}Gc$xQx&QhBZw&3r{zT#pXn63v z=SbTBeIxGxY!iBAuoF65^5HQKzf?aEL2&O#n2$MAs;Mi{7LHOVo~zydRBcpAQ}e`f z(6TnWcF`o%r)G2R;f*KKj1x5(9jiPAPHkG*V(%r z!I?j4g81q|9&Yvv`v(<2Hz-4nSVay&SM5U35oN3NX)|Kj?zT6%b z)VU_O-fW%mie)jjDwG%Zbte>Hl^75;-^t32Zri?S4sPOiCo@nKng(}+a`yX$e#s!Z z^oKb2*SmV^%71hR1YSSDhY_uW8;Sy_XYe)Wd}=j&JvjWey*}4XxWcYiT+1nd;wny_wlG0C>teSuDIgr0CFNXyJSC z8{!#xV14OMAo?f}d$v7wm;_TwNQNHRuExP5t?E+DDz!^9Ck}an7IxBzeaZLB*u3y4 zr=l*9FImce@0Guxzj(%aGR|+UvXPFQ48m-+B}MnWS6Y1NSlHM|BTUJ#S6wJr(`J~N zXP%|_lb{-zQWg?%Gm8{*`7ga{GVvdJTQwWcw|^z}aV^hkHI3m{G)zdeB=hH#HkpXV z=;OdixL9GjC0eGd>7|GW%zK$)ol{4dZZ;b<&Dxu>`oq0JH`M4$`*@}0vO)C*j+C-_ zg|w*01&mN_1&t-{ZXew=8$IUJ8t>%vCM4UH#qi%3(0N7k8r#Eoo&2MheDwH}{o{Lu z&O6T>9jj;;kgxH*Fe#}8xqwU>1(O-w$lUPdp8@zO6S=XvpHo_z#K1r|QFtS80nMJ5UB1H-lD{~po?e6hM-Cv(8OE}r?`x{+0; zm!6;M*&KR~*QX8-(~?@~vEjRSx^V_4>&?UDptv3atJr3F;_Xy{1Y2v!`R!T@X#o0Z~JOqM*6zs18;Qjc0Oi%eNvc@zt{LVVvJGMI;z1N_Fqq!+i%<2z+k|%18q> z{;BZ^ujJbG%!w55NQ1hpJ@=`_te{jrY z_b=llkCz3nZ8+t1d=h(A4I@_N_s;FI)@rW|_E&}4er-%`_Ely74D|H~7~A24lf3?; zP+jgY6Uu`Yk$d|tef^0oRtQTFu`SQV0)fiza7~6S@)p^sc1l*+7KD@UBHfn&y%&}_gL%4e0j!4H9{FO_Lq zM`7dXU`d*Lvf|xE8KREnmKufCYI+`mgag5uJEnrMshYir21bWWRuEWP^*%Q5F(X z7~h?$DJu?g^;NA>>cV`FI4oE`I^8)L+DE35){?K_+YzwQQ^g(tMAi2sL_+i}XBZ=a zjK9wN&e^Jf)Gf;8bEPX&31V;${(BlJ)Z6fxF7O`Oq$;GWVS;o|kyjP_x36qO#RtZ9 ze0HKXa+x@MDwm7JtXy!P8SvbI7nASUj>75h?(-04GJpt-c(i6UG?(-+XD!>N65?t% zhVC4*4X(;I_NyC>kyy3#K3X$qSIi#?(pyCpA|kxs`q<}pE+HcB_dmGW zzdrD-M}X`?8nBCL)(zSRF<+xs;(pw6W{+j>A$;pX-Z77>tBNZ#qWkk zx>|=y_JCxrfw~~yZQ@_ojKTr*oR0W*xMty!&|n1oM&-sn+9PH5KMHAbqe|>jXd^&KsId`TAvU<8UE?|>75#Na3QBrz1_}#SiU!bS%EmuC0cfPb z9Crlo9i>aq?|(hlnzk#GJoEAjIUEDy-!_a&+!hDQ#4slT&hj!7Kj=|VD`aU7NLPl^f8Z0V6m4ku&EYdPmpeSKu zs45;01Cko>tMLK;k=zkg<9<9(ov~4A<~H$l3?J{YLN;LnEHn@NrycrErAAcjxcpXff`&TpA#W zSzyNrF#k7wak)s8v`_8k?li&QLrV0nsH@Ewx_`(vxC{YJcx*N1;mlNRc&*fa%B`o(5onrS;{n?lDTfgbG z?Zs)RhN-lcu7tXe;K+?j&2ePXQmTT{!y|*ym}5S@+LhJwF)@ArCg4b=YEL49QCy=qRJJWmu#wp8d{kq1Jmp7Yc^a6-1WJ z;@ZxO$YX8rRNMyIJ(COW{rBo^3ps$D_Z=!(A@qF6YVY*nctdqr`Te5F?Cyxz<^L6K zah_%txPxE|kLF1ai?xR<-^y<{B*>i;b^^b}<`&BtS}7BdKq~}L{T>M>f+0mZh+!fp zB(1D*fXbPw3P=(@ zCT`uKXbBxu_6s2QgSy+(dF)Nnc^!9aKfn4eV9FmT3ZdFFotET8dt}6lnrl;#9Tmom z2!VJBGrz{}cseNh%MLHq(*- zGtKTO{rr}@uf3mfukB#A9qS1GBvX%xjoM|yB>TKi<@EMuZk*|q<$N$VQ`MFA27EVH*11G+vr~q~VdEROs zig@{XdRjF0$Bm;}%Mt`c{od)MP-LCx#^p`<9~Ki;Zv)Jx0)pOs?`Yk$VtT8iTaCTW zSeez7F>e&%zZIsYeEJs_p*dbPZpo){UbNA*LK4rmh~e52=nwi zvcuqAV;I_KLd|n^woyRBRekGymbq1(#_e|c zmZGi|nL2-Kxm452nOUYMufWXz61z+F6&0Z4GKXbaH(|14L(9Zr;Yak9t8iw4&{ptSvIkHcWp1UNWponUj)WK&E zSkx!5ON_K@=aHB4rFI)SjYqhSo>XJA0kdPe zr4^I)p{nDb`_x+Ez+}6Xzw`m|lw6yOCUoRk?h?D@&T*6E$aEmK8gQ>`Qcu4<*Yq~5 zKDVI&td$qQ&Nkq~+9W#t^|`sy74KPDA6iOgiMdQ70zGyD)jK6D+D40V{uvGMKe|-D zFffph`;6BFzjcTruc`TRPo-O;OaG?UkkZr0ls~&iu6CXmlhfE<)rgm=NKNMnPYZKU z8iRS-Fd_6b!Lebk)8FD%tA~zVlZwso^HZtmtm~NMI@qa{RzS#OOzlB(0&TIfO z#|`U&^kn@(dA^pa2q`lR)~66lf~D_8bmLXGa{8mLsxaZf3r8J2y||jU_S!HF?a#mj zsCun$tEmmbGNNP~3J)m5=-a}@{~QS0J^SlLsJyYwyKru`avT|XdZBuw|GinI2kjJ_ z+=HguBOsJTrT3VX`*B1#w9xNlmHA33(#vMt>q(t4tJOr;m3NBHfOn!q%KN#)3$;@G zP2-{Sp@aV@lnICOsH#eGX9uPQkgK(Z`sCs~k5Q?6>$4t7Oi#WZN-tR;WEX2Hbv$Y? zdnkBRYiq!+w*z5P;+ z6QYVGNWBn72njX85*IX=la!KG`x4VDj1h@$KcQi8u?5G9WkGywovmeLk&TcU50h_M zD2bg%VPIv))~@c-m!)Kn^omP_UP&1bt{vJMAP-V`<#9w_k9Jl6^4$Z<@sWlZk8t0+ z1mus1L;i(<<7>;25zBAR2qJ$AxfdH8yT?%BYj&<#J=w0zRo3pyTU>~Uw!97f^)_`& zY-iV5C(MKF1i>9Dp2&)l^Nb|3n))ph=UKv|Omj+exnDB^@4g1?0=g3^PLzYDO?1G@85t#RHV#GN;Z>d6*!@mumvyiwcHBKXmBbqyIfa>NKggA^mncn(pY<89TuUqJ^ovj~3S0V?I5B)Mepm)N2yRJ|99&IUHL*e|QX<$M z&!%gcYiAXZ@%JN7IcV-v+Vg`a5O_ng?K2Da)i*(Bw!tNhV^w3l%)IiE5ojTp%{GZK z{~)sWAWRPOtwK6J{dWCh7@TAa1PrYZNs(!o#cMTPAAoLTju@ z%`lZPP_D_3M;vWYNW7YNm+&lya&X)>y+aw*C})$mQwF?#QqT)E6n=Wp2Yh0?F|_oj z&>F7ihr?gN#>hPw;B1J>O?YpLv|dJ@^sRTCls@@On%yeF3fPuO&`%GTN>Hws0#--C zWT6!Nc)uzkYdA0ZPkW^GC!{&|My~d@fCbT0a9z5WYEj2Yc7H~W>t?V>+Tx+~bCeIpV zw~Y0w36Yh0O#6UwWq^-jD}`k7JwpG;Wg!G!6j`;Y*r!@PvkPkDzQ_Ls)+Djm*HkfoLlIY22fnMTmUSu~ zik{sHzbtjd$F-e66g-MCO_Vk@dFJ=s{WT~0tpiC;#XDOV#n1pA?k?scOD=mhdBOHj zEEKTp)c23)GT9bSIz-9Bn}D8bdAZCQfhP zg>N>3mHwt+Ih*MHL4}60tUpMnab(MJolywVsMsO{TUq)u8w^mAS$NQ({gg!rFrDtp zR)R{L#&QptN#=+1WGxlh)_UOr>~2RKe*LSOooif|t$TA4RHb?;2^}I+tP$L5FS&=b zRFN?2z-|XK?US4G(`R;1(8mU5L+hkah4H_$*sQMgHeFd6N>{WR6X~wd+V;iKi5(x> zH*j_uVCD33yHKk*^s4Xz2jAKL-{QpI?mmr6RtOV_$4o5-HaXQjW@GvphY&MC)hxdJ znCTdR+D{6qxSQ!CvzCu8WUq@IWva*4!ai$0qN4U;drL!l9R{I}UE`u()F#n;vp0$k zJ7qMz(fQMRSRWNhq1G|r@g8U`-G%M`LJc)bTWF<|RdwV*^is}IUsAwn z7C5I$G(6v;E=`WD(mYSFA>kVcCREwel@wa*f@lU%wWD|h3wlgdpjwqRMwWk+KKFr1 z@Vp}*r=Q1a#y>I8$GhbBt!KrWJOHTFp_(5qf-NF{cwxU2Qph_e;D(!rNBvfV>m;H= zZ)cm8i#i?&sg8drwZ|8Ic{@4h+;t`#UVExbtfR)iDXu|NQH7gQ0%bq9iN2JPMo+2xSm35r}8{$ZSSwtc_qB z@Mnv9L{bei%lku8*lk{Bq<;P2+}!ai^&>E(*k1QOS$35l?WKm-rD(OkISDm;NlqxM zW-bCx)Sj#t!LwLd1X*u6Q&%Demy#qjm-4#rLS;MIu|0IPP@{O+!ADe2Svv>R$y-y> zE1Dm|VrpLJuSfgjb;yFZQO3yy>YYe z|8sw@Klnm@DSV#K^PJ~>&Uv5noHHY40x1`Yyk4EN^5Ko`&b#3dQ?|Fd*Lr$p{`i6R z2XlQ+Hw*2j-ftBzTzWoP)iPYPgw5AN7C#zPv3;A|{Kq%h@e!NlgCe7pzoeUMEAMxP z;9Kv}8^Ww}R@)edPXEC%aLvg1w6sJXls~K47;Nq)8@hiYdUnm|!~cLQ{}fuUo-Wwj zRtg@_pFVU>&+hWcsVl5Z!i6+gEhPkdgUws12yP#c(3mjmhj`j#~Fwcg^ z_P(~`#P=IN<&|3&_U9wH8BkysqDLLVg2vI!aXO`|zxUkQA^wpJQpGNBDy9A^FTd~f zfAX{Q#kT|b;F%-P@K6QmLnO2^nSBc%@#O2>Pk%lC74^^TuW)-~Ld9Rcbr7mrcyNC_PHP~6iLI7IAFQ@3IaRi&_2IZ zXGTFa`^ULW+caED7c?Zdb}nrH_1eDkY{l}2$4jU7XeyJlq-we@NTuHFu<`{Z}FdH~|}JpgqTV$qB@k0tkMcUZhmZPhwCd$nVc zomF@Ft~1d(=#G;D2obyowb|r@kA0zwu{}h6;qrBS3f%0)Yxr)gvrp=UVm7 z9qSv8oB(qn_8N#!zAg*|v&xOu%pPYLgYCLtFk`S^qYe0zu`yu-HafTJ3n=*3cjv2b zTWpR3t*Q=>YX7cm{#3gU+%qqgkth$m_%k%I@-69W8F}(^y#7bAL>>T zz}*YdoX-xKYa?Ah+Y?z-dV1=43cmbGKW0eVYEvNDil6nc$Ns3r8(W>o%>5NFU$_52 zCBHtQi;kgfpSd*JbN!&d6?GEYU6&m~P8dxnz4GvKFt=?B(QnA2Z@Ojsg0uS0r#mGs zT2Gw~?D=25|5U$eWybMz8Av3>*U*lFhgoauS8sjuf8}`LZS!Pm#M_Q%7F+S@`$7g> z+7g|KTyldvmGj>LJGmnIFz_k9kH;F6KVJka(R{pqmFT104EYpQ>^Qjo zgmdS|6ZL*sX;;v;?ekkRY+~(IS#{Lg!(1Izb z!HJ0R`58`1>gtXg+jU#G4e`efUuUFPJ)mf;!-V@sTTD1vOW3_z_e)3-f)2H@n96NE zQ?U71N&h>X>15ZGAkL=CPx0hM-c+b#7CrfBPK8CKL%EMd)#^jPv?1k5Ywqf!K%Zwj zpKA2~G`Mi{?9x?^>(lqw{&4zzDavDxIf*8jwJm<({9RbBXw!_){d?t~Q;OFHv=0e+ zA%`7<+5$VLoDWn@O*2@0h_S;V40-RwYU0lRgq1Gu9x ze;&Z7mJ>||xBpy8NPZMGc5VK1_rr*z$3t+GKf_@c*O3H*6ihxYCkfUIrRKY8CO9mg z{QJ{Yg`$9Qv$L|ZX>n!wBw2B3Pe(79@B5y!^1|J!%|2{#-b*}JG8sWdtXjh6YJ@50 zL)aIdBwzW_asJgI-O>wwV1j1kJ;WVrzkF5ipYF?ChX+`I4T%+tGj zFMI?8#vAYc*l}?B`{~Y!?+ajn1%|MJalkkWfq<~FgIai%rQQ`mJu4)7Vi7MoBsm3C(`#&Q6V+ zf}fKE9(0^sTVHwN=s%s^?@xwxzCU?iyqNOpI{DQR;C0ICG#Fx14*{Lw-m}-*K74r| zJ#ozW;(62R;12WGAsvoGu{EaG1Wq9wzbN~*ld~;tN3tE0V~VtZ3y*_&4|Ye7>eh#hFMOsET0`ceseaE#ejp50B$sdKt0)?6D_|jHfhj$kaUVj^=#$!(2B@ zsj+Gfic!&FNeD$HT{bqxrjb14x3GOanvLftUq&o_x>r%x{`j`1V}5K8ESQU?AqPgx zH#7mj^iaIGh^6FlX~pUk!v&P@=GG4@p9aa`sg+9)OWUq4%D!A3O1tQ|@8Z?2O%2x# zykAsK?+A^IFnMN$NVp^Ji{(`2j6zW2)>+0-%3$k_^vlt^`GfC%Mjeds^Ln;v*P%d< z7DxG&YaX|J`y!$rr>4xbzrK`RuNL}p)qIOpJ=ln-G7harcL^2;gJ9&Iorm7Ix8rGS>~y0JwR>+@ffstvnuqvg`UGOKy(I zmTLp*cj6CooRc2EINQDDF5|*!!9z{oU_)X~1B_r&lFve5i$y%a(48(f=Cn2WNLrMJBNGf|;0j!yeaT9G`=isxR9$o?N*CrY2sVvY0`( zfOZ`FN#n;*k$OU8UHSI$_2y#Deep+z9^qXnlL?p^Z*ENM`V^3n)rD+ou(Epa8QXEN zH%o~*A+X3oR%mpcHNUz2OxmX&9moQa7YRXQV2>bt;L|YDR=@C);_AVIU(Y;urr(`k zxp}ym^Oxt;>BSQmG#ZD-pkXjH24o{PKp@({@NF8@NW~^PTwBF8=VH@se%c8qABQ+}VAi$xjf%sL3Onlm zWc11tax}f0O|RSi#gtNq%<6OAQGIz(p1xth7jm*HX}`Rb* zt8qcYIyNw40;F%SBASUAsTwigpL^|S{nD3%iD3y2H)`h3;af_JVh-f))6x;D$GKVB zwg+8u=z2i2LpSV;V)$9Lng5-VcJaRG`{x&b$xocV;aq4I=KgT%*yKnO1<1*R4ieUHMhVV7! z=fIAvZniEC1BF2`XbmL@2Lj{MfZL+H&qt(}3+jE&zR=kB*P1{1%e(J4zvv<%5Tp_S zCb$6zI2<>)oV z$nNpDlecGyM~#jr6LMfP2}wIq9$vYKcs zd%3UUqu!4{0fPuTGexb!}lbTGE5?gFSs^UOvb*1)lLU8ai@@mbYyw8+Sgjir} zD{gF9=Qa2&l;hJF^}s?apt^e1JYL;hlyJj4Ata!gFdV-X$4yY~d3xCD$LS!x(&p_D z#m1XE?V~$uqoz8bFbgnUpxCzH1g~1pC?Z5thjz}o)hW6&wSd_u^nwg0I5Cj0V?X@Rj@`( z-I{Cmz-{NvJEwkq`CVCCzjTW;3O1(!)j78a4B5W8o3!-ze{p`(FrW6NH;o^qyY{qK zwnT%4tK`4dyVTs-S?F@@!%6jmcqnwk!ziBv`NlYVO>Lz_43nE_G@dBV6)OiNp5x?k z1uQxo?1^*$ViE6sGKccWcQ>VC31-n%n(VGL;zhHB>R9lpSG_5Dqm3VP_=59?F(v1o zSg6>wf|cOW^hMj0`8M0+3n^`ZC+w%EVrT8bvo08Kxgsi>hN-eM5y;{NZou>WQNe(q z$aY7fJ9PNSkMl{_Zr4W_JUce~ixaW*^U8b+IQea9nWn0E*{4{iHyCyFo8+D1$Ssp;8=EaUH1vaL zIsuPDf){-LqMY?r zEzKppWMkmJkDSXs%k&Q)F5mU-t9a?#jbAR0m(upszx(&~x-Y-}E>GKY%reL30AQxR zf6M^~kL1y8i#>2!R_`^(jl5$G7woVE{zlW+4M3LrNL7~V9o8mF3Xwm8j*N{;@@onnQXZ?f{{gu@;M_t4u@e7w-Tnnt^9rj3c%J9bSdw3C$|c1GO)c-Pag z*0_kXKokyb6IXIi)L{9H7C*8L1GUucb8y?C*-t0F`(yHDb)sI&y9RU%pizCBv}u%4S0$yUyx04MZ)a}H|9Vj>`RCO2r;@jmN%_#xcc;cr zjbCn1x4EXZ4rg{IT*+Z@fP5wO@5{T>iP| znDpf5E&^)|Yo@wbgyYN^ZAP-4bjyZEe5Ys>a~5{c0#Zb8Ab>n6PSx=);T?E*%ft~j zE)>J7&TDHpg%q%CjN5}Xc0g3AW$vzzC>|rImRRwq5bNVpi33}Cmv`l5642-ku?_%8 zfCI$xO$eL-rc|6_x%6E8-LKl=lb_Eql+`G02i6gy%DLm$jyP+0#I{`*hVpP|f=sCL zyj3h>mb4@Cu3dVXcja=af5m){AI;sHdok1Eu|;Dt3kXm(sQlN(9JXONxj}Ki^w#|^ zI}Uah`Z4?5f8tfU2;d$D&_JAX`CLVqGA!#IL!h-{%0@ zNez&W>J}QnJG@N^0k&(0hB6vSCl@S{@emlNv=|xO6CRmQQ~frFcIAyKx$P3EP5_oR z>XbTVLaPmoPz>rm0GdQ!$VK?h=uTJ+I>sb@V+6;S#%ftJ~<9dC@kx|>u`n4CUKZl^|mx(7QxE31~#eY09XClcx7y|(F%!TFwNuJ2A=BCh=N_An?>^fA;P zCEx*pI0T0kc#N>HJ(f^l%#jm5mmy?TT`5MeMm}z$L6seiX-(xyf!lVRT)>DoL1}L4 zgqb{deva;rdGZ*2nD)Wb@IIzJYx6dQm4Qlx+@gsAN5`a!#Yhh4d{)W9oF9({R(;7XAuI2e`kyGsteflkV}0)? z2E@Kaa(ay6ScxV;03Z;lX5;A-vXkV~d#AsjDqsv*r8(cf{x!Q`U*GSfA=S3vT3*_Y z=lAh1s@J1&I1UD*Ol&P0F0zs0*GiT@{AyhIdU-_p^Nr|ZW|0cy9Jk9MCK0ZhXOjqn z*+KPvSeijZeI?`J^S-1x?}ngl5Yx!6=^y}2bP5=V>OD`!6lZNU_t|fOBP89aBAz^J z$(SSJY!kwKCqb`>hxjwxi8jxJ-8Kei2H;BV@SIP4H$oGJ(1mv+=|V?guya=psr%pB z5D`Eu;WhwMckfo~PjhjYNhKIYvq2>Bjsp!%2XcCjP<&|E0?KZF zRS};CX^E#GkyD6kZ4rO>m2hPmNh1ON>=V)7OZ1)p#F*!7(^f)(202kc5j+0J^jMFjP(s=Ouy}M7J$P2axLNj4t`?w&n}zIjq)?jx*ZXJ z96qem^ReFJPb&eC1qfjLVZOU>T30(v9UM7Tdhu0(crNnT6E&Ek)~JU1R>*cIaN#}@kA?m)WV z1K$lz@xvVY+!oqKmgPqKp!x3JaJ$1wsEi%1+T-R8jc{4-zF=XZIL?ipMR$z}ZXe&O z4G;$GG(rehSgte)e2W(b7Bk|NL15C*3@5IM&`CfgChl@-Nwj3+O0L9^bWlLta5!}n zqziCG6oG-$dojkaaBuNKQumAaTH4F`a{z~C(#Hb~v)0ihz}58u-qn|b@&ZWl$zeea z&vo9-UBb>cKi(1%;yV}@POziMqh6nZsRYzcT)eSIrRlUvn4?fe+ud^3=(@TpEDK}~ z0pS)ci{3~q7xGuyQv(i@`I)%9G5TKcX)F9St1VSuhtu}Nj9#U%C@h2}77E3|IXEXB z?3nYtk1{p8pU0lco#JnXbzW?Uf?-r(g05mf{=8)dTLOnMxr^=MeGRc^yU<7SkplZ* z*NPGI;}*t9HFE}a$$PF1t{zzhIC2xJP*~O5=CSqWu^DPxvs6tM8ASGi8robJ$n!KL zHa-~DKhl1mTWYHWBxVan*K_KR$5GR4RZVFy(Osf3g#tMN1z<26h>p54!7ApE82w`L z<} zq)RMH+Mcw3J`44!kKXsr08V#)bukSa^>&MJyJmfED&Ib9fc{T;Yr5v&O z!*B!aVca7z67G(3Fb$uqJRWmESCqDO6zgN6X6*@qSPi2KcBxq(?eWT87-o==5f|Ba z=JKZV0#&QLhB*kOiy*o~ed_?t{Nb!dCGY*ie|nJR-G;FKJDGhLq)(7Jb&L5Xk6le$ zkN`aYHqwh40QAt^93mksoLeD`K;(gxrvQ0sNB{HvJ>oqzECR&a!l~d1{IN??YmLKv zlSRyt@e(+}I-34MCq`$9LIEolA2I26+(dLJlW=k(4&@>>f^MG~sBONz{nwgx@ioR= zxJoAXk0fPkndW62{0<%@rl9K}ylBYh$UT+k4nO{KmHjzz;g#;)^+UJot5&aHbl+v6 zrbK6f5QRZO5MT#mNc4V#sJ?e|NtZu-`Tpd$Ih1diVMlsfbqBWtbo|j9+;u1t0)cxz zk;DFHy_-3EFd1{#Pj{$HXsl{%62Kiq7p1jBkpAGDs#X#p1X$IEAcX0zf;WgcKzSUb zY7BK@jtFLKS$<>=oq}+^Y7Fy8!R#)9hSb}D@&t-AE)-WBQV&%I5#d@wD{3Wh6#X)U$Cq`uQK35xs$z{YGx*j!i|N} zVUDV39o|@Z=ZgxreV^a`y)E_Rz+Rn8%?H$#RFp=93pR0y$~?d7Si~1)F6bu$uf3if zO>^O#Oea>n*wO#CfU^WPHD|*?#52 z%$_47z7odRlbAc4-oXhy`$AS^^_Vs#0ffwe8BkP(_ z$o_`}c#NA3I)9i;10uUaVbJj+*7KGs!_kog4&xY%=4MiOh1aCA=bg8ladL|FD+2p2k0EqQn~O4(+WZ^upHVt< z=J?V-HYG~IzJw#&5O_6}Ou6IbxK(b~kk0V!8~2mo8=rAP`ooyo7u7tH9EKwvc~EnG5`;Jw$%jyyfQRr5F*sD8 z5}FT&&Qu9H42=`nj0cXcq5G(ixZOdqA7K6a2n`z^@PPqUw_1&e?W7C}ol7(xFz z&D-jO^a9d+4a8f!Q2iT0t}&whpECds9h1FNB!{GZs$1R znQyn>yeGexU0>VNRa-K9CTeDU=C9+&TncNt4sFB+xgS9Tky@_YDr%@QbW~HB?dW{h zDr|F=&g8y?l#yFCgW4YqXh%+dQY*II~+c zcs<&zKxCAZYeNjW$fei@OqLWJRN>p+CgP0rQ65w-KjysQgJ?_LGRbp7K!b4#4wV2K zPfP+@gNh)TnO+UhG6WrB>w~!)z}V78utA_n*q~u_IK3-|9+?{sc))=Cy8whZbU!^S zqN!;%?QS~+Q*>`32gb2YqJ=|efu7)Z0jMrK0oo*r>kO(EOh)kHgUgy8+~{z|wJ71I z58rcNcj3`WIrHfrqeO%**h5<~3~+56Y@6~WKWy~XRg^*2qHz1|G+y`Ga!94q@*C(qR2&#C|O{l!9- z=v?*ep)vvJO@QT3c-WY+6lI5rp`_B#{c|CxJ0%T2MPQh9OxbL^@vy2ff&jKBz|D=T zf!vA7AbwGsxk#bVyN;laTGtxqQ!*f|y4|60Rb&KBbt62h%a+a?O1v`2qwM25U?>+o zK>8la=ER}_b1?u~3C8OXusOs%O*A?H@oh#V*IYsJo;Vcz`&QcvkpqT`nxl$f1 zPw--xkmYSdpn4d0XBr_fcM&1P1f=G|pNB#}XJ0&&EqS>Xu=5g`<8&DHyJ+J+!w&6${uwCuMGvNe5Y<77y&sk$ zDSrL!$L#9$js(&_|6aX$)2e-U^<+}{#!LYicHn{Y!xc0ov3xOGR7-4c8>=VI;E%_= zahSFoG(`Wy8?&vg~nx}u%yIM8d8%^ zVKv^kv_0wQ>#XyM_H(3SfPDnEByWk8*Yc+k$oJcGsTKSr32Yh zn>Az~WHX0`oF-9lWuCLs{y3cu!&wV?F4M3-eC6uH{Z8-FejbkBSYzLN|9Q@3_&>)^ zEesg$P*r6a?jj9yZET8)Yy@uk&`n6zSPBlUz7g;l&QaU)c=tJuGCND-s;k zpH0Y!lD~nR9HUTJV4G?ji$X`E(>@v;);hU*GU?MFsVB3p|K2|}F7Y@&^XZ2 zFv`0G(5SJRf4K4aG&iFnFjw)uUhB&qt9E07RfHWAdJ2YC6r+48C)R=vHC0 zj&}e$uWoEibV^Wx!YZcWXFH4dAR`U99=>$pQ=f?0cD0x(jt{W_c|ZbSgTR9ZaCX~~ zA3y*0+I!3e93(O;|KWG>3JVWQ5Skb)M$AzWy;HoWxTII0nS|6>w_9svv+vb5qT$~o|S0>Q>O$QGh@Ga||E zb>`iiUQ4>4Jskfso_`Otw& z0I%GlAz-T!K-oY@DuNc{c8v9_2H(cy#9eThIAV0Ze1UF?`Cs6LEh2|?;a>C1| zo2(EOqgv%GTEGs(0T@hW`?I?|A`$tXmJj|0hw#r65bF$~DjC!TBIw{sf5wX#udG`) zRr&`xH`363jI-@CRl-9N9clmOD4&}MpZ(kF)XU>h2gkn_++@Faeee<+ik~1BVe9SG|#T*5(MHq{HJlu z-9hLDOAy?9CHKY%$m>uYC7mKkab`@SDsIQ01e-d_AU9-CbIE5ttE793gspUd-fBzFCwwrwyvSZ5z*-GSvdHw9jU!9%Rk3u1Z zQBA2|I5wapgY*iRbAYfRqHlva4M+$-u_(ucpWnqYYye6TNDCU0Ynx$ag1<8S{!l*1f#lMIbQM=}Ay43e;(@!3Y zx^xC{pUtx%8$`_VLa5;K${OvlpcwZ(J(Ldxq7W+s(0V%>8WtEUA2r z2_}9SNdG*U?P+!NaQE`7w-XGgHtI|iaVmwE*c@?QX2P;} zhz5pT4JGgdm^SROQKeD9pW*aCXmJiVMOE`5ic)mtTSJQ(gryUW*ITfxPEhSI?1H~8J(-P^hCT<)-7r`7Sm{@dH3Wm?hZ<_+@}HZ}l( zU;-UB1!)C?Ue#B65ROasbWN0+!6Z}a>^beX!;hScRUR0kc^W=Z%m$o4^!hXi0*4{Vz?i!cVpi-`#6!nN;xh zvrMsn>IQ52kR%P>pwMVvV-+@}8?9-)D6}*9u^mkE>hHLY*)WP5_ zjE%>ptGKpE(o{qfa)Soh)c*i3NBcOgy?Z))&m^%@Mp!wNmeu@gt0F_H(auiy7q2O6 z(A`~{WiY0cxn?VzPJ_rZq%PSEzA||yB`(las;1Hs$2OoF2^F6%&{d4PA38f!^n3>Y z(f^<9Unf?F%%nSoX6`PXE$({$QzSFbX_?%mhqo-P(ZkdRwyh}YIi1US`n+~_MaC~l zRy}puMRN)#?!>F?gvr;?qN8qbyl=fgrdHej_> z=oyri>5@J9TgWDtN#h*JYK%gmy9NauHdRJGr}B@|B-_BR4>k0+ZmV+-4Id>@Uw-mG zpk91jeonnv!r=^?-x_r(ZCmC%Gy~rtVAkbMOI?JLGTDU0j0*E6HB+Q)L>xPv>PZ!? zVgn>`sC18lg8FIA=>%sLsU5=7Bk!imC*un~)aTpVw*StHx?v=(1y@~#Q7hZoo{laQ zNys%&j+bITStZP0W){R}GljL_M5M#BrxhGE^;@kZVZCU2nJHwuaGA~iEv}vu64J74 z&5SC=7kaId(;N{s5kPmzK$9HyX@o_J@*+3VV}s?otd-2SPaWr9r3Ewx_$CON_U zs=%H~^K4jClI>hDpse1K;0+pfj({JKrbk9j)fLoC;9>sl%ThwFzlgzO@Y&K!0^zM- z22TyStRUmdT#Dps>e*7I7An>Ou@+G4x_-~=$q?sH8Mq(U`b)nmy!2leizVRU!-C)S z^=r^{qiMFXmXyub@0H<)NK&n4e~~BU0xkEyS(}loc)FAEn8G80WTiGKd)=x#h8O6L z4VYL1mK~8(((Sb^4hS?B0BCg;Ig^p!NnyPo+xHCO83-3Sp+4}vDHB8|YzSgHMT}4(3 zA{dRV2Fl~_k{TEAN@7=9K_iBr?p*>Rhhq{H!6{RyLFAXl3H*Nb_$X%97w+g+7S=dr!SH!E}7e4f4G5Z{Z86GK@`U*+?efpyl@| zS=rN?@+}#Zj^YFz5{aMlvaO-AIp1H7`O0K0#q~ImNn-Qj)O{SZL z`3oh>vI+3DUg#n$D5wqdq)JVoS()T?C>t&L!A3dr+?uH{>7}8Y1zC+Q1vO{tsX~9@ z%$n=81HQJwQ2J~#(4DM&Dxc^)J`;G1qmk~Lc4 zKW3i(V!t*;lp0cAS}&D?s4WH8$-os96g?N7r*b~^z6%XE|Qmlmt!vD3#k*FDX9XB zc6F8RbW<=gD7DiHEg>V144zP`f$BXeoR&IU*I=2QGcK7hhhjeDXdMHjExc}bPdXZh4C(&+eb^%maDjAlV4z<{QW=JijM*po=3rSl`L}?D9 z)a5C=dc~nhWm0Vg%zM)+TN>wphdDshRD@C$@s)!AKsh%@-N^spfJQo=B3w30wttmc)USpeZMFd_*FD(cWgE z6g@E|Wq`H-Hmxq3LveK#$pz9CniO!`Q^oEu2lLW| zF*U~Jm5mKG0%L~gaO?*mTAH8BXpSo}^V&T5di4ihk7fok4eD8iQZmeu%|R z_2SZ=k0eeixceu~+Uf$x*zJ0dz)5!umqMrwsDakM36zqELRYGECq0gKAkexn#PFK+ zbFe9!F;PlrDOucgU^*b=J%V1xcE0>oeJla(jtvA=o6LYphB>P+g{$ZMTk2|OWfPz@ zEi2CHsehEP@vw9N#Jm3iZZ#aHn5>56FlwhX8wwhPOgYt^OcH2;QYAo<)%Jq&s}a5~ znNJdlY3fxSizgz|HIKiVHS(C4wp^xF1zn_~?phfa9=djWpdl{MT~A{I?;~L&62_L< zDwK31<}yj@!c!P4pk(#MGu1=NWR#aY)xe8UORK>;!#Zbo*Ff2fhGq_f7w9H7n{-!d zmZGQ4B!?8xJQ5)le?}~sCJHBA>UJT$4wJT*P5`-7w7~mQebeGK8nb1{TJrLf^jRNc z1m>-`AGbD;sspU6Kk)99-cBEIL_$Iysji@Lr%>T9R6J>>uIOjTK)}$Dl5b{9Wb!py zC42i2DUP{<;S^+0YB065JoayHstJY{WG*KUk+4i*Dx!`lGu8-Eq|G#D0$40cJR>Bh?r9G9L#q0|{BRlw3W*RQ4{CG~UI-&H?A zJ5~>60s;yqt4CrU=kJA8l<+3x%21yCVHczDperqGxkgDC_~|#Vg$i?qsCg92VUExH ztsf2@mCogFfQ?ledFiQAwBg!gUd-1lo=YAusJ>rEee~DW$+qI6D_5Nhviojd=A?rv z6IDV|)Os>%V1cW*gwk}va)7I=jErTcYqb0~E{LAWkvwy`nb3tF5J&>o4edFp(STKu zO_Z&WGGi2Sy2!N^FVOU)HP7j{NY#Z3Bvt)2Pe_Fb$!fr~s3Bl)r;un$86rwxB)NJG z7Ilyke?@s7E$}E;<4?51Vxg@O#Te+F?WNzAw{sd_OrrE^J^h6w1&u?&yJWg+H58MM zD`RejQF@fg79_(!l73!-^D8akggl)tt)GWE=yUmqI1tcUrdB;Dq*CMO$qcDq(PmN1 zhS|}6)7bH6?2+w4hvcGA{j0ZAZ04*iR79nO;M^C6Cys}fTaVR0fBolLOB$36?jQ~= z53i>2jBK!LI?i8061B){UF(?3n(i=^ONKO^B1v#ClMX4XsTVVHgGj=x?3-L+jpwe- z6JBRpK$9^Q!FH4BkAXjJ|9e6KLsMZ;1k8g=6o1CWd7Dhr=4kDiq#oN?=qFw}09G?8 z71bNr59o|P_LbjvRvii#aV}?n?mCYf*4)-pnWS;(+OvN>I`|0be_O0^LSZIx05SVX z!?Hq+#+J=~U}U;vx^&J=Fly*hgy=abkLz zjgV?Y85VR3U7F>fMWNo~jC99j38W6O4hTJsB-EKsNvFiXRmiLhC{bz3#MHphco^E3 zkKdrSYpVw(&d0jpo+W|`l~w@cd?13Bgu<8leKwAko{D;H(5FH1q~(#M>1Lsz`s}S& zcnB0vC)v)%(lZ&&p0up&pg^itz5J_~k)5u$Q&<;B=}=&5fQA}OjuguFChx2>zhc(( z0Yn1z3{dW&wG_|?A;VOJB$2Fi=CcFmQ=W6j|6%I?T_#Xrvb817!E6~sR0Ty|qn;5a z(bFT7gf%Wbaz1ZwE7{#e*zDFUKf|qStQYIowwge11$(E%yUpd!#gfgEW(p`l4#gD3 z&R`UZoT&$I=i|>sghh<%u}$n->U2i22c+YYlJsFX)$XMOo6bR@fv;9dn^WwJKZlJT zN|jVQ3-y$vSN@aK#l5U75ezg$110q`QUY7QTh4dk=Kc>r)PsmCnr@z$AYs{TnS|23 z((X+jVl#@}4EYFc!-+tLxQ!0o8pcPsELS#{CN2~Ml|A4{BN63fgLt`Ht zN->F@CuQs}cAT>{b!Dz_sH=sp!T|Rz^%7?D%*2G;{5RJf z%dBH6;Mp|(35l7M2inBJH-LJf8%OFRMVWZgn009-c?cnB>4UQ7psZgPr5T7gL27pE zoZPMcnhSbTr~;lc4%3e%g!O5Ec0U!3Q0^iEH@a?gHAWDj?J#efP0@bSO}-BT*(-m? zeL7FiRP-EY)Cuwq1`vFGG1#+2GrTWUJ1?r#==n^zbWFobQdc9&8(M_Ybl$0Kr>US5frc@OGmmxYr|Z-N!UQLzEqf(su)sM|B@pO z@b%91m(=m~USVpKWEt zpgEqd#FUcOG)2=2Z_K!oW!q z)7YF891;j6xU-|Q1aG%7hO95u>7gIKu%{4#oLIw$qM)-F)bpAvFL}Pa)I>qB9khQQ z3JIj$csRG;GWEcYI(IuGjh2^4XK>CXZF(CZGBP@kqZkOcc*1T6l0q^llM~%{S=hSN8Cn)+@ zVJ^OY2beB$7WI@6n_#V@?lGQ%yRV;I7p4AzP@}By>7vLH#8m!A zz_qT1Y?90X9f}~P!aqu-rDkgya&kA6w;Ui*)F_ELzKwG&H5zWRsns(eK=BI?M(*Ff zltUr({3%NNaZcy2+S5_)>L2O?ob`5k#cg+2AJdEBBa1c`XNU@L`vh^yry!@Orw+wD zKW`WNFd_Egp?a+0ojPGd2#>2?zdn>Qf&m99DGb}&TBOVv2%o{-sR-g0ony5THo3ME zk(C_hKuBby{(aZy2{#{Q3s-F39gQ`vDOPQ^tKS;I4XVyHu_N2XvLTux`7PnHqf1Gy zL{z^llej6O$ZSTTNUkL$8G>_(dYQ|papAJbURczcf3Q~b%+#`@nd|(2zRYwDT-Y4o zY^WDA^=Y^aT+-^YgJ=9rOu4H3HIA2?Fc4Ynu09N!*>oYfr&*Woi6=t;Pu{>6HeSJVuc`n^n!2E8iy*whS2lqdIhEWPh-zV%ZHy8Z>+|yr;wbfEvVqTX0GZ73a-EdP zL0b{jXhuSibjTwa2|uxm8GHn&<5B5I(4!{n-&HdaQ_1P@s+m)zci;VZd##$Ws;Wu03pY;NF&X z`a2KY?M{p^4V~$;iybdD^*z7kT)nA>b-j)|>_LjA|E!H{+h>C45XozJna?ieu5N{} zO0TY(agyE1W)j7ai91YMZg>q`C<&YGjUo@FDzI90siZP;#bj&#W8LkoHgAL04J93P z>DzUrtO-RZeW>xb-KYE^Of6N)4MhAM3$;LaUS_)?Alie;5fzJrg3TQh?f3<1zlD%xcIa1J_j(7lAMxCiM}5O4VDLY_h>xK~{#=fEEIook}+F^+ryZ zPsvO0pt&ZWQd0U$b(5yrN>yaPQwb#Ok)U8Ly8IGrB>qqMCt!alwIuD-Pzae=YgD~=u-867#vYsT`q&iG@t$8&DI1B zFc1#4gw6(@44%J;1X70qk}_$TdPeW^srY#eIC2tJU9ej0zMYWAeoi)l=DIaggaJY1 zG3hcLi!#w~TaSA~HrM7luoY_FLr~h`|3}oj$1~mk|Km1RGtn?aVF!mHm0hS-w9RbJ zZO&R$Od(RPsV-uyIW6a7lB*pYbCy#|2PKEPdJoe9=|tC1y6T$hQtJEk`Q!Jyb-V4> z+{T`-=ksyAKOT?!QI;&dHrSchL#^2eNdF zH4==p=!_jfW-$Q(=ZAURSWYOIY0B=Vz+C%_?_js_&yOG+wCl>u(0MV5dl(xsWpW>J zJjV?z+pAKm5}HMlBP>SoTyXfrFQW<&snN%5Ek?=2RZVH-#xvd)QW?5ZE6YOw5YH== zRI1qj1H?H21Gvkg1Vc*MO13{`s%}cG~IPC1wS(CNL2SVmHUFlfJ zr8a>q2_=^^fUr$h5-mm@d<7g0q0pz-6nx}80AYa|dV_Pu1MjfltX1hEYSGJCdfa-$ zVp!k$VVo98$gwl@6-{Dd$idXjQ-ZsLX{Fqa zl%ED^Kdi03MHTlXJpJb%CvFDsC)Vnl@b(3(Z~eD(3V+%k|MtA)?fXOL4V6?_yQDGuA^K|TPyj^h=!kt5&OT^M{?*gJ z+xA^9$cdY6ddV}#rvNIB=1wKSRjX}(iD1aP>gEn&2X`0&Iz2)vuq@%Cl>+Mt_ONx> zz)!!k5Px@ix$^9n4a#VY-qRa9?ixTYgoz*70tdUa#Lp4f8a z#cl@LG7!)qz=k1`AkA0zK-^}3XbJH! z+xLX2s$@c``t-0Gnbm))YA7YUf4;$7)614UxwE>+41Qmy)m)`Dq9d?hoeNuM{C4~< z96LG94~pT0E`WBX>qhmN*r?wzRLIt|3chcn_tN?R4z|+E#q|WMpis-(>UZk}Z`$#= z>1^OO^qU^W5hx%JIl+4JH_cVX=8qO~2)`^Va~%pzZ(*#8>EJ{M)G&})X<}j?(3(}W>VCit%alOTXeZIdk*>{ z$E@(n0yw&9Rahqy1{n$&GMF_$vpvVTljxm>8-lgEx1W<7p@pBYP_UtsT_V&ZO{M;j z!9-Vunwj6-J~|W11WGCzb#F7tEVF@m4qsujCjFoKs@ziF$p4#J>sFDlQvjc1Qms&WWk5)(82xX`B-kf*J{~gErw+k z$@ep|SVam0BJd<9)?BHNn29&{0zX@juVj%`d^5bO48#6rW!L@v>f7GdjxF-7H+!CI zGO~0p;-AU?3J<{;KF)1jLggK3+Cgafx8d!HoupICv9Ie>FeFQ9dDWv{_G< z`(kMko~liY3EC7F*Tf4K`hQfNy~KQD>HWF^|IaD!S0j5(1?L;$CbS(mt5OPtixo;~8Y5PYZ2rje}X+R4R|2dN)7iT6ezr&}z@{DcDED z76&S|Sjhko7c6D?c|t`-nO3)>-6})g@L8mGcf>=H@eNWu(VgP;D&Df6lqFBk zFu@fE%nom!WwtvLIm1P0q*@ePS>&y${A7N7!4}Vr;PhRxvP=v+PdGBc1-{d#mf{{q@B&pDs?dLy zojTV0=+UXSu$%Rs*)_ExbNGwc;Ak4$Iq`Uz3KZf52`+PiLh#Tfepwt8;NLM} zt}ijubM1Wf!TH)-4|#&OY5|DN`jtCnY)WhpogU)EDDf)$1chwkCT%E^n1Tp4)zANE zg0DKi?qH~=IBKEGoJ4G|tRIz{v5*LhUJKMJRwo=8{PI;dJo>uwSASo(EOPL7FqtyLQ?u%Hi9$&kAF4Wx&dUw_QcNVNfli3l zbX%xhOON#!ocoMf_?=k;bJx;3l;YcJR-Ja3ZgeDh@6&7OzIG%J38$KoFbl*=6W%e8 z-0P`xI~IHT*sARmhvYo&4{nr_nLS2G( z+%~(D&hFPApznqK*NP7(Ng}L%Z%RdWrFB?m@-8%|ZE_AeNO`XFo@j^6bdn9_QS%K5 zg%HBX>d2B{IKO>2*;y8RovO&xL-yWbaZogm$qwg11YGe|>k$Ce+z) zk_Xh5H~nQuM46ga9t`#a<=f-C>u^KvvtLRDCVTz^bPN!$RGNDn*dw`nec_rEKLl_7 z{}*$=oCm+Z#6RBDd8jbe`<$V}I`>1U5We8#rST ze$a({=sPyx6hs)5RG)+s`kRMqy>*RnI0BJ;K;{?jfIrc!+s?+hePK2>RiDTY`5}FH z@lb`yZ_hXU6Y!4O!c8_+qZG_|YE+%Bn-v?n5cvE~P`QSwXj09_&A%F5{rOHM^}+Y_ z-fUlGC7v)ILjZN%{StGpawPZ{?@^A-^r)m?js0{;%1|3dNAJ6Um-uXt$v@wed6)3a zzrUPfa(QOmbi&6a2}h4S#hPVRxT&+AXc;Xf6^KMm}^FkL&qKPylBRv_5b zihrdu_-X9xA1=fnTDP^xTY^*~w|~GLu#><(G+mftF$FRD{-3Zeo~K|fvybI(-ntFM zI~iS5@1R9F0E?yy!WR8C+jk*Oi*ycayweLsZMxZ`{%u{ zbkuzs3|8du7Opb0<2TT)B~`3EZXZOXS`;zKj}Ir_AAFtmEj4)q9o%IJfb^R$EoE%7)r-< zTt3`^*9{7LQ5U4o)7|m?v-0dFp{^4tV|3Y6eM4}fjnyY1v%j(a)N;uN{GI1nH!oYb zT*`gln>0*0&m1YJypF#7N7xDfH*Wowb|a5mO7>#kt_$CwpSCv$@@>F?*|fmb<@t&hU7d~a@?64ujhK;`j znE6hciJn;jt%llg)}@ADz2RKhffxw;a3KeJY1QR>!v~Q+q3jU?VhmxFy+=UGAXa63 zHOL-rqQD+{IqmH=a3optWqQStX1+WXx;V84{2aomk6_dZ-zO$v>d?a0 zRHGG`;|KxD{7_Z=#w~Lpz1%!{ZVVt#!dkB@-n!DRrZvQ*t8wUN=oi0pMp#zEoH<^@ za?ER%ElJ#-Y?{~>G8=GRX9;$5JIBI@58H zb&$j(iHC72SRdE*(&FfF{p+(eBu$PozRu4A|7Li@QHCb)|H6F_Pj71brNoM&WtRu! z^-QtcmDhi2@8W~I=uTdR9DQyaYZ(gXX{r`sH3gH|OaUgIRO4f4)iRZHayxgoU?Bct z{d#2XV&i$gBgmHY?^ho;&EbSuG>x1Sefeo5Tp$Z6U_-?l`r3Xw$FC0f>43dhY!iBW zlSL>reG@$|i=LzQO=LQB>Xf+M@T_95|r%IgJvAO2+kwy%UhR=12FFUOrP92 zlu*XfI;L=2}~Wc9~iCng0E zCcCya9^xe1#W%^){tLDGT*YX2R!WiLe<=x)g0!X%DpUEBw>mY7xF5{b2xGPE>iDs# z(-wO?c1k7)Y)W`%49g9(sb;kNw}yAuUi#WGPdXH>r&;IkPfwB#raE>)8J|Vx$2<)3e1sB(udh<7Z&dv89H(6dAI~6 zZ+sa*teUDWy_w{~iW@$t*dmfXmh)9+?fxt%H#2>ZUGthgis=a(d|$_*nyuUma( zt!*&KburzwxE{+yn|m!r7a@DC@2XnQ=@+xRtpNgg0`z6cg?BmVJE+5mXS%Y1c(Q^c z|Eu`uqDg@gDzr<8!n);{CX#S=eNJ^N=0A)`on~9hxybTpk8B3w*7yg)wSHLRnV|=H zQwNh@q=8+C^#RBqdu+p7|M4B+24P>rpSN#`KfLwQpEob54C*s#wDP{gNoF+s-DsLx zC>txk`^3)Uj8^n_8uGd7?mccAsT28X$D5<9sgzgye=RD>d+ z(?+e%5(yBt^Xn}^C0l3|owOw??vIfF@Z9ONhH1Z_FF#*9{}eL;dinel0uywGN`3Yv zc^^O3J81l61d?(7&(2WsU84<{7}sl zN*8lO^~Mvn0X;8*x`M9BDeU0_xXQZJ-w%C3>p?f;PYOfG-UGmknqaiRbZozk~ zIQ@F%Rpt^85&U6Qcq?^3dFIyS|8B1LwGE#wGD=*_F`vx!n5&7|p?sgHNoY5a2+@wz{Qxs&m;%4l5}!DWDM444oBr31 zpILkn{!x_J5+BQwx!0ZMD(q{2>=h3^vVMI+ttPviAn5YSE|0Tcp7~tEYI?BRF(8J5 z(vuDNa&QW0BqC}dtspH9tMXvC{l~yo%aJelpCGPo&0GjAkD*%E4Qwh71qBG7z~Ybl z;=`iPzFo31z1b!4Y>1ldYCgJ8&JS3(?{h|U!B<}tgB*ujE#bnHc!*z0&yipg+&Z8J zux$N)U1O$41x-N&9bjN}C;iIwk41HKPUpkIpvtw* z$ZTdNi>WGDs3a0VP!rc}y_hh4d0W;v=t%(Rn#tOGd3g-WM<=XlK-HW~DIa#ya;9ro z?)t<<*s#}&#%q_s-vv*2EB(lKP#OctO)NkImP%BKmu@!x&0T5B`@ZAZ1LYc7SOZo& zYPeu)F4fokrXZDu7LhIebZks>{vZrzT-VI5Ru0tWY!_tnJHW1CVrcj@#$?slPCu)+ z5juCkM(VSKt(n(m4aseE*5IXb+01{#CUH1Cc=`#9N#FH!k&G=SP1(BeJ{M z!fA;g5h=Ku+HKrG!4+lQh9c zl8UK5dAyULMMHOEWDT@ zuW3<#VBv8L!Y%31Xtg*1{$!WMx9a#(!#E|W?8dRyjDrl{+Aiqwe`=pK`x5I81oWn5 zas&GYmPCgw=i4iDtE0t@4~xmOjemSTZT0(XdT_W;slQ!K)a8gkE3qbv0r!{R;jdL> z3ciOXNWA2fjoPEZC+jyvqIL+=b&h|sqHA<+b+LGzQXO32-9FUp_3XLt@q^UO=+ig8 zl2wpFw{JnnDms9)8=aogSgm4*0QV1YxugWR+TlZ8jHRLb7GeEv2RUxnvJ0=}by>!{ z!Atb>MdfV2bJcCkxseQWRs3AP`$;eDw42*cn#Yr#?_A7KpWse6%i&(gNq}bq*#pIb z*Sn}@EZcYh4e$umriTgB{DOQhJ(Pg*M=^puy zFkJM=-W0~eRA!gyE@qs$R#T8>e7Wm61D9R@1pPK?8fw4&q(vaIGf~ba>Ti$~N>0yh zDomqV&CVCEG$cN5?jmvP(>il)4MXpiXiZ8EaQ`4k_kH>fnSL10zTc8ys)exY{Sm2i6yA1_bOm+bp9GekeXs#e zRTa&9wEBrjh8_C=#|YIhWZ>n4ADnkh!pOVDA25rhd(EmJOmIyd(H5WlVOuwc_FdD_ zNFOL7pw;^MSh3&B=nQ7omlrM(aSSVsNS@t&36=Q({zm6pJKf-j+jV;I&@iz33NzhT zl_%6r(p-w=!?7W6rV-N;G%A)&;OU?8n%GJ$I|yuFN;CTaH7&)M#p{KKHX; zj6zxn7KK)E+}}dp%=mw{KX!4?8H;IjVUpbMc7V1@#Sc!rP^- z^>c16A#S^2$&`}C&w6t@|3%ET{N)=*G2dL&9O2>Wu5rX(^^{;~8#Cb5V@sxu{cdj! zPv=i_9dFrp1J1eCjksgIo7|op%1`(HF}HPI=Ra>tdr_QT-7K+}nXd;sn_WEw+4Pp< z2V5!~wOH@b{3;wb_(`kgu+@Inab1C!0^j<-vJyQAg{$=a-^KaNpf%lQFP@^I>;1HH zZ4sqgnD^)!F0cP`H#)M3Fj=lC0^pXpv&!UdinD5oKE`&dHSIJ-EG_d}TO5zwatq}+V149==R z040$Nw&c<%wQ0Swv(pKBpE-=?cONcY+gcnK?D+b__GlFnRVB1*+t3gwe$4Qne9pOg zMkl5ek+9^dwU=!)aA9C z+phfL+AL()pffWaoBjGg_r(W||0C$B!hK!$BM5+$(|e#5caZnxon8+m=dr|`nJp-Z zi+Zu;k4JNFE(#LwI;$XMoblpW_1dhVvR;#>k0>NL{HGj$ABFT!mevMd%qGq!2#Q^c zIf&qi-@7gJAx03cK^i;cAlqR6Ojqz>1{vD^ONj%`B*S8X*f%YJ&S&o>Nr)8bEj{1s znDvONEL42iA*X=C<#1(OD1otsd42^37P` zaGCFrE~y01%XJ8MM~JH*Jn`+hQ$6sPdtMj*%`+5N90~wPF-6`RF&B%ULg#$8dJ*!r zg=2jeinO6ZGbSaK#Hau6baop0$ZHs`xjhG=4_|c}o)7qAm;b+A&l(?2*XDPRMA`P{ zkgP}H!{aKUnwcI$_2&4(YyrV)t$?C1%W2H1lI-**L-nXG)bkRZr#_O}C(5oWv{V#_ zYjrB{VCAzo$)o!vd%x&ht+2fAzHZ>CLjjAufrv5HC&FZ85(ikWn>CZ@GDn@2p>WuqWCNvj4=%LJ_w2C3#x8csfdwgU->S zwcE0X^4wSTsJ$hO!d-r-vm=5sZI~mv{*=AS*+t%;)&<^K@JH5VcTd z!aoBJkASe}WxvVpZ*8ioyTeyTYv=dfkB+}70Wikr?xxrOhl+_cxl|(Bj7-SK7iNto z%*>Aii$CKJFf#yxa#l!9BhFgoRBUy#<> zRgBV*e%h7`q3K#NsT<}omv8T~nC(CM8&3YO zzY|gx%LA=6MK(XJ%xHDRr{3h6Fb59(pDGk;$^~qn2<=)h)+KYQe-mK04Vz_g(zj%v zQL4|U`83D%z=`N<`rPY+--|kVvAOP6mCz+*w(N%K#}L z!{dcW1D#`JEjfCvikQy~*Mg>fM(B49(2F`SBY$alK7DsbRK%o=2#lUngaK_P5&0p{ z%N`p+^dUuMawCW}@Q>R z3z}tr8$*t9i_FOJUOw`>J3Msn^mT`fhTJc>N{;ccebyz`REt{6(wPGG5NzV`XaCoi z_x%P7{dd!Igmx;0$?Bic>(T76%4GFt7Z5k8TD9@IO1&>cz?qnJBn*-d0I<5kX+8iz zy}-*1zer?a#^@y{&Lu9sP&?;yrzVS~J8+FVj(+p_E|`g2W={@b#?CmMeNeixRb%@F zqrHlWj@X}tP7)8dA7>@Gnh)p(?$7gS?b=vf*<|lp$gLkUQIw!B`y{e+#2G~;@6IvZ zr&}NNraI`Y9&DfSu;7~D7Q)v}O$-1T10;{_u(g~J6GV8{Rbw%mc8xXiLg$NhC$}jd z|Mq^%{lXtsnh#oW!1~;Ur?p}Z=xmQf_#Pg~UX1kpPkq$)Kcl$-yMx8M5wq!3b~V~J zlL=suuc4^VDgH-^B{zzDn;IfLeDDg2`yZ6>SSt{a@l;Sy2waF%%~!k10XIH?im(^| zOPSXdS?HVVyM8G_6opru2!mXuq}J`57fp}2sBR>!r0pCves>X?V}pOoKUMY?6;WuH zp9uin@%A_%I{TZJV_f359(dRI`c$LOr<BN z(@BWY2dP#*P^Zoqy!w_}BEd#2!eb~1o$i^M>}oZWb!ZVE0PPbpx%H*}DR^jCF;t7$ zMj6;Vbf{uSI(sCYEsL9F4q46MT|_1(dXz3+7a!ZzbRm=tWrs32hR{?>bl-h5e}!3h z)7=vWDgb)-DEBcp>Rt>A)m+)W$Q~ITOQLP{AmF|<4i6vs-_peGL1WiZDYu{l6_rId zvid~XRlOw==fN#Vh4b&m&@4LFx=BZi5;oI38yEP+->#a~zI`_4f3^>{e*ju`;Q;E# z?^w%o&i{DdZ?^)9K3Q%LQr=LzrnEI+mE~jI5Q@Y*uU~6{L2YUhMZ-y;8JL+Bd)-+I z9wADXZ%w&xg2X`eI72EB63P&uSD+l=(ZvPNG+DmhDPUA;3i8o8{TSkHv3o~`ls()R zTh*Tz_~r6>cdUj;*Jfeg%IHeAd_pUCujYNh59+B*x{=EmWe3RFBY;UBR&ain%FOmR z)@K)gTR2*MkKy|xx!mIHG@>jQ_jVj{&{SkZkmkZf`;CUgn;r*mua{|!6=T1YNGo|`#x5$8mFugbkSpuZK!{< z2h|jWcOp|*92k+K4R(G1e9YRw6HP zujo)^H@REt&(1}BoA-WYjW}ri+C8_+GVrrNlMU6hIv(*b3X;!Jr8u9!d*)vx@s+Hm zvS>npr$%QZCMggbk0BI7#hjdvQ5hD5_JZPe0@y+Y%<;zltCzYh(rv-ARY!f~?4VPl zQ+o3S(V=bIw92Hh!f425W&x4==abJRzEO|9;*OcawC5Qhe6$rX|vIhl`&S2kW}l?A)JE4MD!tv{9T{rJH^&~7J7 zN}>OQU8UbkekokrtH75U38o-TB5_Z50umNwKlm9P(F?~N|U9FL6ojG8i+g-bS zt4SW~nrRux{l_v;2X%IG^etsCr{L~Iy4DecY#%fd2HMP_?BOSGK&2~|mA|cghewL1 zT=gYQHKXQPERz4~L;<0W0AC5-C%ZT{bTcz7I(U=tV8SEbOg6avPvou7i)_ABx^51V zN4Zu22|67@Y2cR#a?LIkAJ+`(fgV1O8Q@NVi`2QRhJ8m+juk7=eI8)Y-c0eE#MjN% zIe8ade@o={P!}2Hs27htX`^tgv`u13hdW}7X&hi$bYaO`0^Z;(7Rj{o}^W zjvcT>n~*zn!!2<+TA5z`G&73Zhn*L7?xtSr)^b}!@hUeN5HsUK+WY%R8cE7>`g zn=vjlkPs)oo&d}YeUN>12`{}FWJBX0dMipRFXxTL`|e_7V$H_whjF=Udj)u35?EvN zt16l#K^v7v9L1q?%?b1{z{FH`Rs!i9}Of^N}cM1 zf4^ki=9GKYgxXpUhb%e@4U%T?9d!iO9YM-au-BC&uP~4vEupV*y_kQC+>5jx)mPhXU+0+8yyX?{e7>v_@g`U zYE~hCyY~O?HwnpM*ZE1;tu!?;{aud*jrBz5{daGn*p^$vF{hJ63Iw-q{+H6Q(S@Rh zjngZ=Aom9V%rpp^wE4pYjcWrqWP^YX&>E0te_p;;ky(M}1%>J9NE9y|GoBVZawjt; zk1se`U-e5d{UV8aFgW!SiP#r?)Q|afD}BB|C|k8(aeCqqCW~hesW><}Dc04_-+sak zz7+JmNDgLM=GS`ASc0zz&?}_-Pq(`MCP`q3G(<`h_ldhu_oIgUsI9FZ((FF)Da4&J z;Jbp_BWN^ekeT@m$~^^6Uw{+{U$iMaXLIuxZCn5qv&JHf~;F^^j{Q?{batd}L^eUtpK zx?#2Nw6Jb@57bU25@w)T)e;SAfC&`=B=Dv2eX)2?;F;#-oxVG#H0oBqntLvv`2L|R z4uCCgBrB8t)TQB_)#EEK9Gj}!k6bVuoWD}Tl*%0?D+1#{!1kb$oU*@RHDJGFb*Cb@ z`pS(_t)YWlgf?tnbwNtVvyibbWgW~^Pem|sJt!0G6O$PY9~6~DHeGt!cXh10<3|2F z^2Dl_i2-OA$+~quVL0gbBhHDqPQqlx4|LwQyN2s098)T!NigkY`hQ*X$Cmal#NQMh zfgfaCPZY(fi4BTG8eJ;K;j+HMzVqfOlJE;Bps8+@AFOg-#5^ouju}EFF?jkJ6&qr z1DFzG3>};5Mx_Omeo%}Fc||$0uOWg3{nBsO4nllSIh!I}y&V_hk-{c@m3?xQaCto$a=Ccdh6jHmu37q z@2xiUwc|OlUs6AbtuLL!w&!v`qup}wBl-L zMiG2a(B8g3Mt|oRzy>&O03Be@$@BZ82hUq1qS1nQom8Ur(!?%$z9n13?n@%Z=;c4rr~cDDNv)$b=~0Dj z=Da-C$s7ZcBYCg&g-EWa#LQE1h_>9zbx(NYyFVzmQJnDSn~1;kC1Ln{-^<_%m~|eN z@%UC+M5_}n0rfbl@vLv7MzlN|(V`Fe8Md&}9{W#v6|5Bp`%u!c5KnB61!MPhMs3R( zW7lCv7UmUf?Ba7;)o}|!j7F0rYJQnW2=8ec*hMa~ZiubFRrYH;1aIEHgTj_DEq5p{ z8rS?%3R({D+$l3AHKlG+InF}h7B@0hoPJ#B=q~fx0YP)m{2|UwRq^!`Sh}0vGJijA zI;DP@`xDNA*C#O-j7f6MmG$#ioF{|6r)Mz48IAKM$#u)zUz193h_Vs{rZlY`G2FqP zR7F`+NG&mn7*jn(frvg2X^byz`ib@^S9wXo+8Mvj?`rCeQK&GDF0Q=k5~F-OCR3g` z^0YAx+ys4lz#dXKnNb@%YV5lZ59j+NO!+Tsik`;lVxn|0JG#|_2!Mjio}i`WQ#Tmo zyn0NozOI7X9|Mh{*ChlQDk*aG?VFk>w%1Um7Rwq=RQSuFu_PmLg+DdIVLeT{0+(4> z0HA4VK`=FMls^7=RP17Ga)64R;Wgy2OsDM&}h3 z-W5(j;&Jm_MiG8^BA|W@fk8G@SRU&M_V3T6?i@49{H4UBiFQ_*c`g`i3R(qEp$k*VTPKt;}^{DvL zz|--MYa^xU3S%E`_Dt%0tPn|enh!Uob{fRcCk!=GB2Bx!kw)hijY?Ot1p=J6E#iQ? z57>2)rwG2P#A9oE&X?xQ1JgX_W=ZTGjU})AbBW9EsvJ2L>Ti4T@$J)1X2kjJjIn(# zX3_+&CWV`7A8vkdp%PakqbO4}6@0)`#Ku;*$S2(c>l1J_6Q;)1+i5SUN#1mCZ8Ja@ zL1)U9rIQ8CFG)^MX09zdR!-`7q{e$(4n*Kb-3QQTob_pYExn|eQtM@@;0GR7=jREGFNA`z%b&YRE* z;8;5@d_gyLJWodKO4~>x9Y-;ZM0L(E6i3;ATmebW>NAOeg4l6#n81d{?4#<+C?_(pK4#KI zYaF06%Bf$mkuDlDpSXpT0+vgX!qYmPW4sjvxSZoCMkQxC(da=D9HW#g#X3nDaSb1v zWkYgXcmgnAE9b{%uF3J}JSom)WwUf9ML_((O45?Tx=FS;e=^}i9h{E@_Ki7FDg&$) z$kF^U7yfWQJc3dfjzSg4nkT`8x&VUAq+0jdUl9{gL+Yj?cvKPuDS*XDbHYU{8|8(w ze=_^ISK4-=Z(LOynm-gPyZRmQrC=2L8(C({gjk;hoIGzl9L1REgo`WWF{4Ehws0(H z6IHjbafZ}6EAe83)Fn|va_t1TvySz;jQvuSTx@HAl+<|$EDF4(Ij2_;v1F%PP4fnZ z?CQoo3~Ggdqi`@m>-}XLB1^yVb(EW}OTHM6w1D#k99Igslm7~Cp6)G?B&k|ob%Od4f7x%?UkDG26CPB9~ z*4?+DM~n36;=v_fzogrK8X(pOzqbsjm#VDGS%_Je-PLrfJi#P|Nlh~IsE|6WcUhrV z$?c5Ji^v2v!z0Cs#3>tvCs1OPJTfOhvy4G<3O)ysFV#rFrl**Z4Z+yLfjvS&jQxUb z3{4W|Y7uEdiMI}`k#QidbXk0AxYf>eK}sKL{u(U^TvfDmFNhsKl8uBFPaJGk4r3iN zCvKG+tbwPHja8EbJn$?0p&Wg6XpDx)899W$VG%KDpWRi%Vo)t2pm6z6j(ts8{w|0f zw{UD9Ron%nIVcjCclpYa;uc?+8nEfZ{f?NWwtFblo@61KxT2A2pQ_H9hZ!@LbL;^> zMvO6y+$oG$iID<&6CyS@#Z0<=R8tNY10s@g zbqx?nQZmU(6+sb`sC9*h>ftJ=8!nMLE6G4EdJUoM7k0z~(b$0)<}!-oz!Ah-YqTlc z=SzIKMnO!0p+EWi@pjs?(oL%_V}_C$_!-<-JRF@N%M&}MpgLdxg9umXqh?xdEs6Ho z=}il8&=+s3*u$;Pg9e~$`g6_{y9#Tg>2lLH@S$RirM#jN-BiC{iC zP^16@(F*&*u>*tS)|*EiGoyUXu-xWWSLcxAQT6exQx_&wz-fc$U*Hhi;YsSZ(wU=u zPK=5t41U`&4+*X81> zQC|QjPvplCO#1DggzulW*^`5rTIQX&RRmbjILbj$w6@U>gLXKergS@?zF9u(3<4xd zER{LYf%Dds9yccMuVNS&+6##g5!||$E=4ibPGsMvR9E=`1s~#5fUy*$#HD9kEiEv# z7dsGw^T}MKIE57;XUO-PX5%su+2pN;#bZ)h0BG=iHa4ug~ zkAGNsD6@kDvpFR)7?e6#%Z=(I02igyG1DkNx7M?66`Dx~cKC^FN2XLV3hUfS7CDWZ zqEJ3&blN<+S_sck8jr&0poZpAL#H7+Tuc>1s$h<})R18-XbvEGp)Mq+%4(?=78a4F zhV&oh zQI46*D$I#KWf_x1FjN@TIEf4Kb&G~3l7>j@bysR@4ycD#xMGoHWXjs}^9oI4m}F!o ziNmI>GT;eZC((NWQODX8z)xNk1y2sRyW#=GmwLFi$&fn2f_JjJ_)~F|midwBU7Lm? z_R#y1^5WK30zZzLDWyg^D7#wMsfX1W>}-@Hm+51g3gFmujGaILjWJYW0ggqY{drfc zrb68Wsc`3#kmO{3!bV>TTla&KM^z>@0*c4CJR!QeL?AZ^@WOIUneYZSLF!H#37vIxP+k?C=AJ{5H^`!iQ5%8rsbXq8CK}x5@&dWA142kUia+DjkvK6K{J**= z#}u{L(cE^XEJ{0Jqj152gaJ>+A*#@YQ8Qc+Gf5^LmLW=Ab@d#NuBJ(Cp}}AW5UE%a z{TjA>r`J&Pr4uByfaqdI!rB_5ZsabIfj;|4B)I7sXQLr3Dqn`JZY=I3ZWMA#`zG3K zY|0V!*G*-r_$5_Eye)(&n?DwjhWHxccinL|?1jdi_}aFmBvW+?0&h0a!Og1y5Q{6O zPL@P+Agrwnzu2k7v*f_d;95ON5`V0G&2B(3x@P7F5haHdC!H=U#_d!dc1 z8kWYhI1uFgMP5~vHf9v?yA4H}S|ji|UgxED`E;K}0z?f$t)B%o`29lv$$?ic5gE+y z^d96BkM9Y4xN^57>z1lxPwbV#pksb@XPjC;21K3#{#b@@s!KS%Vp_!k9G3Wq1!5jq z8Wj(>hy>l4u7E^TB_6L%i3!p%+<5qQa9Cq;e7Loe4C@e6r>4j)baHwbmD?nef{TDB zkX1z2ok12+@uj!x5%_p#Lw#4B6*Ffj&k{%$H=$}n!Jxo4r8gCxG*$ebQJT_VVp2gy z9pC)*^*G88upP$Vj;1-s{LqPlXxM z$Ba@%&gx-8Cn<&P;3Tz$NBCeCjPfg_lgWil=@$s(iF<4fh&3paJd6qm|KwI(gjA(W zrfMfX2U-YGswy{BA}eK_l|0Y?XljkkttMblZtJ}(D4FzjPLlD}(hRz^5?a>158}`5 zS^$5Z0hj?N)s;MjPGht*(8>#Wx{M274#J+QC8PnDq(Tcl?iI&Ksv zt!IeSMah@6c&Y_XV-8194kl4U{CJQ7fEiEVh^1Ju46n0M9gt}e;DEo_H-q7@ zp4`YORHm_*YEhFZBnu9cNs1RZj6j@G3AmT8Sf0VX>uOutbNNvZ;|lRA@~@5;>#_j$-|mB#=9aJ#`EB|UH60A?L*NUIX2 zw|x{D+G1RlK#>8?q^_7qByx@cv?{$|3F|sRBJ$9XcZrcU%g}p8jWxnamrH2Z`g%8{0Ai{wG8+3+U z9_54X0(5y4B#R-{=p}})0E<6e{Q@#BFXkAysz5UrG!5P0r@?raD{2G}oIbuqma~qV zTlt&Nwk(bZ6!xezx7}GwKm^i6Vag_iXV48<-ckrO53-@$2kiGT=?nq?&s- zo)~II`sUr(%V{)}L7SG8n@5iL2hqmV?e+0z4hh$^(|JuHQ4A+?9lVCQqWr|psBZoS zh=<7YL_!R%3ixID3lx_Luj3DQz*mA?RpM9Oz#0r$B;5%UT8i~mH=?sjd@kS_mI&ea zI&r`P^hCS&L=pIJ^-B`cAYZV`uHjhSYM&UDE}HHp=HqpibgrNAVdlQw_DY;ycmdpc zvJz~Lw%o_HkcKV)N$goxM-NvzBS!>PDtp$%ug%bfJKBklK$6uB0X6GJrZnM!M9Rs# zYLUNOnYcnR_yCwU$*@jWx|5LZQX_|ATvd`ld@f%n@dtbmcsV77gn41OE)66d2psvP z3y6x9Kn9J~G~~ckXlYuvy-^(;P9Y#gSd8O%@#}Z9f`Znwe9uR`569uOXw~XFxeo@%c74*4wO*=i@fo$9$j<2H?=}JoEMqR|}p)CX&ws2loF+I`6n9ueXhp z2pYmLfiPtytPntCs<9%33hpm*yxO>hZpKcVKq^nw0pVCPP-vuf% z8naB{rdS!T@RWiM0Z(zj06>zxKvck>ldKDb*KNfsatA7F)az|0@(9UsS|gYY=gjqh z{Z{hbv!VM5oNSm-(e3xe06Xq+R&QhL6xOc)=dOfAm;Y^b;uct~+{!HqfO@@%;}kbn zrB`D?Nl&TX3K4|UND3$*C)qM7RoEQx=!yMuN+MRA#V&qV?f{oW5~st}L8BetiN2g7 zR!+z*P&LbAEOq6@@NKk^^jT9f?wA}alz=-n>VR?s_zwD1zjc*+OPFM|u&Cql6|HIu zhP+vUM!q!fZ;aKhKH@ZCUh#1E?YcZ{|E`vTxjYZrlE%uq`V#X)B9BG_#kLkpRUC+H z)}@Cn^J?+D@#tGg+Dl;jx9+i3*KX26Y9z}W#1Zwt1I*3t5n1kX%_D)GaFer(4E~_$ zTGwgW`Z^+iUS>D(C*)`owu&xf&)GM`Lyhi8c)D-zmvvyjq+yG0 zs)JMcl%}mv-(>~AzY6DHF*hVuW_4x4n4nR-4(^D#Ieg)|tqfeUoR&$qv*8@JFzEkC zjLTo@rO_t}wQgb3xJ!JP0R!AUm>HrS{0S72h(U2Du?$zg2^hoF)&bO80EUERCsu)C zS%`FXr8qcAtV$R{_d6tZ`hO_t^ZNgve)q_@o5Vg|MKt7cxt{}z!B|Zvy|k)x&4!An zYHcVhv)n^&Qgu-7Iz(18_KS<~e2vqFu3TDfg$xvq$E)@;u_$>#rOOg*YggkjJVZwJ zijXEmFJbkjqEp3cD(ze?B}-P8HCRe#-4k;gxawjQ&xx)b1o;S{H9#SVsuZDUBsV*Q zVv|snolMlTtF2y#$Q&W!Rl*5qE(r(_A1ts97?18N2JbU6hGU9O&l$cTn$G39IQCp= zWiMfwQQ*QYb-||6MktDw^Mz?FNzLmxe|xT06>>tNR&Hcu{|Hf!1Vw11?46lAYSh_74XCm6hR?=dmT0 zOsR_78pNX!94woFHIUF%*Pqx^l`}yWHPt%xw>C8f14m8e(!>g%i??Qnzp7H9%ek+O z)s`ac;bAWNFYFG;OaF7!N5~5+MBLPT*_DEvxy=1UcRTDS57@&9=iUhB@-b+7rB-@F zcbIwz|7bJApo_y%)wB_A?MyY>K_;0&z>#OTu8N%KQWr{=EYFFRDd=_+wX5@e=`aHf zIHNhBWY|N?0E4cdDq)otx+tnH^$&r*mFJU#@g%?ULm|UaU+j?A$zRu=+v&mmkjoGt z5FHxa(A8MLaInlu&f&ZE4ZD1iDdI!GvoQ_iIw#;U0mCZA(<9=H!$<%H6f1Kui;~9l z{g_)Tny=Q;H-4nNcD2GOp098e8X5)XkcRcXjv>4Lp!EMda%`+@Idi%)Em;r_H-jzi z%1jb}_~afiRKv9EoilBB@NKprvv@g-5g-vhFBt0S4vH8<44{!BTEp!`we94fw~2H`%)@<#;>txx7lZ~b6$O(8#lMx(Ey!eHlHapu+;aP z(#miWOpzznvok!NjpM$K1s=$$)%$Q`M=IASLq*J%aT>OhHeU}rKefTSaSO13b=6D$ zbAVu&yX|EyII5^P^#N01!Zshr4DQgS?I}zU11F|b1RB9o2g5Ff%~TFv46G3wpacn* z4zKJxokZ*I4Ldc*ZMtMF;SWaTN*q3?tOnWClm^|g;r4P<{L1%o!H-V+s1gh zMmH5^%$9pvni`i&MdPg%ULH?{;!9H%DkTCcL?2qWQu3Gz(P27seYpVnEI|R9hZ!uv zM@Eczy4MbfAC^%n;-lcIO#mQyt6J~{r?Ho5lUNC3P?fq$7HTCEwd-L^C;*q9;322Z z2ymYwR4z4K)1=TTjSmE`ZWIiq>Sq)VMID_)$5RL>MoDb7MJR*2vUhy%kq-)eD3pP2 zA}Ns>mKbK0T}}o@D_=J~r(_1W3F1q8QZ$L^+t1T+_X-VAJK9dT;PfQlva_2I=Ijp& zYyWoc+vDsU*hlF%++Y=_%D|-z^lR%4`um4dL>76NT0V^hv6= zeU1->hLi*oNg1vaT|l?b00)Zz%pRh>)noTd8F7--iAKk7{?sg6No+}HT$pk*L7s!$kXGWZ`9F&T@BVYtLxa0=b=uE)u&=vy=La~$@}!e z-k%a*U=7?$ePi&A%p_FPr1#u7Z=#by0F`m zKQvV<7IrsznrVR+!gw^8gxDuPphcCF#NZ< z8}&yugRRg#8KR6xk$HH1-8xyTM?_Oqz+^HM7Y$BXxHy{WIW3&nHY5L!;I{p>0!fJx z%7EpB5ecu4YkdgfVy|dp%rX?c^&GWO5r)jO*G|2-Ykl?IE{4|%YFnm&Tz26+3c5cRG6H!W5zPdQH!&K zF4!wVgTGv6ceVKTyrVcF3R+yw$P2WDAVsQXSUphmA}7EWF9HrXL>d?D$58};{XyGf zl)@Cc_tV_-ZPmpl9k~lT4|GmpTq;+feV6hrn4KxX3kTkPW}ib3{-qk42(7T@%LBOY zHG`L8HTlTf4bG&>OSWMQzE%j&hqcTuq1Jw)y#RPl;6}XdY3d zWLNo7pe=`xsccV?nsx*?qskJ73k1!KqEB$aC@ojj@y<(1+?E13U2TxC92Ju6wnds1^v;JULsF+z_OO)32k5A{U& zIyqWk+5KpkP$h51WgtY%oYX;<_))T0UJzlAg*p&pfWv!G3(3jY-YbE_0Rg^c2r--{ z3r1JNx~-Cp8@f1iX?6BX;J|mh?FQsB9~(Va5a5xll^#g9^YrV6h-FMV$1p}%NQ5MDmG-ing6_o`?E&Cz7_kC17T=H7zTvV2`4)GNJ9nSV1 zf_|aFo=4@*-D+H!+wFn5S7N${aC?J7al0EK&*MG0r)MAM5vVB~&FFEA(d9s34auO= z5}bm^_vKV$7bKutVj?yaVg?}_Mud*Yi2EVRz;AP9QBG_cXAT&32?*FOr)2Oz3c$b1 z#A$F)gCNT5IcTfwWI!q}09QPiICL)Ai32giJCH!Rj7AYasbr1??F@L_SXd&hy>%J6 z?LkFw@b}M#y@4__i1^2YO47cE%lsq*eVm=nqyW_7~8th-{znG>uUHd^+#X8c<8iy1_ zT-H!I-)q-dg*Ad(x;#A(b2>XS%plsrJ=50L%G+aAS8mMpT+8GfHbuv_h#P}b^U5A1 zQT-ek@HzKuxcQ7fvftkJ@JZAe0l|J^M}5=7@&XNb3;y1ypo|;k z+3`^$B~oq>70s!LP-q4bTMN1?7^E$v(ka6hbidHk;sl7e=ZsDX!40C%oP2Oo`pAWl zIg=d4@w|42dsrTvLB}e%tfHS9Xs$bW(5@EQ7+=Nb?5T=CD=Ty@iJb_8%14NZqX@S; z7r~Y~Ue55M|6BhBRuQ|tAUijo%lQ&I@Xh35t6uJYIY+$%$`f^mG`cSGFJ8Cy{Wd-r zRuY=h&d(2)p163m&|mPT}>Q?jq; zARyyaAvpj+L;@pKU~6S@!ZdAQ?48-=G=QTA>xqazF~I~&3bbz#M=N1$3yADhM24#k zsAlXuU11Mn`wPY6efGq>Q+lRo>G3L0mC%bU>gwd7sxk;T2K}D@^|rJGXD~?gz&KT)3dt^m{1Ml|(xhrcNC;s?AMRkgE~p#UE|b zs(6&kyiR@0z}-}?)rC1k7HmrTKLD}!5PY0L6zSuEbq>o6ye4>%7&R#OL7@JZDQqZ; zR;eImJ#I7~orTy>9mTq1suOk<^eSDrChe?zsHgsZ6u~e!8^M}m1MnAMoJIKpm&sE) zYYsddl5$JE1dt$G>e(ZpZ)~IIo51AFnL(^7F$l@-D5yHvmc)`>K`}=-VG}+gK2l#* z-iWP22pPg0Xeed7#2Dq;+6D}J#oU#hJp`8s464Y(ev9b%748Vl3cVnYvx^wgIA2nUms$^Llb@%bc z@zO4bPuzVQ2yLHO_rNVrbr&^n(|?j0;Q1|yR0X^xc6y-8Fb@7Q(cZmAT#HT5;!9xB z3d(}*Qi{$&`dWKV2eavL8W}uUA}C2P7BEm)fZppA`a~Ia|QL1>z)urNJB}FCximYEBWOK*~9Eb4f2n3Ho6U8}e?9dUh^6>+0G|>97x2;XtZd zJh@1fUXvJM3Ct7IO@!``{hLD8=Ijvo+duMw`x6jZv+1m7J=);+maL*bf4CpadZ0FQ zpGm1`NB=oWxz17Bh)`S9l|8A(9}~KtizraK$Je64a>&Wy^5K9A%RUGkFcdlpT)z;> zL@fj;#geyMu}(nifh{kegGjymr{73tOVW6mF0Z0-I4|Pv@ZJ``yf8RYo!lRpbMf7#JIc2w5}S33xs#nx_A-Nkqs z#DTB7Rw4d-gj-U~m2#-r9_LgN*~ZYZVrQ~UbpkF!&rWDc8n7B2D(4Col3O7AL*;qBk}}GVkpx81#r+< z_U|K_{k`yIdk7eSG%7n4N3e_Dh(G(#WB!Q0+BkVf&f1Mc(m!Ja3o&*hjds6R*8h89 z(XOPj{xidYn3+?5dT86O1Wq<^X$Lzk*k-o-%vC+LHed*6a$H=zQ`fQ`dw(!cVBBb) zqz>I?UERv9T|5c#p})TkJgT6R94oqE(jw`n+Z=H|n{KIws!8$7sWj~|wy^n*>)uvsV80VGz?Svkk zPd7a4JwN@bz_9Gk7HJXBL_*3hG3zD0OJP+!l6?9sKy?Yl2~02yQ-TlFj31FARCx^}l=fZ$T*Z{(oWv*7N^F#&+y-Bots< z4(p1st7e8V#5xcYQn6EDU)5_Cha#8i3ZP?%S7~NH z;{C_9clwESCN388w2xm@X!k{EYqEfs(&` z5Z3l5D>o_ZawuXvGaw%o9k<&51<{TUG(79Gd8ThG4P1aVowUl%A`_q6>yc#kkexDY z^d@_zdSE#k=G`*7y|o6*FSzs7Lvuk}J8mswwM@BD|8^7+*atxmo0+jp zfH>G27?6nj$|R0W^_r-TJdX>emQo*PWgBF2PT1_VKJw{W!nybVi79^>{1~Za*lCOM zQvWW!e-DoBG~s^v8*i}c{c-y2LL9DJckdQSvoem$vb1R%Y8G69=8Cz#AN!%gjtNCn zvjYixS+R=mDt@Dd^|6ifX+5OGPPc&kk>hZQ3Cg4>97%V@lSp_&L>F|}-BYKd@A^m` zrlK+CoN2d;LR?J>33x-WsFnDXL+Bu>R|P8ac6)=QNBQNJ->UhgN{jdE7w@5qmHsL! zIdL3Jsg`@b^oepR>uUd~U>9C-ZmcIeh^Jsna1CSgs<2?X%S{WI|MQE>({fXQuZtU0-G%?SWyhSg zd>g5-qg+l<@b_!~&$$FeL4=Zap#Qs~WF!CRTXlWLZ&d)G5BfhJ(9_tYD9y;gdx1|? zKA(H;jNG3Q0&Q*!J!L&#qpRgEm=+J-t`xNux|Lg+B23BHAvvvsj-zS10eM~qF?Swc zIum?D9BOLAKzf7XC9dLjHuD|RA*e)tsE9MC3#A_q_`p3C`ttB+Qb^vNh_au>alUl# zq|e9?xkdHhHpk$cbUQ@+vr3_?CAB;C+@(_W2H@8OHnNtLdBOukU)!a&i&7>1HG$gF z#HIsZy;I5g_@>iwD*26jii%3rWx3PgRDJsuj`cmYr-gXl=ac(%Xk|=^*>`THBPmEZ zvcv46@<{y@CRL75^s#gE0V2wt4E$xbpv)V=&rubMZbDlNNk_i!pjQe{r(ZhV_BkB4 ze^POalQ12LBs@2X9o#JH7qyL*`aG}pN;x4sxGxK?_8^V$#hY*a>RU{Ht79qWd1Tn@}+%59Js>(-WNa!NnwAHik^PEd5N8R z_O0o_cR)(poSo?~yRW%g>Q8zq=eH)cBR?>*tH?l(^S*j!ZgUUm(}8@?gWo@VKyNzh z6WWJ;c>-b1Y5J16{w!7_d!qfO5O*b$p_+9xKJ4XfzCh2336$A!a8oi{u7IY5Inz`2 zL}j!^5{qV{EF_9@`tRCIqv*2+LT|Y}(IDO`l=gn?fOMK9_6;IUs<9))XJqsc@MGEa zF(x@*bd>ID-No34qRePxQlEoqcO;}Ro^N$ko$~K{7>||rE!2Vt=FPME|D`YQil*6 z=V8Z3{JrjY^}*#NACzbYK{S}BdzbTwal2eI0(YiHPd7dqO(w(@A5*UT3AG15A9eu%OR%p}N4#_H88zL-z4HGw+MC}+50LlzD^!jN@=oH z#=zmCGO$4YJj;x_3B`ctfNeEmwm$3czanumw&U*VF?NRQUW*~mFC{ix3KG82`MU^J z*DLc)Z`?_7hv#dZ4jmY)*1+odF&Kb%H6EJ;v(avu#M06iJ# zi|Pg=wGl?yEM2oXWKW%mc1Q1<{Rc#*cizr{~E7z zFZNS`Woo<8?_7?A--}u63}Iz@eG^^@aE?G(<+VZZO=+|Z8~*SpZ!65#ux_^gLvD6I z+fw$xBJGv$K}4+p9|!(n@`2 zNDDTGBmDowvP_>!cO88gwIFZ3lJ%chZ^X!JdF#cH%is7XW-y|qj2871NYx=vNv;f^ z7EsOZ`ST8wODlzq*Y*h*+TW?Lh*7Y`StSE1t8XFj$~qnFT9|o$yeKK`k(bKv)XJSh z9WLqgVM#Be`=PJ_o#mr}hy$zQFuH zaK1?T&c0z+fhiu@)aV*TwMfXJvO^6Fw|=xEb1z!0#Let#ymPb3(#s`{-YvgBhF6np zeW&U?LzA029z~zIn`2P4YU%%BY_O4hVa=RiQzsQWk0_AernfpHLT!ADPOf(T@z=QZ z+}qVXwaXIOfA=*0wK;g^M#-U*sYlvnBw{U^4jcl_P2sjenA6|FFIed9?$ywfpP z5_q0Pk{?~4?j9Ieq?iXSK|2d$*iA#$EOjM;v%r zzQgmuu{Q1BjRvYCeBChepo>9FRS{>dG%Bc=1W`)}MIYM2648lhIBTCTl7);7{9iNc zs6ezB?OoE`#2$<*f!&|bzLZ%vvkJWrN%C<;IQqwSWipi`!DVZ ztQFaO^3j-7wNP=<<0xS%>qCX4mgj=VULWRwPdZc=MwH3I46ssW*Cux`YtKCQ|IPpL zvE=LfP46@%Hzk-8`;i2xh~O(NLkG#!Bn1=mm~(NjUS48Y5l2CK7!jPcZ+G~6aRERW zRg|vT^X^d?%4^SmV$tt9^|6_lj=Pt}AT*v;%E>J7L`X2YK;^{|zCdMfuE zZ2}@Qq94q!-z44t0+&ah0f2rbqRl!C&r=B=+#~5)U{0oKb#9vpCmNNk^7DOl=O3vZJ!fA!OUJ)kbX>i5=i_}9{hf4!34`gRk^?6oHq41~ zg6Chyb$u@nTrYZ%l#`9bb4|{wSL*sAfKVU>t~#T9+|WkI3^3d$gfqnjlKwGV7r1gMk4aPlBtNdTl}i?1FteFB8}cM!O}!o$~rr<`*I1 zWcr1*$(Zo)Ro{}h?np3Ym=$7NBj)A45^L@v0`e$ZaWHUODxQ)6Oz0kVAfT(JeytRw zLIo+iep#a_$`T(;hAC=oVUb}S_F~OHU!}XOzg`MT$87I4rvRs8Nr+4GVA4Rw#{1=< zLdDkn>BY+{)t|TVj{xrnc)%dXK?UOxQSPK0){)sR>8Lo+v^S7*vY+0b%J_mEuPlhW zDQPTnkYwv9u7`FSndTnx{_|7uc?ZHFF?>&MFZiTyb$@*;`cv~q$ZOQk5)_>j^Y_pA zCaH$)w|biURK`0yg-Ic#N1l~l(22`U6{wWpXX{@04Y{A!7zG}^b*5wZ=zwfrSwosf zTu#vP`%pp`%g2gkZ{ISs&1P^D2enU+wnRJNVQkJ$_P5q+C6f*EWs_lp#wn4$^7;Vr zuY)y%DA$sBpIUJjC*T)9<1U&N@J5Ntqs5cufUJ_l)U;hnIw0!X7uKD4`UQZrd`$h2 z8meL&lA0!_V)p^k_(Ys~vUOc&iAM=SKnpoA3kJ1X`gy2b1)izuVHhY_;EiavW!rub1I)Ku4i2` zu`N5`lhgzMjQsp&YST+VN7OwFhqv-!_5*S?}vZ8H$jv$5fvGF27W!y4#rZ%H*h3V@&g zyEz=c)bsGA1c*5RmhQ9ec5P9iIX9XbtK$YcN%#GWdboW;9KA`usE4|kySQLG< z*H*#kT&wpEK0PAugL)K58wn}7v9nVw`!6*jLsao_P9pbD7*AA zXg`5GpjS1gjT|=(Yd$^s`_6siri6Kmc;DH7wmF<7QsDB{dZzWDyJpV=4z^i-^H?nh zm-kwSPI2-G##V9ExwD36>bD35qs!SUZYLi4u^a;p*R<&GRtid_-TTU+ze{wGJFT;p z+eF{N7cWVpBQm{ZX4*LByeF_Yq2qJW)F7D!#I2?E==wyE*9&wa^J=!_Oyog#8O50rsd!-bp_? z9=jtZkF=-f$+;JMmm7NpC5T+cgM+iEb4CH>f%HE*>@0Z3nKr~$%udB}CG#UcV(X7Y z0!B@<_NF&@j%}}X82I2Oq^pqsYzxzy;L;T1|CYGQ{;p+DAHC*&@SsG1p@b}<0sCB~ z7SQg3pXyrgt*U#-svCNO^NuG)8gPQy@XawaFtuM&2{7(qzUV9~8+5W~On?azurqg? zIsrcBQ^u1XZH2CS7TfcwFHqmna!jylH+%`a_X^y?N%A>z!2n1-Eu)Za&;yes<(|~@guSUGxG))hZI7`>%tTr3)lIjnr*jq10(U@m zp~&f~!FU&jUEeJzlqyQ#^-;EP;*R6@Jrwe+yf;~9EizooOqoZPO#ZRWG4O*Nj{Dl^ z-CC1T=R9MO(QsU+{z^E!sS7Z&St(&Ref3n_;<)N#G_*O6^74>YZ7@w*aOqu|n|wzo zti82ec3;5{lm1wfygUO+PNv-W{T3IL$)JRqS*`_ZGZc{tJ6f_J*0dl)P2c<$`1SKA z_fi;jDpZyOvWd=o%<_5(PQVENubK2GF4W*peFMcBSDxz8Fb? zM?5~5iu=g=(#%mQfSp8_qs*GHzCLylbIT)9&e!x+5gERoLm&BKYyzNIbw^6TSw#e* zaL<}>Sd7rph$_4KwLG-pZE$wXn#Z*pji*Mgwy~^m7x0yW;YaWTBd&MVFCWsT``NMHOktLYT6Ix~z7Ce!45^(>+b6X)9rN^k+aswpuXlos z&I2DG8ge>u9;N&W+~bQLE`};Nkfo#|iB%X#OmSINv2bwCl!y#bAch3zINw!Q3q1=} zHkDcnQNUGDi*Ts=Fj~G|RXy*mhV=>8(Ot{UZX^_8u4LaC;Owf-z0O~!cusFs;9H$+ zmM>Qtfv0~LL(Qdns06tG(fi2pg0oJCyC&SFok9jz3cof6oVrWX>9vU0BPmJ3QG>8@`u?tRd_UE!|p z+BgTk+)rfofK0gj$5Njw8^;=TLZ@^!Pf#sv1Q~E;(q+_ki+B3fg3Ap-xLC zJB#jv@`a5B2nG%?mXAlgXn@ZjTlKLvXOcjou1vn?h1M|g@ykaY8I^Y}NRBuY!r?1~=!4np~2uh95XkEk>^ ze7)(m*2F5=#T7r>sg?bSrLxH!l68&vtv}sF{2(qgjbuig^|ZD$XMc|AaKCxqvez8# zaW7NtGNun3m^_e?+Aaf&`v|?Oh7}^}K_CgOvh7rUiZ6A}t;iVTIZfDA2?L&E3m}>V z_JGnM1tuOiMie)aO62R}-s@WL*9iZouknt#1}tP&sRwG=zYrV9H0i^e5^$FN*=ZO=kKirp5QV9N?Y6$!H!>x$hnvlohjq8c5XXkSQds-Vf z!Pjb~!m)1P0i%JmPp)F?D7lAVQqan)uJ2bmG~k(rzXfPeU(2^>BC%g9nttiW3WjtP ztdAUxxP7=0CDud6J{$9tW5Z9_@N{Tgn|?S}Mwz+oRyHLYzK}>fecFIuM@x6d8;{rC zD*kv#-?y+yHXp05m$D)UHcAzv$M72xzqGr3@Bii5lG;`O_pHK2%TdX}%zXE%8We#G z%g=tN1fBIQ#m}K06rd5gDTt;{-!pBma%KqUnM$1IcUatUBP0mMLXEXSm$gq=-gh=* za&+bDH5pvj*SxG|Um{9P?QM+ppN_qWu z8_Yc7YW6$#;FS>Lrif=WPH<_R0doq9H3Bawj=pa_dr~t`cdK8Al<&yf&ax5`p)pYg0D+u1(#&M zb#NSPAG21^@cLnnWIi-ST{4%(e(h0HxTI&H=xl_`4P4n*i()bJDGplhHtmt>E8Q$?HxO=roxUGV2Nl+ZbgNcbR zf8P_}_ftWmLlpIWEi@tMnJ3LW?<+t0e_#Gpl~jtUbRWM+E_DLy0C;jMIwQo^hN;S` za=L(zL77Lf^31skyR~j(FX}uoyq@k(^DIf=Ogc zXF#kt`vf>;&E^zmx)o-Wn0|o2M?)WJ06`cq;M4En*^(x7fL6xP8J`C!5>&;ui;}Es z;UI$pgskXI;Sa5h)wOWLf(r@?aK~u-RY+W710c(E;*#8}sJ~l;)(y|-TI&_mJe;i= z>O7z*9up0FdX_Q|n~z76OzvrNjw$+ zxJ@^aVYAstRuK~CosaXT1*9P#<&#l2*|XwK6W2=m?`oM`Fn)Wej@U981ze~g5X4%7EB~wBmSsPWXO{DAYyEm{Yf+7> zJD(Y>6%VPM8X|}z;Md>?tQ&@(7TWkA3?5&fMzD9%mx?WMAGrrYT|m-Y6jcHN-1gZM zOmHiaLxR=>Xd|(=MNs8v>9YMbvma}pb!>gC|D;iMN5K6#wS+SYE0YUpN;+w0D*_x9 zzj_%>p?tV?JK*E1$ic!Kq`S=Cr4gx$gOzTmrKwt9P}UAX>H-|Ymp0_KPr}A;*ErL^ zWoco*9p>AO`Pq%0!u|Hkl(xtWFUTdG^P3>H%%PU;tLWJ6VA!+3vtH{|whJ6=o{K%Y zmk)MNqUaEy!*kx(H>4p_O7$i_rv7 z%})S!PXvGuezFsRWzWC#Mlj|lp>bXddr_TF8HML@-Q5q4!Si&Tc(G`mD5-g%*MPY! zfI)jjV|GXpEuPb z)-sZR4MQ%UDDxcy0dtSDXE$yM`i*i^0qs$l)dTh^fy~3B{S}yWZ_R&Ja7j7|_u_JY zE2BEsT3uh`%nLSpwkXX>c1!qMXa*Avzp(Z@{bCDdiB0igAFiV2iY$sIrhL1Tm>ZA_* zFLCj)0!Hyo8LXBF)?Y84aUy%7v8dN;BfkO+(Z7gV}(UJzY! z;1XyS&nWbY0PesleOkV561!}j?^0g|3(!hC{y?SXa`^u*w@@%gVm#uYqMrnZfW|Ud zbaoTxoh#HeDIkwwXoCDN>fj+zc7=QVn`(D0)8k(S2MV{oeu2DA3(YT>&*m1)XR0dq zn>zKPLg{6&Ij{Z!aIzgB+cL;Z><|K!Kw;fL2*Vq&@?rOtwNZgRydiLFV*t;{cCxDd znLRGeidK|UfHJDzu%hN0?})h@@l4U(;SoR&AtbsgVXk)Mqauy^pOAIga)!Zx*-RJy zx;;sq+o+#_#rwN?_RXh9 ze0DKweg>kUv=&%O1Mc?4XM(g*Q3F$NgwEx)|Ff7Yb3ZZH9Jl#li)lJ_rg~YVy`e2q z9aYyAgl`XHcD<;awa)2R*RJl$EiYcY<@Y$X6BY^%z>-CtIVSBmTxqc7jbJ7Y^k^#1 z6Y+E9WTGBpJov>DV~aN@nJ*{;HLO*thUbumH-C&IOYRg@_z=F1s^HB?!2F;G4#cN~ zo>Xj0R*)}~s>OEj1*LSR&mueJQAF5$fbG(mCK;^+*{Z4$hv|o! z{*!D(#>@<3Z0Kv6K_g4$1+wC4|X4JEZ)5=SQ7u0 zMhu^`7Pux)C+w=C8Vu~3w$5tMTX(cy>5lg8Dqu4CeeAvY0bC0+j!MMAn1Dj@y%N!k zVJ~6+H6UjbPE2fsicni9fLle}v89;ZOJfM1*x}LAB6Oi#D$6k) z!;Dvnz*3(BMb`$3o&wj7@2A@M47M<-!c)P30%IpdXpVtF43k+Sqr7e;$A-Z=LgsF##cFgn& z!QugYEbi0z$}JZm<5$H3`Vvb;&d%U6Er2UG@QkOoTNwHZc!>0!uTwxW;arr^34GA) zs(RGqiQ&J&Y3@`dVkk^NMx zoOAD9QOIMyJ7z#+EA_EMm6{Vbv`0m)s;+=)rIN66(h@(E|Dy23x2?2}tsR7N%GRMI zeeZ@->LDAL@~6gO)qn?tZAgOfHC@j0g`KvEpY1TV(na5x=tR)7x9+L-{EHFmvz-tiMS&?bQ}b--40h;Q z)@H-Vn4V+*v#QEK)EOR6R7PiGQeU{Y^0@61pHQJLlf3>}k8_(1npDFxl|aKcDyKIW zE5D|zlEli8qP(hOW(8=9(DFs?M{N<#y5!FkU6TGTYu;}9Vg>If95=kU`(2W%bnLRHvND8&N-|sDU%7>I;;Z}S zg#*rJs;TpcH$llB&DAl3tDvPGd?Co~nz>by$pWDt*>S3}{mH5}CgfpHm zgL{@48uhi4I4a&zIH&IGw&C0+vgRnx(7yX1`A*rk{?EvjIgWGKdo4BbzGYI>FAI0I z`1u3>q;?oyFnDPeW_;dwJbs(AVIMpz<7K3Xo)Nvgnhg& zF=j)wnXv)|%2t)*Py4MuD-+&}47%{BIhl~^{F&}E%@(V8ku}N$*dmb8vMF~ANh{9U(u*k%yz^nM!pJoo0{rd{^2#aE6!i1FEMk$ zLKcw#i)Kkfun}C-us#&EzP}kz-To7^-x@nXcim7oeN=S$rB%zFD@x?{9JDIb!UV7! zePK>XhcXBc1||m<_Dao5NP;~WNHu5d8eC#;d`a>=x>9whrbPc*@I+V;_=iG2xN2Rz zCCn;woNF;V4vds3sc0So9|J1T*bHGPscX%Aet$kjMVV09KQN5Nk#zid#Y^)xc0 zZxvp5JUszQFE1BSU`~)KYm-|K3JPqh_p)rulEkt7l%=~9@V3sz#N;_9v`XzS#jR#g z*(cNLNBz|6B{J1no)U%?)No=^m$=TZsigBLJz95SN;E8C=M~%P%Z1C9l0RLV^S6At z)9UI93f)$z;CT9wu|-*Nv==#-E;_h<_c5EVSyurtJm6s1X4d%?0$jf~Y7Wo%)yy0~ zyngp8{$5hYm98u%)Z6Vf#&==7lW|zOoR*f1&!LWOW<)_x#xx)7`@puny2+;Zn7X!) z;+fUIQUqJ#pgixEBFrB)r8tCJ>H>>3fn{AREV^ZLIctlYVpZM^QrRYt?O1}oGaBs6%D0?W&nt)*<}wPkFVwDgKIGB{-)= z&UHr{@tSrCxmUpa^!oLxInCqUxdrPei{Z(9t5PCJz;<&uD0RX2{<_@tiQXwNAWs+vP6D@bZF;eaPZ7}-k?T*zG@k3qUHe@TtMxFMxaVv#; z738GK^NpHv|Al;fy|*wt4Cg8hI!EUf&4BnV04Y1_<2SUewi0(>Mtq)=~l;{UC~Cb z2TbIh7*vo8Q%N7W!D#X5ZvVGv^8&4s?_lF})afMsae3@f-j^uAFa+%`FgCh>_$)%=#o>Ca-uQNq091>`0fd zG+;pLi?&!#XK;VZ_*qbb#=Z6e{jZ6s|HMiUT`u~aWnZt6ek%g{4D=UOqLHpQRqEop zZSbZf9@djs-M+IQ^g4TRhW-(oBfc=ul@rC)=KZ%>i_8 zmk1FHTG)VjzhUcwBgqNeYqP~^UE(KP{u3jIjurw9>8H}x?s{L2JUTJ!0#gZ3W^$&e z!$h@nnqg{Y-+`ASz@26-==n#CL8KZBMD4>LVw!ChY@#nf{}n^!*BfnjJ@`yKHf zZzG(*|DGWRu`vroZ56}OgH`vr>XLp!31@?T>RvI3>>Dj;m~#7(p2NxCPuM7I&Sk71 z)N)&sb{~Q+A zdN@qzght4VG0UD34iV!aabj$@B%$PH)n6q0bzP0&-jE4pR`7zNc>hm=j&`+})<$*~ zTF+_OG@v^kx#}Y??1R8=Ix?duk)4k<|~|NlbdAiy(;g zqjR~E{goN6AEc@jX8@YU)Syg5P?aAFMBe|S=*r`n{{Q%A48zPum^3z5?u?QMV{;_Q z+(I!#xwT1(%~3WRo1@U_&K(kxN`)a6lAI+}jwrs4YT>uv|7MTJUhnto{d_%N&)4&H znbWfYJ_6*LfB4I-QYj$|mim^Og!^NMFx0o7YPdT^yg+&ox_KR|19F%lE(ID@B?EhG zCq=wOY%9p|IXoP`5(T~!p^?0CuMuC9Mig;kczfERk(~!dY>Oliayar?22&VcS+Sn@ zV9T_UTLh&$Gu30>Hg5!8UU4K9)3nd0qtZNkl;s5${>q(soLJhncu=J{&{Tjrf48*q zB5Jc3qCf_{H_9?d&p9Ec#i$Q#GHfV}I_j--m#z6SIBF;t$BPey$6SX z<#gC}rn>X37T#B3q)Oq?wsT-pvI)>5gd`)ZA;r>QQ0xM)2@xn5`X~ow^28Vg*P|~f zGO|!EG9O?)sRG$Q?AG-yZK&Ft{E1eCKe9wlRkv)j*yL_0Soyh?34H^hmB7U}Ak%ah z!oHaUWwOpFV7{jdn71v0-v1!Np=l7F4att6!Bg?eG#<{>LA+Z}O_trv^%sj;^dGhk zzknI&8W~bS7sH|Zh25<^#?9#9E(87|X>}~+;qt_YFFz&B`%!yu(;RRYFe9KDv0w%& zwze`oH{Q-faCvc0+Jy1?p+=SSbjR=Qsp9=)jx#Ed5h&jQ&|b7PVgm&=icOXS$Wyi^TUrG);{X>JJ0N-w(ZT|8Fw}Ft6 zmR>&sEGk9rNQj{x_nN-LHcfQGrpmBR2va?a^0 zg&;d+*%9JTSXrALU>9iGb2OA`gK#J4dkfl8(7kph<0Xn2TT)+kMnX6pu?T<6(2J3V z_pRfL@bTvDms;2*WM^#+B+%B2;$Szn*=uM68f5mhO4PG$wH$lMW_yx3L`jLd(mray z{ly;sXc&7EP^U&^=Pw3~*pbm2sOPS4X-j+XwPkczyp1EbDjk!xa6b@VGyr(Uiril; z7|x-Xv8ls}bNu$lXtOe$$qyXJ#^zyPbi*C0!k@{iyu7#!MGqTywbmOweFbqXM$x)# zxsyFUGV9(M{^CYY2ePvl59SXX&9l-huq_4+J3+`egKby*fq^|%^Z|-P!8(Wov3|X$ zXA_hp<_}A{F_0f3g5yun@LAk>`Xb+rBBM9&4hN*f$0hSUmBOa+NhA2unN|7%QlArr zhoo8Qv0ZI_ODm*AbXclPO9`111xFbrjKDaH=)L^0S1dqn9qXK;Ws>S=B4Zv8wcKX4(!^yP_F`}A{E@IG= zDt~NrJr|!(oAA1rE8;9F1!Fstrn_=eDvgPY3YAPi-he)tpF5=xWA@xq7BJx~dMrEc z+d!)*vR)*K79^G$Ajo{5)NsiUiq+@&6}0T|D=Q`YuUch&tFy3o>?`g6<)2ahg`KNx z6BqmAVv+vmKZV3gt7l%m!Q|4kdU!F^i6WgE&AzC#Xj#7K2HvUB1u2p>b%NNIP_Vc? z{j@;SO?+dzV3BAOD!&*NE2T##gJJ_al?&EM>A?gH&tEZHGz$!eOO$^qB-8zybSpXR z6LqQp&VYZ2+VbPZ6bs3@o{9_xpFOY$>#Yjid#gBoFgpX7MJ5H?DAI!&5wca6ccZd;zmbKt5a zlQ*n;I5DrwN1_BFv;bhCtpo1Arw?B!hIK(jpb1;7)2asdoIRG{PRozFKRxOi?z4E5 zpES^+EYPSyhW1>evQiEVi&zvvAtwg5i}eQIhiC{plooL3Met*7nLXP*WU2YIxWDpQ zqFG`@z9kM`83pXAw^d1*{ny^AXH`9mo%1>J<>5Dpm|uy6nX@rhpox~h5@zhS=Cmgc zT4m>JpyALoYxp!+xmX@FAnbr(!uOD7Bi#@m&kI(d%ped>M{6o?|9@o zy4VCUK=$oHxqnJFBgAtGO^}brxgk;VkPYl25VZ$KWM(1IQ{ec-Tl~KsO_etUP>7MY zv@Nz1=^}Ewk)eg5vH82HvwV&NgH2?Tt0kIn6=~q7@(4*8UDOBDAQQth6?mDfZz_%e z%4^8sQyUKYICB0HS@gyyj--57X~002Vm3fC$xjs?!x`VRI?m8~wSP&?V!R)UtRLSK zuyqlqpJ$iVMrfND5XKjS2nN8BWi(;|xq&mrD6TEaZ;(7<#P`Rf@`q>V`5Vvl=z;yI zz6J=2qw1ILOGaSq1#{#6qYn^kCm}&;dBQ$aBd_wGDSyrd4z8tT>;j$bjh4Z$C_#!t zE-3jXRB?%7vKD4I;6wUZ1tQ7|r>}OAZ;lv6Cr<%^g6fM?z`V$$2yS1jc4&FvWo&?q zMk@$Tv$a_?y)GqvM6l+iUk5giEc_lZf>=};Qni+~9z8IET|_X81|jV=D9N}Dx-rv{ z=#QSWelIcM#+ero{E=&qfCVcRjG~OE(ruh2o{P!0o!<+7*S)k*(5^Aou}?F{QwF%2L)T<$=PlM8g}upY;6xk1GA^YWbCSM`mpEzWg+urN?@0 z$M$`qw)m(ih!6LUU$%k>J40zW3Mh%mKQrWs z0qYxYYYLDXSaKf*K5w1H#+k_EY2E-GQ0TyGE8iD_tYdlQ`|J)TPmI79!pukJ<2yH; z@-E!qGcs6JWI2&(FVS?3!T!6M2Vx0|+LFsnDieG070|LFw0_OH;7Yx=U%B~m9ggQ` zJ^*0_j5KTlowE3<@wJ<|f_xaa<+;-DXX3NB?^Rd+$tGq#tWftU)ysO$Bv~Fg0>(?&fC`@&jm4jL$EYpxYD+6 z7;ld+O*R80F`L!11H!h;jPz8`mwGHO*!|GjtB`D?`rC=ib@}F>8)%%a`7efxKX7L1S1rhMdx~qAvi=Y< zA2Th;vPw2PWCc#j1)lWU%!Unt-@eU8K}|iO_nj=8-^?hK`PxAPO)`_(I>|w>bTgL- zqvX7c`FyUG7)DiY<3NLZ%U@+eEZlZ=QWm5UP$;6(9A-=|;~lZZn8`{JI5=}FP8WBKH_fd%WH+xo-Q@tkbsiCDd-6lu=9%>t7g>vq!#MK= z>sQ7kS1LJP8=ti&FT4Px;Qlcbb;n=MIgMwuJqT;Nj$Y;XDSwX~gOy`u0IiO%G-s3@ z1uk}P3VBWc2Y$q_Tcs4=d&g0ymhX558aN`8_~wa&CAzC44G&*qds)vm3@}Bz%JlO- z6@XAwkOS&|Rkliz!=fbz6^4Q=xgOToaE97OiK2=j=dKY<63aYmnP~GFq-Q*QK6w|Y zHnwQJ+5oVu##!#2{3-mZMb`dDA0<-$JZ`yk2J`aI$s^Yz&E^tCK?jWPDYWeZ_p}aO zp__QG>Y6X#8x1?dT_vFoyVR_Uf!t9z13Hxgx&A2Ee*l;Cu&!f`2Ei9TC`u^-qnjIp zPYkvJIIMO^+>5Y)(dB>!#Fl>S!lUpd^f;Wq3I}UOC*w7`2A42$L}t#O-rELd<$G{o z?s6Tp7_k^62PU&z$T|*%ooPf#*`)vu=_2;5p2~-0mdE0h`aW(UZ9#t#yF{ScIkT-g zTX0DB_(@zh+89gv9C(_LQDJ@6zdz+Irl0hsAbdfi5m><#0sIe<@#Qg?;T3gkfW|IR zMRem9vj2XSg{RP8{lr{N`jB=Y1c6(n{327ztKTF)^ZABiHrol$ zjXcgTT6!w0O^SsVuKO=2`)n~(*UysT?+kiNn*{om$tpM{XI##7+_%u^*7$o!#^voL zOV*{DpXx2IFIliZ??ms5+}Z_5ewZ~M>dz~NPIo?!L;X*tnQD|TQZH++6LCU94txAc zWFQ-e1CaHx_raFJHs~Zp)}HC7kB}zg&EW_eQ;&kgQ zEaB{PX*gqg6xFIi$WF*>0GP|XSZ7ibLUR@~~nYEjD8(2;fM<}u{JokpTzN)N4f${cW72#_m zGF?4%0U`kZwW3X*Aj{hwEj9nHK<8D#9H7=O2ANcd2myO=fqey*?QTJCg520-y~O&dS14LX-5Yp z7SJRcnw>mGxzt(0W8jYP4P5dFi%vGM=2TK?s{2v%pNlP5qsRW7G5CE?rhgyDp?$xw z@s(f>N%HH`eG!HIv(I;dJJh$&|FM5`Qg$X4Ey4IYF3# z)IMy0@kTB}DSF`utSqsRJYJ@l+@#Wqi~uyQB{3_mxe3T5!8>4StwMHE#lE27G?yQk zk0ImlkX^&PAw@Sr0|S&p-tKLF>2D7$LuHLFO~mm3d4Lc}ur^wCIj)mZqtEipZ`XrG zF#7^Z1!|;7`=}?|iwQv7fES@aoMhJwgPx-7yeyODNEpiHNL2%`Lv4C;`0m@hV5$G2 z%1{H1I;rtCAjo#@X3|JNZBlOW?8F7+damp2VCI)S0;!4flC9jj5nLI?rBiXJ`F-xccch zw|=pNOBE35zjWobVSCDK+{%9wT4C%T9lOBaauMD$!IY`(SDBw-7&`+{lNJtaf-V#v zkav=yTt{lW;G|THC`|Z6bm<=lUN|a6r`g$-e=2|W#34SxQe8ZK!O=x{YHDm;lneqG_vxJ|7UHF_XvR*W|O&JB* zewZv+!4U4!d$Z+eaS4Va!jjSI7xZY>smUz2fFEWZ6kw>K56%DW^g3j(2gIb>qpNKv zfHuULkgj2_ov=Q$lN-YclJny~_0v`X1M3DTU{QIFfSByI?41b0$+V*><>*Z-OO|psrbD%w?-}DPsWQ2Rn#$XDkL8#aFdI;~#d1_QArDjxGNC3g zr?z_}zTB_G0Jk+YDMXxB+9|&V?x~koaSuXk{9XlsL|CC#&u&4a9+Xz|Sn*cS5`mr1 zliEQKtA%p|S~vJL9>0)#TWXXdi`(uHP*Hr6xTO3Z9@qy5cbf1gRlgws`canPjqt)8 z#d^A6K&AEdq}17uk)HS>&>%{b8s)0C$yr2L>4c}7r8<&=wlR*=x$YF#*d0^%tulfdO#kMb5Z9WckLs zW+#jtNVmfJt71j!#iRJ@;-#Hp`Xt&Dzbx4P?8D!?b7YneF5#D?GJ)#qziA^lQ3I*C z8Wb?`%uMnaF5%Wi463ha1SbS{#SCFDgeA`M&vt3CtBMDpq zxl0sc`cJ)DG}J-xp|B(*n-+jxlc@v;@Qc$9g0KuX27Ad<5lzALf{FzYydtUp*mm4) zzozrG9|MdI%}F-nYop_-Ur>W=yO*F+xI(U9iLCp2-g1^m7#HUw-P%tE?oaLu&s(0q zw_^4!DL{G{M$-hfUgON;JD)q*yg$(9xV8XyeRphK+4}?^`X>|(ZVxGir)_79m|1QY zAUnE_^*N{$C(7>L{B+@shXpN&m!rqSG=1Kn{U?j0S;nJ{M0^_OiJqB$SS1k88 zpo&q*29*j3L7v)kWE_XGgx#Oqfbw5L()R#l=W?!z9c3H=O>={PbMg}qtSwn#TT#g+ zTc8iQcnAf0e^_8)>ex-pwf9H|J~;9OJL=THsJ>59ZRS1YaS<8Oy1b&W`0%B1ghGky z>C9xP%P@F{*-PF&nCxiKd!pxXC*?98MZd=ScBVY!Ofr5WL)^#XmE ze1-_pP@qct9wLyS5|}wEBZd2+V?mCu$Yk{v@IhbgusJ`gC*O0in6$;2I8+|Nq25ym zFyp>^URn7D@98gPR5??8Y3(I*zE-`X$U>i(+rI~TEbaA_Cl+4;bYd>y?DJm0<|5xIpv%y@mGj<^z! zcExL4>_%ZpulH(I?R+_N;@-I$fScyD4g1zK!Vgn&$Pq1v}{5Siw1MIkU zL%|JSH4Jk1>JEtLG$)6rRHyV|4DZw^nS)XWPY@XJ;@UW3e>QQOjhAzTEOS2Fg= z09Fi0gE6150g;|9cC?xp(rKq9O?E3TRj{J7rxOkd(Eo)GPW z;=`@OxVbT@X|0s3PaUoSBUC5f^lq5Vw^?8D9QsCb&m$##5t4>pacmnwJi z!%ZU7(U0Lk;a|g^XsN{>Yy&6nbMQIqkITLwlE%exd+M;$b_Mah(leoOu0Ts2*rK^01K3 zE8`tk;S~*Y?r?*Y-(&r(_@Hs3A#B0q#)u6lEXd)oQtF?#XYa~=cT~CEy?T2hve(C2 zx>|*|pV2$_-TyoOaKY-2Pa=lbe%DCu0+{}sob5^_ei-?;YK}@h{d|Q=R@%YnF~(qB zb_O?Vtv{Ik`l}N$`r^9E3Mj2*vkz<&rEa5Y$Hx%M*Y$z~n&F`gv~?C8(#29XvTrf! zSRoDa1{t3$)p3aK_ia^hXze0IqMahe^D$KK<5go zh#0ab-ylVBteK%YEnLc@9)<>36pNr}G^Os1O2+V8B>cM5&YPj5N}IQ!yxNXN`EO~a z8Ci0K#Vm9GVupw*8WKaaARsTMnjb~4%?IGV7h50Y;18l?)@!7Kl-lO&Sw5E?Ya~U< zZk_f7lO}`)m(u*=gOij@S_Vt2%@=J{^*lV`Jdi#IWz8tZCZ}K_DQ&IE4xaXWlNcwZ zs2Dp5jf~QxLDEeWsqIv($}$>wtITHrbDv)fQ2KmvynL_o#r#W(bQNALj4FA|^OS)_ zG?cWjclf!#nWUGy?=cIfc+5$M+u;p|R8EaHeA%LU|4U(XTNtb%Py4=ntr!%w^VQ?e zNz*kEi`C$?iGq^{UPiS)+IObw^DYpjE5VPE1wTeJ)X&?O^LW%YgvhH>X>9sRsn|^# z{%JrRGAMmtKy2xu>mU`Nh6>%U)J7Dr0zJL&TobO(Ykr*SD>9B=Iw#y}4CmFRO@{i5 z{eAN$*(^xT#SeK5G0ZJ}RcTxP(d8$+gG`K4onF4w4K3Z*XXYi=mkLtXdB7AVn`UkG zADP(nfTbSf*VN(Lo%rrn1DPab*C6{dB+K}k!bOl5II|HfF5)6fbvjdHa_f1ZWqcnU z+^3MEDfwiAe`0LIwD~XD`hy}AbEQ!SVx=o3(gMrE#*LZ}D5{jET4AoXZn(P%ujv&w){+u#uKg1^7=TGjU8BY-P^wrieC9^9RsMn$zOsOZOBP?5bemm#|bImP&IlNPC zx%TD$EB)K6zthPn|nnr?pvs8t&)#0y@up&sHW17 z`$aY6e20rjKAdgmy%fuz-8$Z>*;V>RAGWZ?xqv(xpm^(ek8vv*(~qyTt)We!SJOhS>24{O zTfa#tlY_=)GHymIT{gfJ_uKi|d^O&F&%_7|==mY1c&L6nU9;W_rXpz^6Sdi|x~vZ=b4tz68$ki_2bRogzMfL`@8^AK!)md8h|`jy~AR5uHoQ%XFJQuMrt7opH((@N*L*Jd96iVt%8bB!sL zFWd>;F5Q3>dOklvT~f-pop#5}Qb%{b>;gI4_y3 z!B5tnG6@8cQ(PZhgRPJMQ2kn!y85$}^Vh?kHqPkK4b*kYs1ntYpWUeB^Wgk2Y4)jU zZa{(M@bs$8<3?Q#+ZC35E<`|u>~|&v()pMn?k`4ZMTGnqjZLlrj+b1UdK4~1R%z8c z#;iw^2AP^~Q{4RH@pw^4D2sk$dc zU;p{ocN){Px`XMTejlme5ILQAh4kY;?W-csL+I%TIoqADJaowrV@T)+&*+UVexfcaRnu@^w$UhCQY^Oh5j zQb}L#7{d3uZRRh8C5|O@2dq>ibeeE@f$~zTFvsC3g&)vPgGP(+gxSv(2AMY`pFcJW zGQW;!dU&&V5exJSx9$Qp6kl-ANIEk~v3Y02e-uE%S@91c2`?+QUn)gY1;-DywjO1> zAXgq}SN5=NR0CdoL09WZTAi(7cndFq#y?-NG~keNu8(hY-^EUem27!JG6|kmX3?*@ zTWK{n26=oX);OV5e0fP@gOyqi@SV5P0t8z#b7E8TA!$f)aVn{v6UEG417*Cp5cAE^93 znW;|wK9cmuVw@xSnyBElg43A;VA3;ZsCT>Y!~^+QoV+)dV#8hDi{YrN{(_0=`L z^XL<9=vzQW>n{?X(44^m(=&#X)j(Tgo>|u;w4u5Z+QpX5dNctS=BUUNa=m2}M<&fY zLSpk=G?op3{0$>QfxQ;BEm}DSYB&EmYWqV*(Ms9jq0%nR^Zj4w`nKhcKzTugZyb1-P_`e5JN z9G%f%XhDbF5A=JC%&)n`gL!6;K=M=>KQ8lM%~5|w=RfbrC*R*6$l5Mr?*bp_lK<{4 zbqfDnVNA_51Lg1^MEC2kDvXoKmo(2i4Q!v2Q=V4!;*bt*7!BNg03i?*g7)d4Hxwn; znc1Ob;DOC1ZS`v5353{ub}M&VD2mzv0^(j zZ@DFv=nv|Lc=~7v=hLTDMo=pC3POHpF?32xg@Wt{>{Bmz0CgGbZAWMSU7a3SbQ}Dv zTbWOLeJ&KCSFCvOPL6IV@@2rbtS!YwS!zNz@Auf49e z-$oBmY(7KJOKAoj2PNdaYX&JnuEKb2&ut<1^R}A#E{<_Zk*Oqq*GuMC&VG(?D6xP{ z|FbYcwy9Xr9f{Mb9Tol0bs(1uW8h7eB!)327v0W~LgC6QJ>icPp}wCw$nD zMyYLyUdv6f$CA-pf5cf_LZg@;2!hR3G)$Wa;Kl5pNKKp-5I;_dRK+yw2(WxW$V#r? zM4Lm$T^dct15Z{t<6A2{u`iN`4bl*_J>uSBTRsHEsS};(hIHNK<$OB)_Nf%_4_Y0q z!_cn+p}EN3(f|kq2lB~$?_de{3j_B>V<0QnH||B~Phw!@3Yk-bb-H<{T0wiMBWo)D z`MmzqBv@qT{2Zk^U*qIO*%rOlrozGXZ~= zyBeuY)Qf$;{-5YdnsxzlN?a#@J@atEv6uJa_DG&Goe@goMGNXUua&n}6_SL`YPbqr zQ%Ar)S7t7FE0*h23309coWZa6nyG?3tUpX&RbF^L(&Zrs4zEaf*+>hdDkEUQQs7qw zup1&AW;JHS*Un0fy@FxI%PPx}uj7}~-1Ohq>(Mv)%0nXCE~!nhlsit$nq1bfE^Dgn z49_D7c62_}N0sk$v!i1c7n#QKGw9Ihv{Y>0U9h zw}=BABnIn^(K*O0VV9IX+4PI4vT{6ZF!{LBZ^!Ll=!cGvfnR;WHA-OW zLzQdaR-=FO+Do!!FPtX6J^AL!fz=fqz!Rfq1dx11CW9{fP?&e2uQN2DHU*&KD8 z2831ZEc<7suPUZn)bk~~esYv^mt1(5;kT+|`u0k3bRPAb9yDzNy(aY4GX{Sx5SS!& z3NV87>Q(CAHHU$8Xv$CSD(m>y_dSUx9useW!5ixVcXSp&)^oj1gM*zy`cMR_RUI^3 zRh~kudETtvoZ^c$e=mnIVA%sQGT8U^h(M7k9}t4Qa|qOtHbu6X7rDI)BycWE1Xgf= z)Q9VxYLysD`>e4&v{-9e;z`Ne>!cJGdWqJbGfkF*YzF->oIjrMcD(S=PiqvCiCrd55&sTwGr(FD@796Fjm+JDB3Xzt%Y@E5W@oq0gV?ucX2b?Kha?}v944>s29yqLXk+avYM^tV9h6_-2I>%sK7U+w$T z&*HVye?NJ*JZ+pF@$Oxfjf?q#WX&0KEbq$NXMO=U&MynvHa2>9+PEt_KM1=(w?d`u zL%zzL>$&bEw955SEP-k7p>Uzmm!)?qrNCEgso_+!xU;6xh z)KIxam_VKve}K`B-5kC2XwLSSz$(+eE**n-A3UMa7KFCq=<>dfB8G@A)bk_Cg5e~a zL!U0X_#oXC-HmhqIs)N=g1wCEtENj#0l?ih2pcey{uW+c|@{iPe~+7cy?^{JtkH zdry03_PEt$s1c~S18G{@TCk`lYo#=Za~}9a{G#=pqzFJ9NcIKsBs<`gr@yYVPSM zrbG=L=+^}I!Nb+1k01_Uw_IfD3;}9S!Wv3c6ha?{Q@AF)W5@o$Vm0&(LY1!6y6$BbUb5w2Tpy9d zW2F>qaM+)*!}l72ungY-7kp?60Ik-7kJqrho(-80hXyxAx!ZAJC!+b;{^Ov;*K0dP z_6IaZPQ<>7M+LyGvr#%JeU$dF+8Gc6K{n~>{YtkO!^*OdR=Q!uYxM^SmKkm-%0QKq zfVZ7wwqU;_dT*+qdtFNIA*cZunTPV z&eNkr2=UtTGs#>E_iCyM$=GSbzDxJ{>}=rkp-! z48G|9U9U3VL-8JUeL{}Ib05i}Fcy#|LMiR)L$2+1O60=5L?O?Y%DVDE`L}9@avg{5 zi9~UPAPFCZvsSwG^?9KWwr^xSnj0B;DWH|Gr+76ZELgDxk^_3g|FwB&kPz}tx$2F{ zjG(Z9KxmMLu?$?h)IM=EcUb}&*S;XRcY1U7;ai2;W-0>hZdo1Xe?Hf!v#SO>SuXP; zwIla@iihVS-p=}K^0%04zO077xY^r z_Gj1Q4ZA>31ZVoTE=}d|l?I>BT-VIwU-%XDgOyRLV0X~urW(!JeJdH(@N)L@3@kt8 zT)9@^u^MWroxQdJxTC5F{+{aBf z_-aTY(lftOviVas-9~4>th4Iz#~6L-iPrq9dX3B+bg}tBJnh@N31o?^baq6s9>DAL zwW4a_vUh>VkiB>(Eb^2a~eaNGg&}J5jQnAMN95C9EDTfIC<}{cU#BMs11h zxp6y*gVl~a)|FbQ@R(O@$@}(T$xtc)4b?$G{;$RSwLeP7KUCgF-dk$UByYK1?lHZk zl(+Z!g@(M9hg&5^NoF|J6UAVhtk)wic{?7dAp&{xwuFDK!@}MFWPWxWcn2BVM@?!) zH1)LBFoUG{=J$v5#B(?;>I1W{9IknE-oEpUALEI8hTqyhif2eQUitSO`p;zRWkQyVbKuKYMOFOtU`ydXI4nx!@aE7 zw;XV4;`URpuRHXM1~tDk`xpn{PGZw|5RdlA+{{s>MyDh?w3*%=xtApr{VCQ_aI$wX zB7DS7Q^@aV|}B;(`}OhQFvG_@hBpkI{p; ze(%82V+O)txf-{ikyGf{+BGSITslqa;U-6EO8cza{tWUdN>ZJk2zV~w2=&Qt~Y@?555d8<%H;+-vyfDt7fd)%v1z~ zunQWR+y~82YJdEQqM-!eqM;TTKZS%l^hoH{C(VBjirBFJivAWg|H4s64XzG4mstnN z=ar1a{GgIOF@uVw9*>Y4sTT<^SkwG=oa!QxS3~!X~)zeWUY zKFUj+Duq}wm{y3%lpfnAt3W@u0LqOQWhQSYR5m}H{-8l=KCqXh8X)mnQHkPoZhR); zt5OrUG)S_ob^ARpqakDz$i%nj3co+;a=fr()_knTf$zv!y>c~h!f#xuBFBmt_e08{ zbT5_0akXX@3pAsrl#!B6_~nS^hwpyK;-i}`pLGmtZE8H`45eR^d@MMEO9=@)4v#

      tT%Zs~JP}?N1n%aMx7J>jF*)kPY}jQ!%pj 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

      cIgoa*FvPnhaYoY9uAnCotnj(ttp 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

      ;JMUh)_u^Hod44U zC~6i8iQZ8g9>k_tTIOFct+)$n(7VZgz z{P_JYn=%_4 zn@LK#QD__++jX>oLbsx%3x~YPyETCpK~>bp_t!F6&;#6_j05ZLa=!GB7D*(WWYF!2 zEmuf{qX&m(h$R)jZag#io+TldyWQm*p0dq=-X}FyMs^|9&BK! zgm2nIRwA?h(oPEYg(^480ivb~V0iRvIzlrDA|MxXF(V&=)NcxP)`clBkjZ*y-M?ed z>`OrL^2$(PfM;OO+n&rZ|DcVnDI|%2_l}o*F}z+m2H=|9_i|2nSTNNYm0gj7;}#9N z_SZw?8>>s8?B{=M2slnbqn-`+^{#!MBrQ#{)(3L73yXGhs&OXiFljM+YjH-lErhpS zho5K;J526By5amHqHSnzqkwEd3oC6->#9@baAh?q}jx!a23bRVJJgWWwlo)SrXPM z*)r=M|24WZQK#s-*sV{WEi&b~L|r*p%x^w6O1x--SNQWyzjT`xcJ`qi;!~xGJ2QZ` z7^~A9v89D2=aLRHUeSawfIKDNtL=K!*z(a~TeMMod&i8D3zS?5(?9JJdWv8-ZrDbg z%UE4lP;LH?DR@i0+;(wgUv73Q(zt6kY_?3p7n5P{U#knwszSCv-ZycXWmU-3@m-wd zdrynN@vi);r5-=Dih0Zi)oM($0t$1VQq_HnqV!iIY^E87uYYa`xVwYnni>ua*jA5# zuleT(tj+QcD0(uCC%iL6&!FzN433P?G|4-tlmAT(18LdVoXRB;2Yvg|HM{(!H4`4# zY}!&&XzEQ!MT+-3(}ajS4mO@1JXH+GGvlvabv5(>-v51X{PG}DW8KPRSdxxAd z{he>@jH2bER;SW4EXo&5uYG$MD&)$5(|cOW)94=Jv#d4@O`bB%ZnkhRH~`DT;F5XBrU1?A=Ntoa+mW&4z=c&LAcG+mKZappZdc7f1bx!bzJe{XT*Ia>l4Y_E#HO*&plx*v*56l()`|_8c~I# zwZ7Y*%R3g?-!KE$0*P(j+pV7D{TG!i&`K!Y2l3S9od123S3!5@NhIaES+~Ju!M3=O z%^mCf*p2zsJ#{{~9)GtO-!lu*Zgii9Plsb>gO=6oTrL7p-lkbVXx@fnJFks)48M=Q zz&SLT@*i3VxZ@NxGk>^-m_MvnIHXdlyYoq52MSZvfqIl*dgHq3%#P{{MIwIVtzG=p zx+l>3E{BWuC7IHq8{Ld+Vmdy=C}*jvgWsO$nxw0&MOJmq74_FMX7#=;bi?33M?IS3 zjjxqi9rIIqTLqGDg|&)VD^lA(A1rq7M1`&WJ4gVs%n*SZ@3+)j=LBoA=UV3HPH?~NeUJ@^BW*EMIl$rM*GAkh^#i9FM)d z;t&ZOnMQ(&uU#;~i!f9{2GHcuqhpN#0=&(E(f6Oc32hPd#Ahsq%tiR7h70Q6EbUWx z%9fL1p4{N zjmSCA>OSsxPgk*Q`Y0vM4zW6^XU7n_EA(kYkh{Dg53>XP=?J&EM@fRvjN}F?@FrFH zSz_T+HTRh7!m)Rr8gt7zF0J7`qRJ85;i?QyeDY`xqLHkwSQuaZ+T!u|(o;px?r{Ey z=jQ2(tKcwYvNde={_|qZP^4?*e=GH$UyP|hZ`Sjqda_EGvQw*!M*RpZ5q!0P z%hDQst>cccta$s=@wXoyo^}z8a{)SPzlvVV6n-aBp=N4MjslAM=f`D@3J6~cmnRZs zej{WzR42vAM;m6OjqC%n=97>Sf)8*{1GgL@H@B(yAn4myyH8rOC~vJqX5%xp2|Jp% z$2MR=_SEAVeAia+4Z%zJ-nCZEBHFvGnXNJGFM=TE3*FwKhO)1+=d&)$50rn_IeW(i zW8Cg&R`Qazt4vvl!elHT&g1=euiQ}35et3$-H!EDxj8jb?rW11&L;l|S=9EpXLb2quL|I8jxo(5 zPvv^gdtT@?@e8M(G8h$MGOaRTso;^~KGn_@;W&YjwDTW7_vFpTQ2@baZ@0Et)F!F) zOQEx00P(ZFSh;wF58*A=qWFc8@dA-#5;&J4{8(Dl^jHAaV%<(NAc#?m2O>hmB~zcEq>`>kFK&zVc?`)H2;-#Pbkvvi(7qJTUP#%-_*BW z7wWOE)|N)XUv)E~MWLo6D!zG+k!3|a78qWEIUM+=QM>>u{?qEhFokhyKQ``7JtQy6uT?xd`K&E(n+^}T?OV9D1NXS?#Aqt>JS*lTk=aq6_-^8iLw3&5k(s`xw4C5+ zv*#ry$v`+Y(kv%D;ypi0LgetJKumq&J9DH~+Bji+ss zRq9Dl#*adiRtb35G|=2;vx>{wrNce8gz!gmGWcXS;z5%D9Tx2Jt$>dciw>+I=9k&Y zCc)s#q~;b)*OxoTpxZIjKtG2W{(jTs@a?9^CvNlV0h_mnk2YQ%=J1}KBi>Bk%?0xF z7HJ@)K&sNuQMlHwzzjSp$D-GZXU%WR*R^vcwCR}AO+OyG_4^pO)fe2=uYh!GS8f0` zCi*hqa^nc!*=_Y~O`+p1;!M{iKwIuDnZRhH;JM)KWA7aJwqv#m@+BtV!tAr!atG+N zwLgNv(OB=wgsmqsz2iI-mSb5tGb(gtd3JM^X?rPT5R$#^)zWt9-ub$3vOhnmvap&t29s5B^aC2 zl6irCWQ4OxfafB+Z;{$1|96if-sS&t2^`m(>EFGK&20G%_qqj^4FN2Cud%&P?PSwp zj;nc$uJ|b}9RIu~x;8eI2x(KyiZX&Pchk1RHhj;^sorW*YE4vXnC(|+;)P3vnfscM zSnI7~Yzc`!?d;rmm`{s#xLkYtx?QIP=}7oi-nU+fZ~nKo-n^mvEW<1VE}-UfbXaD3 z($q&+nm!Jn-tiiVL!BCcrtp>D1(?ZUx63p(aCll2U)ArOnpq&~kfWs+N7`|{#6&8O ziAo_J`HpPw$*%J!g*$2VrQ31Y+}zMh9r6jBS}Yn943CP+w0Q$>)i}(k8&fNHE`lsM zwIy@~Zj*L!mZ6mD_!?onWyS55qqz}iNLRZ?GC@^Cqi^;GpX27>MxO;h98$yCxaH(v zy2hw=HS-iuw?f?)@3F=596HrAu$#Nl)CWnhBLx%3$mfbs#xIfWpNyIeUZgqce0g;m zGEp7RX|E-Eze#b>|AzF~6XgEM9#q0>3GVf<^6X89A)ZU__5cy}=AhrH4EOAVdB&OATIwhMF@k9riThgpor@w&9uq+lCc}6<1a#p!``@(G0BWn z2k)3X#B85;)u*!>xuvW4`-3*J$0RNgGs)vTnIV*k+ z8V{f!cw|t5vS%Lmxax*`(Z+A4kiOpmayYn78`f$-D0{PxtD&vtl+Xj2@1xVJZuAD| zghvF*ESZ5eN=RAyZ!Uq|i_Ii@(zAy>`O066e^ey0JYM zkhHTs@`PfG#=s=JMWy=;z~aJZP4m38Y+&x`Z#X~-4itKqGg{?hl2ZUCLwm6ID|tDu zmf6zZ(<3J%-|@HVM#UTyIhAk_@SD~s>r;MaV_~sy%yhP5;V=qFAs$#)jkb3d+vyp_ ztLVMQ2PE}hA%o-*l|_}Sw|ykA-VRC}O@iL)xv4d`DzF7%PN`Ak_7Gc z=m19vrOS3V9<<1QVla#3x*rqX4a+O&**d+g+p|YPr^#VyDh+{Ok7$zt8$-m>!0mv> z`CqhWe**(n=v{z)El*(bV@59$L{$eE<8+i9CMA20!#dM*LuKp^>JI0@;(Xh~rNg7S z91YCk!Ma9cs>o;$`4GF3o&avJ5eIgvr<6%q z;fp38eK6{PVy3u3Y;$HINk@e7n`yzdemkGgB_;#F(6#8PPA!8LIrSjsU%rWNye$JB zDzSctd}J(uJjd9iaO#flnO75?8x4)ex08K^C|%Ez+RPb`+dxDW4N&jiNPyU9HSq$f z4IsD2Oz-vI|GsZq$@EkEbDw!K(?brMqQ(+*S7Z{Lb}i87-kj&VN5ou=H{bg9WItS# zjY|;*#96%Nu_}x!;j+fNut?tJ($fa(z_Bm$(RSd5`b+wKgKIc56NaW@H=}&0IoD&L zdCsz7VU-fLqbIPl58eH78C6)~I_`mLrq$=*W#oD*Kv7OT0*O@?tOM~>liziZk7L+s zF~4`QdNpb85>?iht26xdZw^TtfTwugAY=i=)=CfolipZAEM7~96wjWvR)_0?%?&-@ ztS{`_tVDTswj|5{IX06=l71C0uFf(D!4|dV_2~oeO$-`-{BmUDwWNC8pPfkkl(yf; zzPt&PCBhD2crA^;zopOPex}qR`@H7Ee6$8HL$7svg)t68Rg`H+wNy|~8gY;c1d$sb z>YQf_#`*tYoK>X4m*sG`MW z@hMcNUs>N@r}KO?ubOhuFIvEeTesIK?xa22GjIKbOL@-3e0*iFb&bE{_;hYbl77u5 zJfTh)>-*15>;K}erWwIZ>Wh$`$q!z2iEgCZhsyKUY*{M z6U^}9x`+-oTz7IjL*3pBTERdbcie%D3b`P!U8M zC3NWQ5=H1vWO+weNYT>ENQdkVec61k7qbkbltZCnC@qqI8*uA~Yd9(#mSP%>RY$SZu^(*+y%z=I~MH=pZ;*C6OFe}&Fe=_IGOU!xkO*$ z)4$p*$(Kc#(CpS?Jw<25U4KY?JEc|kJzN~uMBr4e$_l>mt!sIVg7;^932dm5G zvf{C_9d$essd&!#_|np)tWRxa0N*V;NiV_8e(0?HVOtRfPMUe7>jU!>b=?duuuh2$sB4FP_8! zOiNOAhSs%#V@gFBaH~gER#%IxYzRi^lSc1i=tah(_kptTe@n*Hv;Y5|JU(YY76%&( zt&4*Zxt@@+2%?#iD!#XLio+)pdT@8t^TYY!nYfAo>wk9srDy)f(s#!ty}n_yL{v~j zGNe>+qG_pNC8e=cT!@lssYPbxu4CA+V}JukrZ_T3Cb)2~)Uv5%rnXSf%JxPZr+#5| zIN5o>=Y3!Q$oQnh^E~&s?(2I$_qD~jY0L0QH}mZU50>qx%pfj*P(5ggCbqQy!G5_q zbh%Ol1qslIy`LQ++eg#i9s6eW?P}36QhEy12PTBW;D(@_tYLy=n00LZ=HRmiQ$IKU zG`tElYhAIZL;YrHcb8d zg)x5C@qO8;R}OEb>qN6;PFPq_L{P|~sVE8H%XC`g%kBq`%kdez!_IrPCwY86 zo3T9D9@y>9=VzNM6Wu4pRaR2RxDVBSyiY-cQNo_>SQH6_nK!6Hf^_<;t54~*npP;> zBU6%)U?cyNq`OC-2d_1`m zuG|=3JP!S5jU*sqA^@VBDF#%7V{9vNm`(n^p!iZ7giR{0{5;7;STYAZ2JXBt`zHG1 z_bau}POdXsP+3Xx3wq(}2+A=;% z>9;M(jpU%FU2=qCWK;NujVY^wwq0;pzhU$)B(d5rpt{-fkLICh9sZ2M;_P*Y^!I=y+hBav=c#CS49n_x=Kr{vy~fWzRap(aS3f zna(k(Za$!Mm7l&fy2=vI-w!bUbGfhQ)+h$JWpF}orM`q@R<9 zITc!!EwZAPrMCb5AtB{X^!TMmpL%ac{&6S_N9FM_2b^LNpj={>F_aE)MZ*kW!c^L( zf4)a$Wcb?e48Q$AnYy^RsTSDt@NKu1vwtjp64Y1BR2_cT(Wv@pH5ci&%ag%dr_-7e z>i5L%L-?7?;f|{ZY|3ha+;21ARoWp|*5Gib(vlFkAX3^Xt9S=xN?+T=RCrKaefv=J zQ_3d!)a##FAAhfZ`!7iTcd^srujrHuGpzv;hG8>b52yUCEjgOCHKuC4D)o?d{i%;> zo4j%al{5U7#ENrKiK=>;yZe6ke&;P0C;p0!O_SufE=O?-q8?+Q$`rylr##`+M^>Bg} zrMvYew?288aDD6cc+W1UMef8x_mx^y@6t9@PB3Nx2K_H1iPl~s{`S2ovVtBku za9qH8`C0kl^vU$I5pKF*K-;<})xiQ*U|d-KCG<>Rh(qODH*6o>moz*~#Kcw!jPO8* z6remz7%rc1TOpz4xel{K$)1~i(Wwe?hkwmhzaD`Va?jDWgn_cIGhGSYI};O&hF5Mi66Az?^eMxl0Su6nw&D_u_x9ZP-ryo0;amCPNxIsjDV*vV_QmDf z_oTNfLwCq_^xZ`PGvqR2YVY;UR=?&phSpb>`7Nv7Rz7%iUeFFqnL2xk1@sv{b?f9* z7vT4V2QKVLOEBh!Hep+zv4(=E*(aA7v_zKoqwHkPVJ(r%I2TX#Uo7{CJT+~+x{05^ zdgu8p-L}UF1o)unp?CRn^J|`mwt0&^T}^ckL0nv1Y+TW}J4hehhCNri%wAt}pSu72 zk3;Ey)y)4Y(OeUF?5L+hpb+4(uEd5xaItmM^8ugI!}-@kOpzr}$t`ELKN$zi|8J(o zkH^H`U~|bXd-JeGzE&l8LW|yMR4*KjRy6}{V!R%g)HB)ox}dk=)&U;#DXZ_yv`&9c ze|F!8pcM5G{Nl>WzY10L@MLrhxRL}RF^MGd-K~PTRaf3WSM(@{-aokV?y>S~x9{W? z160PEx?pDJy~ubUJ(;fZkv*F`-ICgpT(183&B{ z9|4RqMlw=*#ML-IYdEHX+D8+zY(w)QW<)_&f=%j~0$po29_~!DGe%}6%N`%8bO@ZG z=5!#SY$%|1UNKAd@3jsbL$M-GoX1Dh0K;~oTc*Ds-b+qxWz+JjZN+EYL8+rUV)p{B z7}VPmE6h{sSL(?3czq7$R%*PGFC9-Q`s=j3_sxQOC_Vi*yYuwstuFqtwJ#knABPJ1$rZhwNPm*@c(65Z$aE<<1(6XxBw2eGz< zA7s~e`hRxZ!EdMha7Z0I#i{nY5X3^@feAd7g%%s$T>5j#=V$2wQY(S{&!ev7`^3S+ zdoDzvHzD_0(ZMfx1cD@h2)`#5Bo4e+cNR$>m|eBF^;UcK;M1G$YoBaD0}&UK7h6Qf zh^goaSYd2oX|H=~%7vT`Gr16jM)UB^1aIq@Bg(yvh$G0Lao!iA2b2lxqjOJ%I_QO2 z-Lg3YBm4D6Ayy8f-+Ev{*^oK0Kf7iAW6#Br{YB~ocxB$q0E{x4$8lhIK3|#C<@pgPPgwWX1_K+yx}ik zwnt*oKgdIe(i8em?vNCehB*v}wmz7zZc6~}Ik$#zqdv^0b*&lzM_4tZN5`5gBu+s- z7ylP+V+kn8f1#sR7%f^7QP>47sS}e4& z-GDT+#!Eni;gVjQhxdhPRf76bNmI+ElEc;vwvSczL?uq|;P#hvxbrUdRCQ%+*emg&|`|4w=v zW+w7pyj}gJa##0Nsm@NOZ_^y(snvIvxhI5#pjJ1UAzZ@7K^M3Yn>Q@}xtj_1SghVT z`(8Hk>*bfje{Mc|d2A>uI9A*b?%+vpnB|R8slZT?)?g?UI{U}t>JysZhR>0jU;lzQ z-!5r)951e3`pXas@iL>B?UW zX+9Hw9bUX!?jVF&bwCv{L%R%{3O@8&*{%k z{Q30b_e*&FEnN;OiMT$jscB2+WVK*~x1b7DQo6Q`s;p$rQ2)l&@KQVh&>%NK4u7lT zj#a$j1V^5|M%#UURhJiKVnGx5`ii6vPz6VQZmKh1=B8`QG2rV`=7rA@0$!`HFcE30y zc>T%0UVh)dc6!lr`Li!O55ND_t^8(NTDXM<;Z1m$aCvt(L1=|a@)y1Ja-;~#6gvvD zX`yKA75N}yX$xA67~?r)(Q!_@?6WW|Th2hHoooZn!?-W1j}DF*!^b$7Vrjx&%#rJC z>b5*dB!|Qkg9`8?!x+J#y#)!X$9p^8x6hEBZwjFiXcw&59Yvg+3nQAkah!Rq-L5#e z8?9|vT=7Q9`{Z-FZ}xgKKy3hnN2=OQd?~iqMa$e*uL1_7+44)L0iFwnZTG+j0OIm- zQRBaUsJ?z7*o1HYQ1-z?`{(<>3s;LW+72sdg`@N%W6)}rfaK8E!aC`JL>g8n9zI)g zf93}Ne^1I3KaX9$e=Rp)kUo8V?wIOFNhI^GJHhJW@r-v#@yCkX2h*;-Q%k$w?zoo% z|#(`gnC&qvG6Kc)`+`DU;()&wuVkM%e0x; z3bx`(#Fb~=A%c;<{dl;{nxq)awuvFc=a(LF+G~v$mb@5_AnFAQb0MaP%E~?n0|E}B zBe~&IkA2-g+=C zb>r@?%U7&(&x=322gv;lvE1%-ZL7Qex%YFs{R=gPiG3a49Sgh9evk6{IqyE!6qpT@ zi}DW(xW}Rfz5|mBEYB?c3xd|EuLB7E3vr@(w~cEE)P(%75JH+F7F3sjVC3E8FR}T7 z!#6;5{ODab9o2Qj-s);oege93FYW*pahL zvu1tpef<0Po>48!sb|L>-1=~0L#j83K!gHwvkriWcZ{aK&i%08Z!$;;5)1)%_6psrYA-UbAGdn%u}_@$YBDR6cI0*Fy(*-QlZYQwK?Uc09&cl5_5 z#>1~8mB_#nD1rPX(Cn^>-a29)QF0dt2PV2ZSw*z@5Jp|s*%OF`VAi;vyStSzp+!fZ zjvpqJyh_CoOtCR&v`{RlanF)~4d@d{Y%^oI3wo^&5{l3C&aM;=pki!fsUWA!*i<4o z4vH5IXX|v}^2|_FRyNG_M51jOM+D}%TR+; zB@~LOQdb#!Wge6$wbLbnQ7ea%`<}EHfBtqL)hY3@>+>0bi~8^I?i1CISE=Vu?OgUQ z!hQ$dXOU#{(TWZ+%M*&On+_uauAN-<`kLQ_^0kG$26!Sp-K73u@$IUGN6~|!!|Ju^ z{l9b-?`F3ocEv>j90ZGXkG?V+Y!~x<;2-;$iN6Czo=0X)eY@{_FOq}Y;x|skr^1nd zGoD-v0Yt?FlStfIm=P@g1@9p|b7i(1v^o~oLIrKN@!(|Q{A$YcZiyhSrNm4)x)70y z{4+|p&I;_DJq5O-LD0h^lcuw7I$#1AMMdmpR+6pIC6a8@kpyo*C>933;|7TEils1M zJ|M)Dm}JbL-|gD|@T5EbP5kYE_|;pL%^i0IR}=0in@@%`-`u*l=YRt^7U7Kq^9(@1 z3_lw)K(35O{lZ!KW2bRF?PP}WeTDz;Kj+_0&7WQJ)H-Dw>3{-{w2OP(twSvgX=~Ad z(mf*h%I#>-qF?j-sUyd#mMJv5J{(Pc?UDHQYHnRuhbA%>kh|qcZI0jylaw~Bo>;L! zS6%V&?|*hUy!yv$+w+^DjoUKGcRf}ja6kAVav+S`YrLLu~rZ> zE4q)>NF}0lLww&{^}`ZL<@DU>odZ^S!7KCdtq7eV;h$5EricnfUKPP_xL1gFfCdEx zP;qD6n9kHuj;fHZBO>NA662sNUr+_k5<(muwB7Uzhll{j!U}v~H(-RwsPm8Z3>^*g!ojpipt;{x^J{|YMis|T z9(XgudRQZgFHOauwv6JBVAe^?#GuC5Gx@@a4l|e-={M}ow3!aEs|mY-g=fZ5OOTS0 zJU^Qn6H#7fGxyJDAa{|#kO?luSOF0>w&08?bP& zA+uS?Q4~&P1+nsmLAfH(YCjxk1d%`o^9*S+UY^e~D`8xgF336NfJ%4*gh$W4zWUgr z;B@WJ2gV?0DvRn+uLq82^9T`uq`M9hF6ar{9?c>HHr8*`CIQ>W#J5#BiMdhD?0)#`0kZ`vPi8s_1MgjN8qpR&A zb8m+ZRFtQCEJhw|1jS%*?!rc7HUpHXBNW5!SpV#mj|u-Q-Z_@Bc|B+hG9o!C2$4Ip zz9Jc6xVIkApuD)r2$$TL5c@3C>Pk70Fg1Aq?O@@?JQ%8<2omKF-$dc^({Sq?T^Lm~ zFiZ|L&1q$vDuodxih8rm5kLBS6QP9I)M5$(_n|99t=Mc4Na&tN90ui_!|Elsj+2}7 zLiHQRVm09zZx7y$E76exH@pQ%t9-u3 z$}N9G2we*_r9<#&Nor~fC<}&S_?(e$6G2?cUnCxMA`Wh|16kkNfJ0B&qo~qEJw4nI zS|UV`oDfj0uzlQ!3NULVKd(Xo&IXZ@krHGg42c$+_7IMc5J;>WRYx>VH?xIJN`@l1I@{Km?Fo?a{i*BthQH8kuBn1*TIHtU#u+OhO!9^x?<5a@;>h zI`5h9NPcire&NyW11HZDUnmaX$m>u%?%pZ^WMf_r9E}037)^V!qqy$bPS&2m`_5Nm z7T!Ga4pn*`Y%kiDTu3{&J=+c$ioF5FA~`~|DeqWZ{ODgBr@wh!4cbwahqw!KW1Rw| zx;L533}CFoY%xCT2&Z_JH74l^y!?q?msmtuOam4M)h}mJ4NUz|t>dE+9@Ku&Zh}CB zVECMcfeNTI}VdKJHngTVWEZxz?Bf# z+UkU)>X_yLcQ)JCK|-a<1ep-$HNixL z_hnGg2_s;Ob_P#M&>Sb8syV(ow#J0FE=fV%<8R~BA)0=HTQ8W%?<_S8oI>T{V>1zY zW_m*Zn?za6*viX#kU;8o0s&0`DADp4^jR?K+X35tu74{5=s|G`25ld4-3r-mszn#xC13OSUF74R4W+Z)v)lekKt@hfv?043WpAJUa}AmdtC9x>v+>=wyPgW(Z8_1i@wtlwZijmm^%b@oQi}~HQmoHyx zFKd7E@m+@HM&2wa$L52S3Njfx1A{G-$U4ywubeKyD|y)M2f;NBb@Oi{LWUkhZEZB< z+2%Su0#QTXBn2r`g3g7WDH^>1w^My}2j)#yFoa_KkxUDFh-*(Eu6a-x0c*n(OI=(L zE-?;}l_x;Cc>$HX3`Z4Gyu)sP=3ry$StHn>tT_lrlmbpge%6|@RaNM1jq$F;p$p@1 z0wRGb7IJ1?n1eijuq@Sj7`&SRLlUSHsCz3urapCFcWxRlMv!oYaWt}!J_|YlG0T4O zA|MVg5uKnqE;_UX*s2xlfA+&|jan6V_}1CyFf!ro_PeRW1RFF^p<>pW8JI{Mnomw8 zRrl5AcT~fhD)tQj_dv|9-DaeL&14ACA_j`nge35&=r;+W$pdIK47~v(cryOnT}beI zOTdgW%HQzbrZ9o#f5w@;2&ytBAptKO%jXP%hKI`yI0gy6!a72IxVs zZg8071%2w=9`Qec>C3n&WFgqV262FuIVT3?v>?#^%{>Sgf(?v5jz%S;(G??@Ia4VW zw+XfP*EG(}^zSagxti{~lp%+KUGND6vpLAbhKPp2QP35fFFmG=By-@_?0fu6&HBw- zjNTU&Mc&+xGu`VNDs_}8LUV^ z5@0q63~*rJ*~tqu-o^$=IFbcf37{j1a5yyy zyb>OSPp%%oZ>#1_gjk!vX)Z{T(ioAKigp0UjK*I$0L#Ml!cH)2lq2I9-ms}Vh?>xY zeaFys=)8y69*L`I{-82o<{@}Mw`f7UkRW%#fbN*C1t)`gIKv6RjUz=31{H`vv1HX-_x|R5hnPpuOz+Ap zc7!vc*9EcHLktpSlkn9d16zZ*vdJ^_Tf`z>>WaX@hp(>$h;R1W5U@%Uu+^KWJo*uZ zUVc9h2e0hYu|_XI28(-ryVo$^$mtt#Ap?Vu;r{{h-uQgO=Ghur$!?hQ_b#rfHcZZ&ViS>PnLMvikb}Fn84g;-sz)`7p z@P+?TNtu$7$w0Ob!-mDejnZK?dmpcN5DaYO?s3A=ge7QlQgl&%>`C&U=7*mg# zi)d`N^38O?dl{t!Z88+U{fw$5-4_Xm~9GTqCKj%)f^DBXCi>Vs~ z?PP2~p9PvD#^YbRz+f&2SBwgj9ojWQgVL|3=%KUWMmpBK^yh2B>ypJ8J%$ECa3!?C!>}uQo;7R^{D4e3s#K^+a8B>3?&rPCqsb=q{^Wcss)T zia~o|&w+$=9cVT>z!tMXpy%UGutvKgcq~^pxIGbUZR3JSt+JwT7rkPhMTwk7dHQ=> zPGM3{xXO?)+TQqN9-=VS1Hn)Z8#|Y-1vCzNV7tjT??AReh}H2eLXp9dJe&b(v@q3u z2tC>>giIkKh(aW{iaL-Lkw>ibFox;tLLR@K>b;$Oyz23N0SoH}@X^^{GyUgE#$#Mu zWpT<#szXdQr;$gBPa=A3qKXZn012M;KK*BpZD}4(YyfaE*ImVWaYAs?DB2UHTNr}k zeetw(CObTCn(fp_{f&rB$E9qJYcaJO2RnUR;HL_O|<&`1a+$oxjQ`$zSIf z(SJ;Q|3hGaZ~^2wka`dV5{#gU4e8YF*>`ahR546M?tQ^)JW!|yM@m$7^io>DN z5cKuHGQ7|fN!I}=2nGi-I#iPH)`AyQP#X{6oY2M4{lz}Fod>G-q^vQ6h+Yh=_}@Te zOl8aDNQ~YNcPqFQ;S!Jt|IhVV24Qn0JO|)nR34f4_vfo_G$X(SM|lA-SKLG}zT90h zbATE^z~yEs3B&VEA6=iq8~UMtk%Qs9SYM>Zgt=b*;O(cLzBxD80-wNMix>m_ai$rNV08D|ZT050{$h68gk zHROy)+PQ@z$n25ppj5Ytgr@a(De((0?}4&RBx49Eb=J_>TAWjF zK`4WZ#?R~Rh7okcf-0#u%79t@NCXjs0!lzSoH$*Zvv3!0TWX`JN}v1m_0?OR1Ltd7 zrhW4Ol~j+e`(6<=lJNl7?l~FIK)@e?q0tx&8jZ6ZC>$N7gJxYM%?{(@nuhXRXCA00 zwGaugDo_XmO?bz`4SXEa=Xe-QT!?In{GIXhchT`o!H(B;a@Oko4F*F`ns&_KPv7ml z&3&}5>p4X$zoYrJdHTgSi?iEbJ=-;N`9{P2r8RcvaMce9)Lo){#MjDvWNZVR>g*3R zz`<;eSOvr54M4W8$haFoloca*WL_cD9b)=|ZGNG-L03oxpN!Gl#4|9<6OBt_aTtua zjXW<<%_i={Ke^3V3BJ9K$8utX>b+py?1)p^%x=JXz!5A&&X95QsI%}LEdB&NkCVsC0encMzP~p8P2SC*K|IVGue6a#7pj^!Gn^8P}nF&Y|Z|Eq-$MlQ`05qZ9R?j==ts zDg^14T%iB?`K(EU+UuM;!?#6ktg_;2Ema(LvD7@cae>UAS89D#I$k0p&WwhXp8K8 zK!NcE5n5X;TA?;<@MUmi3~I5gtmKu8sZ7Q$mMPWqOJJ05qf{(1Y0&BmXl3_aF$9>p z>G>rnLpHTUUF56Pu#~qoHAeKd(@f8!HmDSd#?~gVsVR{^BE_U*G7IFm|W1 zcW`6wV5Vo^{Ii|}ceN)pG(&gxc#rS2SBW-v`d?6Bx|z4RIhHGHkOwzt;knb=&5ia9 z#rz@#Sek`MMl*-!Ske>=0_hrge z3EJN+AGGzk(nNDx;PficsyI(ym5JT-Rk8Q>sN)^-=bEKhN;&^c zYK#>2Aq)88NVTU9)-x73Vwo&hrn%N)ZrDV@0|vxxP#ZH8#_UW!-=eHVArN}n(=?Ac z?LOUqOg22WAvQe)eFok-ZS}JfnM!N)Q)m58-)Z3D!_OPj?5ycDgxuStRgpoHIVc;X z>`X*?2wS930|%|mEXrc2@nx6aSPEKyEMfmi_nq?Um1zxYH%|k`K(u5?(Av%)moHMs|If_*b+kXWwff~R5c$DsikC?!^j!tkRsq``4*xUO&wRR zScbWNi88nDl0j>ZBR=zo2T1^I9lFW0>^|16Y_$DXpcD=ARl%nc90@z!qw; zYOLCxov-#cSY!fwdA7Sj*2e@2)Y?f*46`s0Wsm3e2UM$E3cG?^gn*q}rX3@n!geuDShQW&pPD|r{ON*K) z8G0XDe5o1G-oMmEIp#jOv<7LKg{YN5FK*Hj#7!{I-60xypNK@u57}nls;{`=_rtU4 z$?@m)A==&Cd48~T$t{gy#PX%dRO)%<0@*>SJ}#n^lSvC9Bzsk>fT&?-ZB-ERgQW^f zzmJ)=D04`C0R@KE)LK8Wss%P7Tb+Tfg@|)tp7zsd&0to9Z&S8g_m!`h{P07}R_nQp zTc0$LObV92c?qjEQhJ?d=L1L0V9evO!HxF8EsR+p7U}vbtn!0fRD^tWeaJ_3t?mJC z{k5Gc`(T0ELE)KZQKGqyNZ&1GFcuKn6*Or$>r;KGH}~1(Yn)`^w{YH88+_rTC(&Hr z>$KK>nlJcTB$Y3gtK&i_<$V6{(gHkoeo;X4HrH(=WLR^h3VQ`s-`0{!LHViu5obkM z4k<1;+NU90lhvZR$p0FeyhN4+b0>Z*foL z+e0c>6KYK!gik3{@+oDvDvqoN{5X(y7Ey5dJmR9e=0CsR@fslrPEFeqHEEKVzOv}% zGF$DoCxER}uW|ITWVtD0w)R*SQ>fEV$(YBJwH170lCn8S7<@oNQGf716}-#&}dq8!V%3`U5WLdmeCXCmS@!1v{&3RiifhEqd!HQ`DR3X5dI2Re#X*FXqO2g3Sf`UYT{i!3}B? zExSnM8(Ig1U~_nrWr!*+14uxnvx zc2OUrS3#=PCt=Y{LY-8(n9?}~Gn8TV5piUEIfp^7TGt^|Ej;VJ==YG$Trhpd_6Hm0 zjh8PR%D3WMtkRsnyb)z6Gig;V=`wsZ<`Dcv3M?%{SDFwqHC6U(fh6D~fOlb??aHUB zw;VGEhfI58gj7Nurxw!iJVZ?*N@*}p-FXxuaME^ap)q{V<4|%wGusok3=`GvG*_U1 zd6yoRojY*l}oN?rzrQJngV!+kWZlq z>e<&?gJmQE5y(~A7;*{lckB{6gk3;{sng*yNKsZeV9r^ykYrd0BQ)FGHG1;%pw{Zs zr4FBy4K3SLIWA2bGM5!>#~zD*W`4z*4*bu@^QL!XNf}#xxl#&r9$)@jSU@q79?$m@ z7G(|XZ0-`VGfOkfeW!t3CHFR>ty-)QnF}C%cB7mv3SqX|vl}_2H40)&u=IgO2g?C) z!xC54!k{%~F#9m2&p3ze>%o{zBI48|)OzPROs)6SA@TctK3CcfA*?tbNVh&+YUu$* zuUK#La9;c0hXTVwys>ExOS;nl=mFwn1Q5D>X=(v&M&p$mEMLH0l06knG3a#><$OGl z@w8a3TqRAj#fU;3G!&Rx3V5*!h-{4#&*_kpf!>C_BbrLe#|gxpMaZ-eiW^v32>w&#@;y&CXp(%8nXv&S zjYVRDewO|lo7TTvsCC|;s9d)by1R}?#S6m5$G1mW++xhVoV_}p4$r}cEP55yQVG zWFJa1Y)L?So9ox{(i^0dEVZqU#WRtV29{D#AeF3&3a$It36ILzwe6{^lZP_l&|x z#4ad6l>6(}SDQaIkxpqcAlCK8v1mm)1OfOC%CI4L#tc*+SQbCDQMQhHH79Rhr2fOI zGW+ORMXdtAp4Dy?wQC5MWO@p!n2;0dMT}Z(^C<& zk+l8EGDt^{$?BUhAP`s%7*TzJP_P7TZvb+?rW`>3+UIH`hI-Bq%U$zeTD<_k+z{#_ z#oHXhS6gzm<`8l2HjC`~dth#vql_W?P}*5w#pEQ|hPBai~%@Bg2gx4BW{!raxO;tuui5}DUI zO|6k1#Zo#yr2r13oWoQvZB!ex8=m_Ht4ZaYRadj+R0n%zJq1*YDJoVMBcRjwe2fcybg>yrShiZj-r-^7K4c&CKa$oCBCJf(ZGsM2~fn6_-`i<#h)?9<^R zhZvmydp`8yDTgZznv4WsZZ*CxL?Z%b>B76E6be6tM#7YIwg{+q;;lrCoimWkBAG9(ogi-5Iy3tvH(D=nft1se zqj%AbZ&P;wED1a#h*L|j1n{%%@Evbr{pPx}XFtfu$EXzj8?({&}L>THgV37JvW^AXt`8l;iit;#m`hSXy6 z7sxu4G%UG9&5greGLbISvhQ60T7^1M^ z+iG+$4Kjx*Pd{%MhbpH5Hi?H6hpf(P$=+Dbtq-4f){UJ5QWbRzA3+*E)gyykY3WW_ z^%y~Q79n&#<$TIZdfh?F~I4-+9S5c9OYfrmQawYs008ATc~i<2Jnw z(>!!X=*KFH2adOVsOfA$4i@l8%X*+32=S%zDOgT$s|MIr4aL?=uHrT{sMEZJ;jzZv zP=!UK?7_4aQ=sG3l^eq$)$={4XxI;ne0}q$#tLcWDNLQr8@BPHEk(CoLn1yTWa>?Y zFovcnFi6{VYlAz5P5lPTFPxV~Q7lR=ePjH~9~rjUP6$GZUM+#W_mghUs*Dw311X5D zgP>i^T_mdi`dM3i+%l-MIsNW+F6levzmLp>#Lm^~Q!J?}t^Q_aT$)#DaZ5SCqj>Hz zkxfkjoJ=%OG^y&v*-`+f`0IO4>QHQQlV+V{>fv3)%=wMAwP>183~@9(XHN zjC!+Ud*fgPV`TO=z9TJPCz(B$LT#pA9Hcv%fg_VdpcdJVw;$aqDCaCftg4K2RVlEu z6D6PG!ioxonpy;4DEnxw8gq7*fFbn_50(YDt3wnC0CW$QQM?)#)N536+TZ!ghE@l~ ztS>b;l`GxSuS0PtOYpa)Ea~gE%as7DV$pQGuC17d&N}laYsTy~-M=GZ>rtq6$bp{J zbpevG4d2o*>ciH)wBw~80b@fPMp)J73RKc<`$KxLcIP{9rveO-K+Xp0s(EFny0Z=H zXX?Y0Mqt!fO2ha1t=Fd(24z;~{@9l;KY3;kqUY3@Yq0JBWkgD6i_CrG)9P}6lC?}jaZuKC<-Webtqx1}U0Eu2 zvD)4*PG48h3Y1xV!zMS+f_#OBT?@ogGG9&lo;Jq48kY?*SfjtZJ8o*f{g0a}&S!sH z=#)}Zk=4)nD4Hb^s=z}y3ht$NgO4!0XL4VM@R1n9B9Ec zEf#6)*z>X$vB=-p3fl4^?PN%y-yV`4VphLT0O_aHqpJ?ipqJeo`N;QdZvQx*Zg|en z-jwe52t9ewac$ZK9fy`uu62W*FVz@t8L(gsjMuj{kuiK{l+t{lf>YO8G5`e2@dgkD z;NkO1wJxKvgxalI1VU!CS3@Y@6DZ^OOMZ$3{WRz_an{)jxDW+owpO3RZv_7fjRTFZ zQI-4apsTQ)kNOCLxP+RvhrK~kM5e>+^{)SCbJx{pR-dR>Uj+V`Z-qy6T_j(+CU#I$zS!&%C4|##cQ`?=0@>3C|#*Kz&g%5 zKlWZNx^Vg6LKwO&*N-L`h#fDc0q9nPO|xgrXZY*-2G5hyqrGxa`g|Z*A=D;PZhZkl zz0ur;E6oHv2~tN7p`h5gy$rtu4!e%l?7rj8*CSWXC4)j4^>;2k%C#)p=xc{(`E3s6 zCtEgW^@M!Pr_LKsdp_N5+4Jz$bHp6v2mJ@++5a^=d71FXW-y~{_=mZGD&5JD0-j+c z12TwOO92YYd5tYLV~yGtV37Fpiw?>KeLie>O~#hj^7e zl!lPhTl#GwnfwKv3Y{HKQ>k%J{nE+`+=w|@KVPqctZ$bluFgI9P`$L%cW2|4WA-#1 z>_%bl>+eaCB-5J%$&p;Uz$&-ciL$skx7e5k*H})VoQPf8ZCF+`t#8_wea5gQt}OHC zdT>nH#h>;9io=vw?#vX4P>X2{=K~W5Xf^7bYud--M!J5BxX|ge>FA`dD8)Uu;Ehe8`7s>}*zrGeC@;0|r z_ya^8${faF=d)`e1sM>MtN~yb_!}|#Tt>YPq=2u!O|Qk&Lh3n`94)4fUP}?2jwf(C zHkrjbx;Ai(V?AxUxHhx!u$s0r|2y^FzvazqbZ$pG=zNl>9+WSvB`BM?#ZKdNe(CLD<`tK z7u~zYzmqg8B$Sgj9sisjq}K|#@C!XpAWrp#RsKmb7g}X0_>@1-#VKEdrBlq=T?GJc zQJ1UJfok}_Y6)1XND;T>2b7MMGzvy-E%U~R*!eyISBpecxiyqKWu@tsHnh}q7x{OECI+%3SS@im~5fu z|CJ^Fy5X8~vMrFfq#w0s=MFLJxoUqMsLV4qggtImyAFUn^&t)l5*Az3N1>GJK=OUt zZjP+I{V^H-kjU66Gv~>Ify=%)~3*Otm>rHmYboh{GPHG|9A+z zk4J^Sy?J~~_?w%@nc;7|kA21=LTsJcr z&+d5{mkqN9b(JBq{R_AHEwmb@#ZHtLEhg=`h<0RSo599>8w(CcUCSJDg&j+LG$)Mp zk<)8U+Ur9|SpWYH1M9C#4Poaq1?u^!2Yr0N&DGWbJExY`9?w&=b1$`G)d^_e-V8d-)X3`RsVt)=upZhoT!azMN`D#X^#!r2Z6?IVCldKQB;rPAD(xr?N${p8`Kq1ZLN=IwusXQu(ok6>n_*^By3Q z`Sa&BB*Cen|6ZBv>rjr@lmQF0Zz}=HcdY(lCh+^wB@T;v3P+X{;IV*|pJtU0kPQs> zniWrLcOBxEx&iUQMg%Z058Uto(`<9w#A!Np;YUMI$-Sraf%>GM9=l9MyK}QebkLL(s(U(cYs`7h=OlNV zIZO^k&fPkQjJof8V^nVGr28`}clDveO8s9x-|Kh%FW0qeTzJ3tdOcr{=kxh^KA(>_ zX44DRfqMbBGa7v|Tlo60ZTxeaURvH3>U?RxoooN)$);EJxAQOka2xvK0{;@cX|U~r zL(Yfh{X;oZmp;v+b4xH5l)$tX+uU1Z81)H{#rUF1u2HZl^0_7J2}5CZYbX^%LU3@8omjYbbgX3^x3X9W*F`T6cDuy4vw;yMzfW~3~NO>llrUd1;- z*DIs7UbLtsifwp1O&k;u_YZ+^u z%Bg*AVU9yvht=ACPAN1W zaIjR~z#LarRb@n&Fc9*PfGPo5(*eM)A)j^n*#zWuO&*&)f!v2Qza|eS9)Urs( zFh5X~u!38_IIUsM)7f&2K44lM1O@|S0a8~ntg9e%EghZ)AynqSNHyC}XQ;+atE8vXqnero)Frbmje z4d%STpr~Qrs2n<_eFsG>MeYThAit`igAz+N>9daA|A8@(J31YxlEFJ;Xto`>+5xwm zn_^sySxu=4`8BYbtY8m%O+|zkdSe8#s>Dq;($SWfVRv@H{H4pLQ+|`Q3HvoYdAW5F zF4y9fM{*TaVyh!*lZ)Jy2zk2OLH0*1EqwLLa*CZ)m^lSVuSGl_UsW11UkQv5IBWTD zVuSAWDnpQYyMxod0(9Qi0KM!_Cxu&Zh&W*(c;aI7uWwG3t^01Vs@%BXPr-u+c|}Hu zqGN)*{M^aiFVWMgH+nBTm#*G3wEL++zC|AAX~Ef{rQfz73CvLbb)&&6pVJe6Uno71 z)+cSgH%U6g&nsELPr&2eAGqjWT`d6Xpe)3+7J#o z(Ff6&XdKoVV}c@P129dICMFW|_8gPD*{W_(w^wio$G>nNCnyvSM;NQ*gP*-dy=Uq&oe*_rg_8M z(lOqM2JS*Fx%k1^5Pt<&wN>@A+-m4b5cD6VQ7?JfYlTq&W(pD?nxNI{+7VxJR)s_p zV!}5O7uxi$)z%={%I8G_*H;sV4~G_*KVK7iQEToAr#ZFtz;e&XDfqwGYp(9@r5a`) zM|~DDcQdw0nKj5~e;YJ6O|x^NjJ*zp@(7=!Fbde!1g2M%*R)>U9#6C@NbU5$S*8(3mFriIKP!}dd;Tui4SJ>f82_XHWo*J=5*?yFB$HC z`5E1&gK8^m(F{Sc%xW*A#$58W;m5mI9ya70d9FgBnmNayqw=91>cYzw=;s znQdFC^38OeAQvruV=n&TmFF1q<{TEj83mg*2pBtS{D}UXenB^PCi1IaU zd6VGcpz@8QMeXfjKYloOOJZr5FiIwWG)ucWod!TDW$u4SWI&NvSpk3ozHJ3rM#!YF zk`|fK?juL*D386PK}zXnj-%(d@$RN*$TVUPB^x<7XRdQ z03V7b{?^*7|84WXcfCF`Pe>*ki6X(Q{?ky7oi+UITfy{$1jL5c3A{9rO7AL`C+Lfg z1#1-N!w;>t8!(Y0CT~6O_9Pr)cxFjh$Z{*I+Zv4reSMI3@^=n=vgxmczbi~bCDZUE zh(1R-XJts{#y`}z%?$WW85j94*%%kjkGDbpz3znT5#FlkXAl!K-keVdF`= zeFha!&hxB*7qyt4O4zo6h7_FMI>r4ji*MpzVB zqe}*~@U0FiFS0?HqId#)yzr+ zVrktIZM^lbt~Yw2D)ZKCs^(#B z{m=#9l+`0?TZH-NbpCsg&mK)-$bJ6h3!4*?e>p((Rfs5W083_ z&MDFg8d>(O?5R-+o_ku5BfWV#_o90rI!L({C9^c1=p@C2pJixBSl&+5by1j;&+}I! zcmB$GWf}6}kWUueCq0`f5d1*e?Xz3AdH3)MzKnSG<9X-3&}t|@xBPW)T$sEf(7G2a z82ARP(*Jw$Yg(K8pIe;Ej?uTRp1Ezj7X5oh8h+E6?PZL%Q>|9MLyBjp6qQxAslzDA zy$5dU@Dx^9ysByGUdGp7Of7pplzuDu=k%GB_3om;hJBZBkMC1|S-kx*^TcoO&;70c zJ8aE*o9e^-b9H5>@NoXyY}QLnO8d6}HIvfpH)?$!nA(6Qa|(~QI*4v2u=mRtWp5>+ zVct)1{a?iun{T^#9&I;$e(hoQ{_oz#>4e`eVLjh*M%>ATk=okPPDC{lVVUJy@4w)> zd-*k#^2+sIIK%Gx9h?;F$tnb(MA0v$NN4P3bnj9ulMl${;*7v^qAdsMui7$Mb8+ztKkMIcxuYm^Hks zs%LlX*RUV5J9e+fw#@Zv6aQ)i=0_ZfQvoJO=EXRkkn`uhaKZ)OTg#Wjp_l$0`TW+{ zfiE+<*;qWwU3~Oxc-Pm%T?go^rId+Zk3e-wEY9h#-Yb4Lc&p-vtSRDb@LI}^9RW27 zDkC8m@!8DFV3csGIbr?56a)R{=G4QUv3@JQ6!wA_#t}GQi!rNcN7tE!bfk0F_1_J- zvdwnyMk~_Q6LqNF4biQCyrKZQHxP%C^WT(Sm@^KncOfcFs>LRPO45akA6JsAsXAi1 zeNEnrwU7EjdN?KX0F&%bg-tHLAkUz$J2XXK-F5a$!?!<`F6kV zf9N@vK!heg$pq5ATjl)zL2~=-v3s z`VVutr63mjM2j+##n)rk6!0DvoVZoXF-0~VLL>`jE&tmr9CEwIprrHP3buT!VWdQU zYSfNjMQI7thiFZD=GXpy6^r6~?A?>!5a`+7?41iO&##X41Dgx*IM%PvhLsh6hFDlW zUmNq()?;_K9A&pNJ!U+eP@x$nCi;BV~fzQ#WbZD$j$5 zt=|nwSRrkgQv=(iFU!7+F|s$sQcVi;28jO*8p;MY%~t!kM6O+LeV-r9`cxm3f7|0n zO?EB>3&xdEe%MaZ3vDFwKVSMeBye%f zLt1YMS{=mRm;F=Y&^L)Nf&dlBmJ9u$Se}k;kjM#PPj37npMpDR;2z1NcBhy6*A$8l z(9qibde!9k{~u9)g$uG{N^9p0ou3x&@g8%ye|v_XEKeKhh@p|&t0NA-ANWqWtwlUI zw9Z7Fj=F7Zx9x+@aT|FJ8G$(#|2F*{@Fuz5Q-Rp^TMRZ>eJln~h#)72g+wA>Td&@M z92l;)E<%bQeHRFk#dC|?DU8mBwgu&e+`Bp^Nnar!SF?5SBDzXloRP7y|6th|3ipGqZVtRDDn zL-O~CJmO@o@qPCwOYi&H_arH`5sLdkr(^y(y-U;B8rUnUjiKDY7_xl1dB5vVjZy3k ze*C%njEg3~uNHi>FR_a;WWP1~snHHG>C;?8X`cwM`Ev^HeE3~X9`w?(=eMSJ;^6iQ zxh}*4vbo~DE~El-ydp~vvV*@uD$j1hUiq=lpVjlb#OtXijtU?7sWIKuwLL9u2LMsQ z=II*L_@@4F(4Asyk3jz`3VbWUS?f)C`B_QxrN@fT{26YAB!ZM~ZVC9C=&2PVuAIBy zG12apKH6d8V@c^Mqhsq&lQ{hqw+;FqUps9-E&L1xU$oM@d=NdY=}AvXOi2_q=+taT z);X3`>oBC6Euj1SsL276O1}dBpBL!3ag$R za-iGn`Tk${M#49a0eX1sqfo7@1NZ~FpLbV>je3yL#Jh@?N9`yn>^LBdSjj=uiY}YD z!qTfhHHK>g4Kg-_yiDIfVc$c1_(1$1=3}gS^)@;M)32GQMh@~um+yGmorNI*+`i4d zFn!F>M09Pr2zhe4CZjUCRY6X7`$W0eGnv$p8N;`#n}Uy;MP_F9yw8>|GXbN)n}7e| zUWTG=avKl;#d}`7S0qhr?Vnh1kd->c_iE#irQTfA#NG%Y#B)71*d<}g~MLCa5#JO!w1{&NVjUH54139G>#Md>tA zZMAjvx#cpwth8r)juU$ZtS7g;v*`b2CqE^c6-vUB zrYcQDMoA4*kw?>X_WWCvXVKNOeAAmNOPi?&Ry9&vaC(nOg$>o{>iefg>An&6Q{%PX z6MmmjrCkjTvC*C1E^0&ag~*>8PmD0A@eNmRGV4>|ZeJnK&G!C9ow+sjN2!l`>(#o? zA)llppo<6akz2=+x7*)0&G8O@;_AmvZ^71|q79y~KFi7ny)E4is>f-dtM`Aj!kg(Z zd0WM-C%VjvG-?_Z3IFOyw?u>+bf-#i#_s#kLzSFCE`HB`{w!^4*}uhkvc3I72i}3_ z6Kr!mjpewd)*Y?O6z|K9-12$CrrSKuY*(Bpz0;+6u24AD)fDm1*BFz}l2D!E+}u;g zFYliH_Djp8lS$0{RB^RI76Werv*(g+>3`=LEo=>rDd(zD01{{rh-lxV$UjCFN5cC1X)z>1({UbdLX(qQ6#8Hl6Nlk#)6tlrHL3b)s@^bWa^+|;XpNF zyi^;k&YP$Ezak<%ZI5_eI3e8H_c=6Cwf_zOY1rhF!92EK7xWhE6Wh1woby}bYf6eV zI#F#(nvHqW|C-eumg_bZp@5JgJ55%sfjd%&fIG+2WCOiu=;3u$XPfsJMJ7fZhwnCa zk9TBs)fg)ya=jhxqo*31z0Y3h(>!tR)q?GV^D&I7;EB$deGmBxMC)okFb2W`U%HYR zt@>t%guqtd8}Ffex-1Ijgbe?vjmoWY{w>+ypviG(b5+#cy)LaP-B47eOP&f4kxAh0 zo;PhI{W)>$crOllAGQj#RGCOnT{VVA6J#D21UIi^!Ds8;7H-(!F17;fo7c+s3er?p zz1^@MyO@Wuy>PmLS8xd+KH401c(rMc2THtRu4lu0oWNQ^-pC(vNeqmm656N^kn+0H3jX7dr1Ocm%}0rxg~zzNFm1c1gqPeMIka-1_m2uHl)?0$qtllzFl%T z==@2^=w#3q`%5s4OkL?xvC3pu*W=h+?w2QuQ(@T?hlfwv^J|EzH>a&*!#Y-WyQJk| zx5J3S-4YEU#h8+~bE7cnBa$S~+DwCX-Bpi$Jc+j!gs@|bmxI%*( zr5!XL>S!noxLngekgNRYj+zxkCW#(CHl9@(^_p&cbI?wjb$`wtIucrUNUulK=zjwK z=Hr@KLIVFYyW?6sU{4Zrp3?5@9*;ci)La-m`zP*W@XoJ0jCw!PH?}uEus0ibMjpcZ zf+i*kK1>1y7|>PEe3w0+w?mBB>F<&BvevPdJs>dIeQmI1owwqi;5QTG?k7g}8-%m( z^2!*UgVgp_$8YmcRCwjL=YWi}UwcfsANd*uZD#*&yAL3q2>FvVUqso~6)E zW{j;AJXZi1)PC`#za=YFB)S+Z@8qmiuHa-})N4fLh6Hj7;u>$$NuyrH1bsKOb^BAi3`iY+=B7-2;%8Fvx6$g#YY*|O@32R*_b+-|VUEXt`DphL9nq8G zM(ljP*tPLQpA*Bt;D4)LBS1czLeVM4D}BM3n|WHrYPuqh?=eMxwBekuPu^AUUnzkv zn~{55h7yn1VRdtCxRf>N2oLwBTTk>PYu@!0%4bEv>&9Wnvl&^ET=by*2X4O8K-p;_ zu|+llR(R3I_<6U(-B`qk0bmb+1561ksZ)h{%SU5h??^Mc+{p>~iSwQ~<+V`5<4(mFe?vyxbNV>UJmh z3Z=c`iWy`x-_q^7S{-5RD*-VEd76`Dx7%Hk2d&G*oK)N^-u6qt@I}Vzu6p+<5M_-h zs4n^JB3dNF(^m(XA5q6dEAuY zE<4&|$%z3|>vf{rM0|RrKz1{57-E?^Y4=A>Y>nMc7}E?g}3owG1LD|{}vf*qfrD9ue4%8uhv`6PoMT>SId`K0 zDyaB34A(k!b6w6sMlbx`!%ZjC*H$z9B&J4ovlD$zen&T91_Ni1M9>dcfB6b7W~ZNf z|KpLc8L-k1mgSr*BCdH^?APXK5-zAj<3+{nWxuetG5icij^ z3X2n*q3gEp{GnJ-lucLg6r(eoF6!FZkiP!m{m;V$FkD0yT)&e^j=%O#tT40a89b!K zmp=+EC<)3e8EgG`vgJQ9ZJ`yt{`k03<*|6P2$9kku2^n9rJLHoKv z=na>uy6`0HX)tE%#}8t z+8p>2Sz&mPncrjk@`(|#Fn3^jZgTbr8rLF|VeYg@i}TX($LKgo6!A~uLwn?ucP@fL zf^65BW($RIKmv0CB!6;HO4aB=|O+zrC`E)jM5fypgr= z+omWZ13(YM3@!KkRnYT5KYQxN$8_Ol8c}y$Yh%j(X{3=m&Qx>FW=YWr_QUJ=YO5_3 z|AFtk$ht4X?pLV|Qy)S7?;7eP(U#%m3Qtb9wi~VzN z*`1s8u;`4xEezn%OEZY6w&N96)GtVP0M;KF9;(go< zE8%yqv}1)~$f>koWkj-V30_=r8wYAd8?tG1raEz7;QE3uXZ12W4;*1*f3b&06loWH z2qihYOA0WjEc5NY2juS*I2C4b11+m4)w@KhA2nEigWkqd7%v||ZR3|k}7XK>9h-u@@zC)b8K`-WQImjN7iX%PrfrZ;YE_U)HgjW2HhnPZ&Dkkv| z2gpzb%n}k|Jfa~{oLqdG6wjarW1zWgA;>^V$0~lcgt$T4D{fkXKfqT^SVE5S%ayW? zjiX5>U3Z`7{8ML7hsKPmU?nI4tYq`ij;u0e;g%Bbk*P9tcVfVTHEi4YWj`2LE}-Q~NISlhvRt ziC=j*+3oR~yrtsnwXjT460J&A@Lv{$aj_IowGTcBoxIFs?i;g9jy>59_?`8W>aJRH zxx+8PCwnGszsxuWeu(zIFS77P_|7vwD!J6MJ-iQ3ePhr%DAvHwEDK^53F{Fcu!(RZ-w#3e~g!7;X zgdFF@mT85B;m9fE6bdBp4%vm;McGt24++<9|BR{(lV6(dhWD(eu5{4Z1dR;0s3-TN zUC2>CA$9PsaB>V+XDv68PVf2UTW8#d;V~Z1Vm1bDR>K(x$;5u<1{3i3Tp94xR!lX7!(EiiZKyk3moup4&vT)u@1XU8-}2yKP5pl z*zi@rv{Pcj_<;^h;3U>lY8G*+;8@|9ELG}}tM}rk z#$Uq9Z$V);lZQDmiF*+5P6`aEvBNwUyGOcRNFQ3_&+G?hhhSkw2vM$2(< z<2QK=U4lGE;;X^N5AhYu^$=(N6gxW{z&l)<1Zv8fDz0Iv6}uQz<(^+&PZ9CrR|tWY z#!|nrSCfC@*IT^s(kmWq{&U->MCC*Btqs@z`T`~>pE}hIL*@wbg?9zxw6|iHgI*+sUL^aLS> zc41^-;0A_`_hSR1#Sg`m!8=w~1j`5ZYCBE_T*A{;jZ0bcFZS={EP#Q}di5A$3FEzc zR--)%emF04u5SzCaqft}Xujn^Ywa(r@`pJ=Ju=&)ha;LVi_7p{_J`N)NE-dAF-X4B z>X)J~`l<1IW@TW%ik6+X~5(bD#TNhy$DiO{F(&v6d2i8F|HXKZ&C^{lfxF5am(ezV8^;%bdjDHo3$qy@IRzvnb47rXTX0DUC zS9IcX5sM`56nWL;|F|b`fCJa8KuHZ}Q22dwKAvd~PJJII? zQyjhVW@2-D&Dq0rF8ZeiOub6FOcm4VKaNLV$X3WLLW`2^p9r0X%+_4z)cgExn>Epo z1C~oxRc;;QPF*KHGRWue40#0uUY9s--A?zFVSZHUAd+gb-etK1sxK+TzqW5f<=bta z?gt#z${X#;LazxgpA8zUZLT+xel#NN%s~&#G!C_PUN5ypXgIqSSJF1NXxQF+dh@%> zBJ79wO_CQC=uEsnpwfK9uf323?lChUi*da_IiKw0l^76);QP+S`7|tb*8zCi%(xzM z!-5iGboAX3Dj0v}f^O}p+pXU5Ijbx(-|d0kAft;;AKqg06&b2m7yoBe*N`4rT4XR# zi(A#KPLBWPaU(J%Y*`CjM7*R0rr4K*newFdV|?{mby%2TQV!YU`04HIrqZj|SASj6nXwHPU7loOewGr#LlW&+{;APi-seQ5i>N}15 z?Ll@b&7|N-X_M8B=DB??%=m35;vDz@u>@b0I05Jl$QkV6@=y zB2N2$Zr(vK+Vcc4)?bqsdBV2Cf;tW;M>&R{@W7-;u*-K`m2{r|PMTDIs}|J3_dpLs zm*uRri2FhzF1J)gpUK!O*Tsg1NXWxkGjwZ)ichxI`jyCL~LrJ z&AoW#9J~*V4PqlgzysOSpp~I6Z%yLg3Eb+HK;M8FxRfasKYNoZqVB5Y184MnV;_(K+iiY$AgOK{&SmfF1o7IU94{D~W=E+H0)Y)1# z*Mmg!kQBR->%-T$Ne=uapkq*cd%67xI9Q&iolz zG-3?tMDPNW5rH;KE?|iFiX7CF*&s53y;4L=9+h^C=8tcLyj17|S zL(u5I-e31_Q@5CaYLoi1r&C}Qfa(#SUKy1*nNh2v zxA0TU2Anr>ubpT~vjI~lbIUN|aP+lq(efI6ax>E9IX#Is^w z2MuX!dQcbr=ckPC^k2_A0cwDO38pB%bXVM@8Tduk$f8BmyV_NrTAa0S!z$cm>Tsa^ zAu90r^;2%ZooVRjaHzMYiWJ#FfM1tj{Y7fMJRtZ{lcYI^h3@j`*yXMc`~El0?ysk+ zILcxlpPq0QPg>K%2IOL#&dIUJl!+xSdN*&E=lo1}aNeiTDP|*uRCGwlcinhp^3CZa zte?V&)op83dbS7}bd08bCoDSv9OEq-Ho)qE#LnDP5S?}=#qB`tdO2p!Y_6NZ%djWjxt|$kxF=xU^1z+j?>-Y3FwN4` z{#3u?KbAPxaVxvhQvPu?t7>kmRNR;q26bNP_KBma57U1fQfn%A-HCx(cuYu}a9-~u zHfrA{(8BExNU*9gP|OBb6)W3<{Qutd;^Flg2NTSEtMts313r>Q0I(!Rbg+cPmW~9n zg{T;k&A_Ev`EYI^&Y7pSsJYPgze_=L{c98cGEtgY)Vp0=)R(&YHxU+3jNSXJ?G+uQ}%p_4#`M%tq#SpEZo}})tGMO}sJzH0+GU{T1H$~kXdXZ){`*XE zNs{68+3WF+RL^*9HJE+5R9b1;H^r5Bqf{fx98f3zoQJNmqc>DaMpf zimOp?Z5daGO-9rtJdBStoD3gST-pMv_qfLGVOYlMO7cACV6O5azN8;{)H!&kUGORw z9s?edoBUJ5aVaTu>lbVdf+UE=?o(m+5oH6U<}lU1OR+(zyxjy8ZN#z~G5Z`bA_cX{eE8iIdn zpg<5DCqkPQyqB@67;+Wb-NskClqO3`S{QlKdD>^5#gfO`)fuTMcD0R22 znw;vE=IPxmY+Ffq(tDyuiV!?bTVY{wHG@35>CG-mlvCXMDoPb+-n^Vi0e|P)6fIK= z` z?1(4X8ASTFwlu}w$ZyV)FB{V*cMY4DFBW)$aA!u4Od)_?c?&jB2jR|?Q*0(m2r7aK z=DH!H0Swn^RFbe6%~f=fb`g+oBFHb4LuRc__-sCc42ouI(bATF8Iri3ttG6A6| zLPW8B-*BC5J2e?{iG;8SQ;jx>UJLJrD;J zqTCu4CHN*FgA~K8P54^{qs2hU8kJjHO7L@_y6?p0@y#oI!CO#QW-8f=B1I<`j6pgh z)CE%gKpQpi$bYt`aLHFUOoJgljL(K0qY50_Mi zd>*~1Yl?9zS|phjfFa;4Ruww~%x=~}R56|t{Sf`Iguj~A2k0(^8yibmiY}KK$nRgxTKf2_>{Zva3tweZo;UZRZY(v@3rN4fS!|E z`Qp~b{u3Y*94Scv%#B-?hep*HBVFHX5jNG`9F>H#QS@)f;HOylB>0JJ6h-nhMsB4` zI!Z{O0J=rKQC-xCNJArRm{chk(gs2~+!={f(Z4>hDGAav&#`xz({c0>=J+HO>lnPt zaZwzVrU83`J$sSIRGM|=y)iGRB&=1&n^{m0Ad(`&z)2YCSP40KgaEKu40#P&8HM5l zts4)B*bK&&tDNxql<__SU2N7)pe|N!6_+IFH>)jZ*J@b@NA8qtIuCt*7GBEz&sdV? zm}?|#Cb^3GLtD|WuOhdbT?sq(MBft#wE*!I^de;?aSX+3r3QfO)K7rRqE%ot4<<{K ziN&mBCM|0W4`Yb2v>3i#CWDnFPskM2glMLEUzOII|)*i34pQZg44x>Y1?|J^FY-O4O9&MrMfgC4sE8Z5#GUt+IB!-GbvJ5ekRzr zEM~h9f=R^Hy5dn;ECDxZwSbJ(m!`R;c<7pfF+HkEdzobS_nFlj6u(j_WXNGY%8MHE z_^jP6bvqZxqkzO*lti723ta@Ci!(={s^aJgP~!YZ|0;@MlvY}_%qF}Of)XI|R!O5l zBZi7iXVAUC>7{_@h4qCzBT(I-dxSyjrJ6qSWK+6Q!@O(P7j`EtTWd;zcM;Ldx)m0l(QHM(LnA9wtWZV;MscY6 zR1ha4RI)tNinrS#8~Ch@R2o{Y%Rdkp?M=qgAefnCw`>M6-M!RX$D;Rb)7z)s*FGmi zisZN}b6BlBRh-DxL1b1afS`fsnbLI?rHW#@B4>uyF7!ZEVky32Dh3L{LdgX+X!GP` z(83Ig3AZVjKuanq*3MC-Op|&NKS^S9(ggO-bdOI1W##~5QMD(>wP<7b$ zv~cqri(V>NMfSgoGzf4qX4M*$sF`|s6gc_bW~ER>K`nR(CYcQrR@JJU%~QpIYGkvL z=@i~Bcl?}o2ZTfj25iJJwMLDT9i2i3PmI#2gvArFdig5LDh87(iFW1#IX@3nHI~Me zsi4G!{OcR)jy!*W!*)FBBiUQrD*J?0GM}mlO2dTV`dus-Q(@LU4*a47Scb~2UQtA+ zi!PC4y?&`n$~^bU;^r7ziL07x z*OY^EkN^($6;EZfD?#pT)(+(FR`y&=3}7kE6%XSfuCUFp76DmQ7znSl(r{=h$<7le zumzsa3=74BX&Ewl-mTIS&7>3r@oX2(OXidGZdhG}@?Y$`FynT*l@s)N44ZlG40hQX zsHH@^6)wVNQbmd-;0+6Er*s)&GPo*5u?SoRgAUqDpfS*8U|&LFRJ?7W9FPt?Vj4=j zlkAcXsV!WfLEsz5%;4$^0Qxg2m0NxNfM^<-WTxv)R3)ek8>8aQQx`|C>Ti(phxD3o z%biaMCwx|4s6D24?|0M!vvwXu1(CHTU4#IlT5JGKa&BP49l2N%C5xUB35Wm{bk~ld zs6dh_m{r9QyC)z5C`6z%5fzVsv9aFyX~5AvCWw{w@^}xIbgW>;dMJE{|_HD9sMno}gi$GU$p>|1>3@3C|$QzGgXuuf}>*+Mq0xb*Zt3l+O z6fU{hz9OYw^KE}1CUQkSBLECSQGi_Bq*C+UZkDyvJmXneG`;53V?{RTt{Zk2USvtt-((8ZC~(43_5s{XQUTqK0W@GAKMv8p>C+ z2t#XkM5>J(EGtQfdUs0{#S+cd^ENY=&Y=}W=EXO00z3>Fsm3_tEkgOk=p+wRotQqi z*HbIDMj~d-9~NOZ<_v~uMxr$iyF;A{4$a(p*0`ig15qFs#Jm(h;%TKSd1UM=Q4$JB z@A>5x$ie2zRq?hRS#pU2HqxhyG%pq)Y*_huV^l9vmV1;|Zk7j3HisE?P4@7NLV1{H zMR8aHoka>?l>{-!R;u+1#L5PDOJRAsF0(bhunUjT%Jh2^-M-OY`fsvh5cWQqdHvsz z!{3DDQnpf8L#{6hBm&7%aP~-Cw`_T|ThudTX%1A!LfuD5 znl$0T%MP!1Mw+E%KMvE=BAMyk>rW~;onMD>296;i6fIrU1-?kac8Uv?;~T(nNlU_6 z=BMk^qenUb1o?^%G%7mGFf_N6T7h}02!Xpzb6s^6Ptsn5kcepvezK)NxO}*_G$Tbr zRwL9d!g}kpuNT|*5rJ#-JZ{AS$nF6{1052TDoU-Z*P5Nn{+3%qXw*4eqoD_=mt^fy zdq;X7HZ$R-`G1to=H*J*Gxun=65O1=XddOOg6Qg{(#3L1SIyu>7$ecu^h5_Rr5l!&Tm59Mm&ic)Y>Y~Rphc>>>9YaIy)an=lfmp|30(`_ZShhM zELRA&UQWg)Hbq1nCK&~u_K_NJrVUl|QP7MApB?VRkejrYkh|wr2 zY^p1D%oXC2uFH_jL*|GuClHTwp$L9Gkkj*kF};8co0Az9iLjx1Mmb{zZY7dqm_!(j z3;GS9+^2?w+7UVEs6u42292z7m3r97* z>CCxPsL+#qDmaO6uKcukR$i(OJK*_O^9hanaA6B`WxW!8`rHKk3^8maN#g-+zjTaK zJeDa^IRjx(xupOB#C((U>mX8a)H5=#({D)_0i21_4o!B}QkQ7c7}-l#{|r5tm7flC zibU0tF;BhA8$yq(3k6sXli^{cw()=p?Dk@<=L<g; z%k`k7?l2S^yqSYDh!`uu7up=V8f?R0IaGYJ*1T2YsWUZ2cnT~oyzZ$5l@9b23u;|~ zNtz7_nfG$guS)zbHo)O%PLLt&oRTLqf zg!F_#ZHqiK1Q5Ls%p3UW#wfIj5+Fo2MI^NCAf_d*XDZZ^+Kwb`KS4zY4HC?+;)v+9 z6i}L#M*+k)FLnX}DnhTJ*GxBZ+R_=6L#$A~E8V*uSb`Y1{&~%L8Q8OjLShbX|V^`|dc6i&BQih2&P*G^$ORVy119 z%**8&7Hu_PJ?BTXt7(;*c6aPvq0u(bnYLny$y5W{kj+`RO`T~STi`5W!E&Ulqr4F| z0GyZ7*o#FX5Uz4mW-utol7WVadp#AyMp#y2oIq197X{Rq8}&p$g(BLCsA~$E#K9n` z571Q6Yh(5T7gFG!IA_JC?>UPPXt1D_+$GglVd|iupPauHF=p zlMM1xy>Q7KERE1e)tkZu+FDk6=vv+e#$6;*@W<EZKZ=cTwH6qMOtIZ2};i7 zCF(7mAv7ZDm0}T}3UlH)f$Rn-0|VMe9vu&On(9P#L`HK)CQShD1Aw>OMg_%t!FVwU z@LreEECkSpr{GWv`KdGkQN=OSQ!_ad;2?!uG$OwbDj?9Z4>Th8l7VjH3SH^0@-8Xs zEGvxf^mNI;A3$=nie`_W$(@2L_VL#}(cF9WkK)<7TieRsRc}GN#BbhkM6AJToBc#B z3)q1pOCFzXYFpp{4X$KdrYK=mv&t0+1OPWgpRn+x%V&Tns*RE$@~`D6qeh8h@xM;}kL=IMQSd6dP_}6dzNBn8R9S%T+-_ zH$gQ9=zo$8Rk)J2M;#bRCxYy#V(ZnK!Z_N>f|{?r*Q;A^;QKH^2N}ujmp|L7*9m8h z`gfnHq+uVac8pJjj{?sw#{o5Av=ZYA*(E1~9i@Ui52aKAl7Inp6CD*DL`xwe&lDvn zaOarm@}cJ0JgZF#*nE^eJ<9p5kVve_V*jg#Ocd!XB4AS(UZJ8ivB6ig;6#K$iO0d1 z5YHOq&q1BV#VfiX9@rg{d28=UcKJK)uyryLlFp=VvBl46jQE`<9865}b$7UX`0udM zb02d=2~QlPOS|=oRt)qvfIBM;3LgiWUQvq$AQE>yxTs7x+z5bjv7ipXR-Csy zUXTEYh#CNil9ewqFX7_Q8TUSLb`H|KnR_h5F=)AY))2oC@83Bz*O&FT*%#qK$DSz_ zt)8+WT4HHePkZoucO#GvFRWLzbUqw}s(_9@? zAScAGrHfSpGmN2&*g#+gBH+~5N$0n0RKHaPz5}ISSm!LWq$ERooQq+EVlSFxBtnelh^+5izj{j=5+Qc zTkqsylp_j22mBrt%SwaVS7lw zZOh6%YZ}~N)89eIN*5PzMv+y1#ToKsCZ!vOgz^7BlFmJz>GuEQo2Z#X8*|KQbDBeB zq^xLWj5)M9=0+q6QD{`8vF0?xoT=PwbC_e1kkUb9sO~vdqPwITI^R;a4&UG9{yqA~ zeczAAoqcwF-q-u|dcG7CvtAlr$@swSA0U0KE*dqm4g2GeIp_RY7BRx_%FYXSX!Ze; zJrfNn#Q^UFZc;+E0$h_>Lz94uv=FpyKwn83p{c!%P=%CinxHzPKy*@*Fe7HW1>oYs znPNAK*T64|)O5(O&9Bwmb$~^TMA2bJo;*MmT|zP*ci#Fwt;MMd#hu6*dZ>n|(h;sf zA%&!suQ_xgXkq~TqBo3Jx2{j8^RKNQ%RQAec?{VCgmSK@SrtLgvf=3?-d#ixW#05U9=?1UNbZxPgPkSz6=;^2V_ zE}6Ch;za_pBEcQ1!Qg(Fg;b$@uq=8ph#wfxZg6yHO^+i2T-)TWbWFM!obWO;Th67Y zLAzf+RY{J0U#Q(PFYl7J?bUFke5DDUEZjoOak{zRoAv?RxNNxV+Ak??QinRiV?$%J zv40{05)-h$0niU*r*WlfWf1K>E;_;5_u1ZzWSlaQl^sjS+IytYM_cIZL#WzD@YAh!vv*v$qs}-1gYR*nlJuX4pt5~kIst!jgxgVc^Sa1TPUP|> zzdkh+NHsRb{PdNRK=vk8oHUPvu}1A};&fd_nIW9c%bbQyIkVx%$9C)vR1#%yrPW-n zq(>@@re~+^J75M_kDCA|u41bv6vI&1T)P`|K*_XcliFKa`3Tt(J$4*yx=SY=Btf7J zLDI|iyJlOrqQt~Qz`H=uQ#KMW?*aWci47DSQACx8iK<4$k4dQXCccJ0VuwICuVAUq zgbOo~hSP2AN3nudwy~h)OTDbSg>#ZeWrIg$ds6|YZ)rLyBu%k;lihfAMVBYkSgQ78 z#k?offI&$@Nvc7w0^r9LIZT~$Cvd2{v#AB?F|#;XuoVi+)Q00XzFkq27Y8Ip1pQjs zE;z_v5inK~R2!4yF^b2r@w)`3A7eDmHeJ#s1Jw3_;Q7_!p-khPk|EsjBDqdHBCGg= zZx*MZ*go=U2#3-9ADX;p&dxv_OLRsRTfu7 zz^_Go;fMXH?mF7BX}8ziP~AQH(5}Tda;#K-_GuFdRb1QInRe~q64u`@@kzu#KDoJk zQf;FeH(e9*Hac~Cr9OtzLRtmU9ZdK&z=y0N(NDO-q@R}q_Ngpm<8K}3f{w8CNGVOW zNNfZ;SZVNrVwxr?Mic`YG*D?I{101j`O#qdb@T`T8Z+C}?(#CSXum+1nOC0hwpw(~ z?cSncK~rU?X-I~+7LSuH^7j5vh4P)cq15^M+mF1I=RF(hsEre}-j}AWfi`fL(2WU5 zNBmL7Dw$8;)Orp#_UPc&SEIIfh{=!{u zMjknyEk+=Y0X3bbEfY+9&^*O-5}%Pyz{)&z1J_oJ6coZPuZ>FtVDCEXJ&97AOgQXHH>K+f*VvcR4gi6q3!}NvD&JeJzneI^q zw2ok8;UZQDgs_3GYksG;NUQ)Mfhz|=rZT~x2?<9d_#07Itci=0IwF=Pa!Ke(Vih1p zMA6xd98&Z}5m_rDlCls{kS~em1JU)j903! zdsO^!JO?%i*3E>h!@xvDJ8rzI@J#9KZdj71U^zAoljW+?Y^ti&YUOI>OGE0?%Vba3 zDP_IF`R)%r``DNpbV)yz7z!3vB;wOIB{Egp59B1K7&$(fqKOz zDnC$~3V8I9^U^jEvryh@I*78f8xyti{MvV$YeC-tjJrg7RbaO1QAh$KOO_2AJo^|V zXm-LK5jU_6o>M3gKq2CGGFN?J%p=#~d@KCr7sAA-Odj`D-YD`xHbc2Gy67EXSh6Ul zWS$r5L%O#J_T0A-(~a*oQrbv!!Kt0wIJmb81%Qca4#;#eMHaac5+R@OqpyvDdP%{) zl_}>utAm=JqBx3u6V{%$Ww06o_Xxz!N@KcBTp!8KF(a4OGp@s0CYy-_lLKnKaC_z2 z5XJM|91gIovSkiPThc6R#Tb4?p*IxGz-i+22~{|Kr}-lBsZbsjCQXMTa>h%k??hQn zWY8%e;etL%Cbog5IT!`LU|FuD^NTY=bn^))LiCr0uMGGV?M|Fp|3hVCV&GBZ6!k0h5uc!d@!K{^CrFxb_>A> zw9~~h*K9Lzn$?&p83WNbaGmUogjBhKC<{h@zM#+3kqDhVn6T0gJ*;82?3z zp@u>VMoKIcz~b4f_(!K>l_D0`#hNRrUvj?s!#|;5C>|3 zjRY9NkwA(Nv4z05DbE0d6+LA=WlXw9L9ru(&qRYMH>AoMn-7W~uj?L{V^meny4c}pQD%D6cH{Xt2*>VSkWQJKY!uF{NY-p-!2S%~u z^i{g)iw{z_u$1_P7^_9kgd+3$?f2P5ShFKz@cu!5AjK=GFUT2Tkb~>AFmz&nuFPq2 zZ?eb;qLgF?6=`%_+Qn|MjHrUoQSQjL#J}r!Cxa_UN24(5G$3$@z_6&YfgT4$D!3N;=2qKInG692YFt4ma zRUGn%8Z(>^1fa7&%BMPi=08nXlie)?z@c@ygl0y9;&s>mLHLwr)>Jbz77X0xm1(d$ z4S0e^6@fL|hs2Qpc2x2^+l#fa_7-qmul|s!fk3~`ARH7Z=iEQ7MZ1U*GPO8rWeZH?%o^@F)$y*AU@aflhWv^f z#pGrvuo{-}aVx&`fYn!3eo%xd=2V7+&dO>YGQ)BnFI4hW)CDRCR!AHzkEhZRN0f`1 zWg*$KeAPh!%!>twBvKD(LeMyq5->Z5u@D~FwbFpO;0gGLnxJ~YXj?}yMABg5Py{mq zmy9MP+Y?3t2d@f})mn$ldYxnhd)I0EYS8kEc}z>cj!ouqeY5CDtcf#%<%{V(9oHu9 zJAh8yeZ=U9-L=oxvx+Hm{6N>QhRO~5J{-~N`E~wznGJ1H{Z}U_;Dp<81@(u^eE|PF zmprcX?xMfro_uYGGDfAuFVV__f+&7!_)h7zHXK5LRO3s#`D>R_oEfG6Dm=3$vi$fi=bUZz5v8U`Z~ILnL3ua5HXF-pKl2Ui1`omo<`_qWIl}a% zG2LysMRLc4|7`+eIQ~;ovpHZVmf zHAYvFv&#OD#dvM2A5U!XDu6RTWv|e91<{IzR7Ndrki_&2)+vX`I{v23`G+Q3jBk*q zc*66w{A@qma_5`vN~wz;_9WO$aGAP8S+>uxs{C2q7-F5-!HG<-e9BJCIQTrV=ir2j zi6ab+3YR!iV9qXB@I1fD(sbZ3XIVbr%o~xC z2pF)60CF-e0h7Kgl7JJ_hhb{2IV)7$352RbuU9+{gp#2(6}o>ww=pg?7{V6=I&7>K zv+P1Sh~XR}2Dm4b?a?JWI|!)tMmnE^MvIcBA^sDY5xhX+b(MD_^Rt0&UfVp8ClBu7 zar!gCrm~QD#3me^om zT+MrM5b>rRzL)VnL-8DC?@#L67V29Q?8)7)75$lli@w}jGO|+Xq!^n`V8%_A?Lv=% z5R1>LwE<;<6dJ+=A6p8#(ZE&4Ajknem9S8?%<)6UVz(3CIhCT%?^6AK+a<-kOU2T1 zE-0DqlXm9w_o@!J-&=Exq)CZ!TUX zUB{Llut*uQ5H>UD1MPpuQBJh)I2q>R6fG7CMKaMR>*$a;{c&GMoK6Ni3mck`=&xj< zQTNDz5zGg&x{;!56oAg|zUX5_h&Dg~VQ6hdjZdyx`B_Mfj@Hv%<(nu0^9ODAGj^fQ zoC+O7*)dUPsw`idoBg{Db@GVUUP1gCwJG%w@MpkHWUu~dpf@w{8Hoe?OJ+|t{u~@#3+UR| z;Ehf_gpit7xoP5gPgqJNEJ@}1KJA)MewAMyQ*`~IU8ith=}2JmVdZKY>o*0#b+_?{ z6ulPhf|&Q;G^`c{KT3-_O5&Zy{;@K5a5=yBa=d=Hbr8S|uR;+Q87|}Ox> zk+_5ZcPH=`8^_kBlp?hwe8*|pcIwK3Q}Va@C7$Lh(L;s-b#Y$N#dI$RvB=Ovok3)sp)JC-fVK7)+FwOvJjL!8 z8Ljf~C)!`Fmme@S63o6x6jL-yWI9uQ{q&5>Sc{VHfWKDoArB8995mtc{~%{yP1fjsAHHRL><~1h7{>4Y z`^ll+r@ojYY{9n&4Tfr(3k5y;O@2)#rzU`3Oe+W3EOTyqq?S4=UaCST;Kc*|JiSZ< zeJLbencJgseJ?e#m$jWs?RHKr#)Wc4!El*z@`NM`N%O|bpd_tCv79#TK=J(r2Idkw zxoPOZ{iz7QnQ|G_N}`NZwL`_RP^gx4rP^DSio8&;Tgm}S#VYIQL$jt?O0c5 zNuMC@DeVwT_#qS{yhFBQtr$gfBz2})F~ZC{vR$9(1ow1pzv+3v5l3$7UCjlIVm;M) zs0VWn)QWC1FMzB~)_B4>E8D3h4ZWiIsha`Mc$iBYIr;JO88TW9;8KgOSSC>Js9Np{ znVeJo#1R+I0i1E-MiU}VH#Y1}*kg?^C!cyDcjbg)qAn2!GwPtW^E_7?68wW2&Yejay4Hj~qJ`cPRL8qb*>; zKhC3a>G}z9tPz<*sY9uv;QnbdsCu|z;PAkpsZGXoP>}FJCUvg0^$wWN<;#*JJBM_` z6pxKmQ_fgdDq%gCD+wl*iwr_euD-!aA}7K(x`>#IrmQ_pJp{9@(+9Qz?6Dkx(?Ywtu=i`5ERJJY2%?TUZT<)T_7k@L~A05=r{@IjSeZl#(YTTw` zSR%sI?lU$rOTz;2AGDoU{@(f#kZD`y4=z0K5$p&^vvchG(OYK;c8XM3TmrM)RSO|isN9Pf^)+S z+=u7Xjisup4eSnRb0&FZ5IP&Nck;Uj1G+JkuIk)hlRYNz3GC0gBx zyar7a@ujkmO^>gJueHPSnAZJTc~U>$khyZYw${}eR?oJ* zLKlffpS*B;j6T_JTAr`!;PTBz`!4g_UE&J>od5^~Jj1CWshS>@zArm};P4Ndf6knH zx$udh=~W`r0nn7O#A~2~V-Qd@1|%@T|8fKgBoE)rNlJP7 z347)mnAqkq?71a}jtP?bv%d+=Ii^nIXMD7&CX*H44@32L(0?;;EW0ZI zIPQGbGqj3JK{Mh!>eawDWvflS;l}%^_Vw2j z<6AiM!&8Sn1-V=UJD6;BMy&TVSN58)69+3Di(yD)EgL4N8DJZq;32ugSMyA18#)1W zRP$*0%;81$`w9~k2V$bPs8?{ybw>40XNn6}<-t2d4!>0ry#2p^Y3Q`<@y$&# z6}ne7_brvhw%7b2=)@X@5H+(y6BB_ah4aW*!Bh9O;sqfxI9x@g2);P;h%{#PYA-`h zas1+rg4ikRI<+^)Y1N6Lg&w7!m*i$!Esq$cdgO4O<^ng*zVh16+{*~Me(B!sPyeWQ zXjRe{!?Mo8vw|u4wY(iY&3U5qfFh^=K~xX7YeZeldL}YTdkm;y$ES;K8KsTh|Md^? zVYBo2B=QgZ1I$i#t1@OAvG%r`Y+Owm1H;>atnf^5k&xF)5z{3b(o`s*-Sij(oonUE z=#iGr>!#~toObKIWdw!1@IUte>eD+Q;5#^1(PQ`?Wn#LMn|W;Z;Ka^SAaW}X?`v;q zR#NCy>C!)Na3U=C@CQWyQHidY zD=DSr&RL|nLz-dXa*OjH8R~+Ho&Or1qm;e4oPw5H2)M1@ZfTr0;Bk}9_+Da>z=)pp z8!$w$7yep*Yk9+Hy9uV__SsR^TGO%Y=g+^0msEo9lpF(y!ZaXei$_kP-dDCY&~=?B zmp2e$xZ&-4uPdXCytkjwO~@UQy7#Vgt*9x^Bgd;M&IQXA=v5~%hyufRCf*Y7{<3$M zBP=7(R2RAM9YhNc_w=;vh}7KpLHnM3to|3 z6&~*-3r)TP3Pi$>W_!n^Y*&4w-hl#~6#a;;#KSS!i^VkooGH~8tdhpbzGPp*6To}M zYM;>7!yjS%Dtk5bZb&Hm&&h4ez3?7^jXMve*WG}{&-s)_G>&`!vrBDP$ot2`P7xj@ zwZKWhqbfe*vYDvD7V_|OgB)Mpi-JrFv*v4)7DNoEpK%fjxa!|7#&IC{Be(fyrM2r;!utR@h_=N zLxA;})S4L7Dz)3d%6Y^rq7U?Mt~=j-POXwVW0n}-huKo=I8{5Qs`rFaT66*DQwa}% z^iTL6GfbsC%9a$?ZM_V$e+F8@J=;Yiviy*s=eIt|6~4c;%jdjy?{)7Vjg~j!UEHQ* zHvAIoDxM(ZmYv4mw~(J5^>qTQJkX2&wddSA{OTGd^qigYWdmh&@OWEl z0?c^4D*_N3y74OII|bSrIIAH4VNj4&bmtQ;59MxQzzyAD`mEcq%Ydy^0(Jedx4Kod2h-;IL-goV0((B(ttu-KOb;S1m#UYJ9>4^5T(JU(p3cp9XU z8bgpd`e7B%0NO2gc{>%i)Wjgy5Fa3Y^;vE{j~#lf>my%zjh@wr{q@-G^>qLP22?+A z69A`?VnhF?t^xlb^^1_wj$^Z(Z(t1ojtUFdpUCNxH3p30=PYg|(eZ?fJLaDHtFN5j z{~o26>R;K^619>t*JGg+%J^6n42p{T_Mi7ZO>jm9=rA6~TQ^w;>L{^JbUfe+~(-L=X_AwEhR(5~^So@~$Q{HBjO19NsS zHVmx6)hC$ExkNOI1uKcwn;J=X<785g9SVX2aDZ3!#s`_}{WmVV4{jawxVMM(LYRGU zc4XVp06*82+{2d7%@wqkgMToR2gAV27-l)_!iqMV<+|WVZg$iwgO9lkqfQgf9Ugbs z@`yqNS;}C1aKJSIlJ{NY-{{H~A_RGMRg@ip8djCy?XUG|8};uTydB!uh9&0skeRjwhy4gT>63K`;T6IDaBS<5Zz@Nt4}q1n)D83 zHM(=e)wWJD3l-RMX06e+hTGuESkK*9Sg9GvP%Yz}7@9uhyenkYTo_!m(M9a8MM(Mw zRKDQK_xh^hYR&rOJ5`c$_kSM0AIcFOz8obT4V%i3n(42L_7P^1@OLUyNX;E)#afZE zyGI8KZ`OHv2X1=KQya;RE|3*-`waUth>h8_yBEj1D=C zt2O=${R6r4vTFpcpk%i&F%f>tAFh?@P3yGw;5d2@wNuW9IPcGv*`IgG_sTo=6aR@y zV8z*)6Uq2{bHG?!&Dy($BMHh%+R6ak4@|$nfgG|LpLrWHiJ#^8Pw?T$ppaNOfTp2@ z_X(zVcwY+d#HF}Cikb-MB0lj!3-o~6wom3d-qaQJus3WyW!_(o6;1^Y6>THhD=Tq= zh&Om@M1E!zLB>m=yt!BMVA3y@UsEj1GSfxYVc>1Q6`J?PUIQ#$C|uH@_373O!k1DoMG zIQlK!ywSwI^HA9t)AvJOovc7_hoz@7)=KG-+uovZmDPEA&Y4N+39O2jX_G}~&z_;| zjdd+Qs1S3ScTNxc(z-OEPVK55%as^=TTdobFUEIY&)AmXij=xL0cM_DD{QOf)9?$} zW_PbBenI7EltB0{t>Sw;G?>v{b`6JywH5SVyVBMipS1U=_W<^#XJGb@`j9IJzZN;+ z9OW0S!?XM%<&I(FfF;;R8t{Numxm)_%$^vYvtxvEd^Gfel7rKD}ZFm3~G+q<<>z+}r$+%t{CtS#hr8PZ%CEyf5oHnD+nTOZ=G&+eJhq*{g0ociqj3vN#xuPp}WW{nHIpMKK{e2Vl zs2AIYjuUv(ur`n(?UZF&$%T1?p6(UjD=!T#ESgN>%Tz3+4BgYQpOP~zOuq{X&ob0* z#0I<`k-u}>6yDm*ck#s0)*=KX`0jKbE8wNu1^^R~R6r2`Xlv8{sCRzxS?=H0qx`>o z_02lxLM&8HTmDl~XnG~x30XehI3M{ziBFL^PITw}hZ8gL*Nq}VU9MD0f8c1Rx<4hB z0gwU!8~|c%9}XRq7c#~$z8}>e`mz)Lm&ya-Sm8+1@lv%nIZcri%&y+wgv@gh4VlS* zG{3~PZrV)e8mh=QIm9gaHu z8)4pjy`Jl^>>!ys<(F$~ohR4~*4e}({f5>J#1Uy6u*vF1i(Ii^V)m9rk{$S&z(^9QC-8E^ljK}J<*ba7&*uxHHF;(GGh(m7@OG6xSvXe$0mD@97<-)-1 z9ajXElX@<3qx6pK&mX?9*TVENXPBw}HS>LX{_*Qtj7qr=ieNgJe_Q}TY8!DDQFWA3 zRBgRXM*=k!$8Tj2<+=X6&<2D+boRsT10zujP}~3Sfk9%C7~GUp$g)xhfgWE!r|co zU=n#K6+1;`F~d0#R0DnO(crXX{kt^7qj|4Jf)MN*BFBzd{5Ug8;6(L03SSh#biV!VvbaelU$BlTf(jCC$8Z{yW# zIYcyJhhJ*+fFooWsfV=UGYzma)C?*+9?2#3p=TZ+oL!rl((7gcZ=Vi8#4f0oR?N_r zGNoXpp9=)FG?U3jco+udQqUmTy`y-JIJe>ABfEWh)U0EF=1_Y^S($VCh`Q7Hd&Awglr`myRaMA4k}7@Rj;{JV0z;TTaEnr;^&#wGLgkTq6M=yj zM=i29^TWe!1~iL$n2LE!U4eqLjxB6sk%gDLTh?c%=^|26;|lOV>qCT&)})G=2*4gx zNh7K0eFbX~A$nQ90~(R?gVq?|#Bk6G+yrRoB^aQASdFjoCt#m}>p$Qw zBjDk4)LG^o!J?y6OSGQhmOa}gz#V3EENrhj%jm$4}nS3&)zgwjA1h3g`B4AVGGF=%?_LOGk}?XwVV8f+gE9HgHSMpk{1 ze@Z&-PX3_f2wMY*Jpg}x1Nbi#Gn}|Rs1H@+3>L*^@G`&#h85s`Mu?8hg-kKB10#>R zh5wo!Zr$+)+Lt<&HTfH%`E7NpYo{aG=#cz~@GZ*P(aAzaj!(VV-gm1t-Gi-}-WN5- z59`wl45*5W(R%KII(yLo`>$v7ZIcY@7mvi2w^nsLk(N`ZC)= zr7Mhs66SgjW_S-U0UOMQxl{Rp(G|=Q&SL*G02a&KG^PXF>57jj(0B$QA%il>&>>yG z+usB>5s)N{oKBOHQt+D|d@1)_?r*l5RR2)_akc=MFE2`pTOY~8jtJAg%8mC+{wOTMOZxYSG6G#^3Tb8C4$-Eg%A) ztQpHh*8?l3y_6eaZOwj76ZQETmUoyx)lFX9X##xwJe1YIPF7gA%9K6^S>@smmVFyZ ze4;dlDB;l9JNwq2Qm$~2ZLt8gOOwWc+8i8n#^pTJ?<|ko?wblfE!!V6Ps}Obd+T*B zr1ORCug&XmD<^_dAmjZ`KesPho^vFd9+Dr(3Ue~WMv!baR75=N@>~yJC_{DdGj8SqC_W%;sIElZ3GQ%Fo@^#bw>Gz6fxMoKU~>$zSR34wC2 zRkWUAvJInXuiwE=`Z!gZ{Y$rV<%wpfi!oGix!}^Egj#-vS5K6vU0AHT2HZ$K=S8@e8(~Kj2&xK_G!M7&XH|vaLuMo8mY*ZKeMl#H)|5+EyH4fXVR` z0FG^7h1Qw$b?GEsSK}!UmL=E8;*k8r?H%X}6i#nr3%Z!H_y11&Wz&J{t@A3Eoh3{f(8JiAp{Yj4xMfsPrEXD$V-bwSM<- zL~_k1e#)O6Lp>3lW@~L3k)%tJg`Xio6bKM@0_cM<4Z5nB&+}ZucV`0>hCnzyDN*js z?}SA>z`h}+E?%I?R4 zV6j|ASD*I9lwD{?;w~-~7x+f>^x<4w-Bx8ZFf=rM!+jspEE8;OpivU;C5ucz2?UNnX3>B*H$L-nxqH6ZI9_yk^`azGQ7p$O+EaJ< zu}+gp89=6PT7eNt&4_wYaBbh+Kg|8ZQ;(=CUAUds*(31WdI1Ak|0+d z8UwyUS6QO}?sLS0Vhv&BAg@R&v&igZV^9g-g(VGw8xa1#1J2T*e+n93l6Fy$o}fC` z*Dh~C@MsowX2JIia8vvTRi;DXXL&^QKX&l~oML6ZPouLcwsTf>if9!?SkUHYq`q3TLdPrDu12N0=JszmHTh)BK|9`1cay!2f@a7|Lax zWbDh%6w}a=PEE`Ta{7i9kj$N&hN7<&aC0G$B) z=5_oW_;s^Lk6*i;3v|L^Bk%CeQl7)@1E?U6cuaR{%k7FF9tEdydKIkEm~<(l$)*Xk z#W?uhHul~Qr&6|7ek2*+nfbXGM~}z@OL-o-K_%^(ad29Y>Ogm?BdoXwMKNS3DQAx3 zA|CWLdS&xlY`2-ShLBRb)Wj)4%lBLXZAsXvl(b{`aTiY-vok3K*KrQbj;YN`9z2?~ zd<&mQ9lFnRuV}AlSSB?GP~5k0*T&M%ocLR~?zoRvq-k3yoT}Xk7zJ3PRbA@31z@#^ zV?gQc^Gjh6O=teh<&{zyW3(~$&m45T*~td{Ef(vQ{}e{qwy?z**a$ax^&m38!0pPV ztsAluHO65_9px)a&6PYVjbKA@&NB-e;oBJ2DX9c|3*Bw?h%{$Z7%}IWKPg$44|E8y zQVHEkzSeiY`#pF6Aph|eA?cxf;Px^{dEwn{as9xU#z~PO6qNZ<&Q_V80j}X7$89)5 zU94M+u_we30dgZ}9jZ$pdsiv!uifec5=kJdb!=JQ3eXk%m!1>VMCc*|T*Ygdf01VM z;Kf?kC4(37zpZ)TR6o}fH7m>As5U&%Rfhi#or|T8<`N}kV$fWOycP@I3EP6|eT@3* z1ApwID_fjvG4hlYRIl8qTWC?lxm@HH(^vY`|H1mfE~g-fKVn^q1tAG8_sQ6%#p3*| zgMCjQdUZtDE(@Zrq60%6Z0h4tyHq4 zr&h4!khQAo>{e=)@Lp5pec6=;lc#d9jd-%L1z~_beNaLqtk+bCGoeIA&QuhBSqdAK zrXay{FD~2ib~RlCaFwl8fFM!W%Im^+H!fF)iqlX=ep{~<0nFT>%6nD}Y^tBnfzJj+ zUgvgQKIi@gt|9j~EJ&$`Sv_e)29pdIuuShMF?AM{Xg4tYZWU{hm;u8ZHWKKB zT0p1H_O?0EzSDuNYK<|F^32!Ni=ZEG!qL0hM)jc9t5$&7WV{S6c?@h%K{V^>F!GtpOF1C)R5Kl}j08uh68q)jfr&ztmF)zW4Kw2_DVAS`rO+kM}E71ZM-U zj^`Dr*-(AP8SD`4w-6XXQd9n_O&P9j5KDZb0BZ}-pj=WA!g((n3xkyL`rJ6W`pAI)GBwnok~wtrKCt!r0g} z0zRdH2Y4C|S)PJ4RkG8vCgS4b!g}NP2~FOL3Q8{5r5#t9nVE8Iu;Z0_R}KnDxXO~W zK)@jb7?$)ax35+IE-@lCuFJ3JzsuBbO-cX5%3K(D^$a6RRpt1Jt7kVdkTf!*F2*4p z#23f?;7jGLbC~!VS5Kkm3Vv7p@1n1z510;uhTRjFp9zds)%$j`qG4hrFV#?!{e*aU z3B0ZTx*7wH7@$6BQ&P)nZY+162tm%9U*G#mHP1{=P7aRG{VQR$XNDC# zdT=_rBqani7Zd5qQ4Z-l!Qbs8NpqUAaj2!g=aF4jtGA3Ze2l~Zz^coq9$5F*+2ex& z%lSamwZFb72h_R~{iPMuTp(tgRslN(4+=&nLGH9lyni|2!H*na*?T^Ov>qy!1(-^4 zWl+Yt(*zP|m?heqv&A+J#Xv?r+XqwdDr{P9MB5QK7vkc8 z@+oWUfH~o7=(BWtts%}xI(SRhn?BGw7EIf2gPjL`=GQ-flntthblP^F$?6u`GQ(ADH2&3ORZ|3u_9dSSL;Kw>hd2%`^8*+vLbs#j#I(xi66PrAaBHQ|nWO2T&1kJP--n;HB7U zo3jJ(jMF9Vjasd!1S@NBjl_iI1+UczpmU^WL%P5>Afb_6}u=rhDg)O<0B z#QbfvZiIh`nM!142(>a5M_b<_+4UV9H9j1Ft35ZwW{&urdQFZvw^R0)v_yhlqW=aT z-Nq2;m?;d`b+UxY7Sb62%#MrvSZWFT1Fj^4PYZD<!Y|zda zp#M%UJiuDE58or($zeW6UZZ&RM2q!a#}Q3;PlPTr{{%8pmnKhj7%vL;pyoGc1n}`o z8rLmBw%CI*B7Rf-M8)eiBxzRbPAS$a=mv<_w@xMzO)oQ^36}I;4Cd=PVh~-tGsLeS zrC4{z`z@+K%xM$ zxI;Y0t+i>!G~QIyP5{08A4DhL^jD`?zjgd;rV7e_)3Wye<7c58_*p4s>)}p)-bS;4 zp>E=bnq+64bKW91a-!a^X!vc@>FIJ8(WnwPVoYD_puFoOU$$r>8psvZ^=eu=q0)HX ztUYo$2YX7-XuKK+ykfGwH#KPDBJf5si1f&HX@nzCTcNZh)OS!1iO%hoEhK(Tt$CFz z>(Tk$^105)35%N6$j3Zu?foPOguxr%0Z;Q1^R+1E8p!d-bf^#ec(Eb`s`x@rDpLK* zzHa;UA4do(YqJ+y35odk%bT(y9a^H(njHs;+yuQwNOJ^UPb=HV{mMIIrNtfDehy6~ zI78%&%tHA6q)x;N>}!SN4i>>f>Sze;J@SdnloZnQ8^L%suS!KjaSOPR^D_pW$sWsAAr*&oUKcsvfo-v79VWv{lJ(m<0bbe@+%hzffL z`7a?Mh#?ik#b#Kh#l2>9oVR?{q*7JUGTvmfvs4C9j((N4gOoZuMp7mnq9O_9f^isMKP>`HFDd6fNH3F-saX^|Zk@8{I7yKf0fB z=X3126vp00+3I@~s!zt2g2hJC&0QnM##$c!H29=sg;u%fGDQoa!OHZ~Bgg49$5Wv~ zn2b6d=?np+LyP%64ZsTI@i8Okq~v%!dKx)n4+K1{v1R}mPHD2BbST!xv~#?+#zd+?`~#jOo74m4vTEBhe&1bual=_T?+@faOS$wzQj7 zURzCcVc@A>FiDswTTdCGB}z&^&8%7=hk#lMyUL|+c{XLTne$3KBg$_^?_vD;1z~A5 z0WjOehTNkNsZE=gMMGS9e$Mg(<}U++x3nJ8qA+Bi_np#t2$?^d9x=cF++wZvF8`HY z&^&wWaRXCz%Cm3Ri@(&rSBxY)C@lJU)XO8WBW|ecUi8cBfeU{PUSB@*$KJ<-v7bE# zkdVCQogS=*JjDjYhw3h%D}^(lB>YD0odZ-{7*=qc2g?VCUe)%&S;1C`_R@i>r@s<* z7%IMmj#uvr#Ngv3TfEf^#+P$kOxSr>G5Ahxpcb>Z!AIty)dT6{P{J}|RL(Fvgr zV@4{;7J6$ymJin#cB934P0mO$qz(yY`QNg==M5kRIPFCp#?B!DtZC53|IUQGE6!W7 zTR1F8o3PM*f1)dE)0I3~#S-l>{Hbjr#BKh6_d2`SA675BmH{j1T&^s-`kw1Co=y+{ z(!MEHe|)s0Cjzg-c67yNXFc|gw3_@H0k#6kA0W#6dfhyd^joAFXosV)k-yHE*t$aGsA`pk&ENvo<-wMR2=@P{U%!oVum&v3g5ET$h6&j^bT}FzL zMAVeECnaOXsczwO&XW3$%6KPOHqj!7P7V>Aw0gJa&m8gxgp`$nlKR!00+vdTfX4jz`_6H z=-T6%-v9qDE+d!Cb<&u*gpjFRCJb{|%v}iEB!wutC^nZETQ;{)D!G%~Lvkd!EIEac zYlP&w4uxvb@ALir=`WAR-k{1>NeOyKmnc-QGG1Rj|KbSXY~K>}TyG zY|Gb2(tosurD&R*^(;&(H7M|OfzmV|1VI$B0A8dK}1Ap~`@{AUXjM?oEw|Wr*nCex`?5dDcyX2>@S?tgE=Z z@`zX_13HAoDsu%l@?nHbNQQ9027Lv-nZKfn7|Mz|C!noNl`)}<;FkqGv`~3jO%*$A z%Dd*)Y;Sl!RvShO76j=EZYYyKqvHX(wL=8}@T4V#w8_r3=^Pfa; z=6?PK9o0F&uKGD}Lssm1=&wZIcEt?C{DMbNMfNSlh)k1T!aM)MOUi@+t+~<=!4M3q zNXAEjvy^4f>OH-ms*7+N_-H6FcB_KEv>8yXFTcq$0t!X0&f}pdWDeXGp=2$R2ivelUBxlMXo&tc8rG#%D&v=gCxF$v?T`<04RXOI zMz*>XLA&%8e16ypXtUsqwTXyZn=J&iN`lJ=c}1Eg7n2SmK;CZ))`*YLWz zwi#_URk=@d!Lrnkt9d!o>8aC?rJo_&fS55xk=!w$Mk&d<&7VFLoE2axdu-v>hyG1P zmH(2xZbSsV4!Rfq)9miuQ;O=>Xa6|7Rgt-6;6Yq>>$47nq%mV)6)6@b-p52M(yI%R z5-%#m3Job1;WR*=79CEDf^wmSC}WA{wsSU(GFTS|4H29MM#)!j-E18f#TtbYLA`#1 z@{TrMeHAR*3@LBfbC&Jo5oKA+qRFkgsNCx*bjE37AbL^I(ICtN@TC=RAgU7#h0&RB zf6#~r&f^6cXl@K(;TqsmLSowGMCy=tY@^xBPgBPBR0j_|6F~{2L;SA)Wr&Qu~CAS3io3`&D#`CUnK*`s!XxaV#XL)%&*XJRwVudHVFd<<$m*i-J~4gm zMO!Fy5n2>TvvcXyPG0@Pc!xr)U-oPO!(ltsDs?@d9f3$w%FVk6mZ0L`E(y9o1LJSLwp?t4PjHvEz#YDd59A`wB%^Qt% z{!Nfrnu%+;J|vfSAHYJ*Gu9m>M{Q#p(%6n)81OHQbw^H;1$LPkla~|#1nA-O`sS(Z z8{g8~UnYJ2*2(#~Ph~A-EnM~cZe^+X4p9AETR8L8*fgxj{pT+RTB;45_9!5+BE2q+ zwt~z7`g5_^eO#zD|2z)Xign?qQ7}a^pI*cOWI+RGDN>Sy2Yb={?ni4KM?cUq;00Zm z2TXWYFe9v0*c!Y)l4LH7y$r>CMn0_yL<|aO^=AxPy-Led(jR@6fim-0o1hNQI0Cwb zkr{bzw(qfIRhwO#q9!8*C3mv`P_~cet*^n_vdt!1xb*0eN6O9#zgFOVb zAnaAk3Yy#gayK@Gq|hjUMPU>iS0jT$VQT7P@toS7ut}1TfR3_QveFlY21eh-$1;)( z`_AD@9ZEU0OLP)7Y{)oB+IdZ}bpUQV1(FVU;B%htDj2d_MqhTtu_;tRA3+P;sA#_l zQN$)f2vr0NNhaXpN>BO7oZ5wVy&g;<$nJUf?Gd!;eE9zn+1YY?m#I0Jni{Hlw2FO&aB$oWig$8baRrC_sd5>oF2=)*5XxYDAJ{MG7Ojo`W^c zYm`N-zOp{D9iAmd9>hoGMI*P1R5WZJlY9zAY&H%TlBviP0WF_(1}EbgsH<+t{40D3 z8!^!QBikGHp!-CGNDNGnfD6W;PS2BN^jN=3rF~<-`_HYC(DS-HPGCTE=ouepzI(61 zGK@OKo$)4_kJehvdkJXTl7Zd7kH6NJ!|ijgU)$3Yw2%(wqLvopcixtGr(A)oT5hb1 zZ{%9UP!r_f!r_^~@@N;VyGp_V-${(UiMcrHA2kMA!RnOvDBQsDiMXA_*LJ(Z#m6K^ zxM}0fZIeu?qSqIgQWvK$M|?e%`82neUG?xvzX&^jWto5b*$1>_bZ&s8dSA_$<6N>IouL+C2|<9_ z8^}UA#F)84g_YhUp>*&~^d$71WzFpKTnc1z;Rgb?lZQ8?M_5$w+wyx49?Qgq1wA7; z6oE%TDuOPoDPPl-9yZIqh_77My*s=L^tAG~6)ll3u+>7V6ix`>2;Tkl!VkIEWfCkS zQiN-njOoQ%Xd6Y#ygHNXPzYH2DIuDd_1HITW8WDV%y?Z6AnAxPy#U({=KhDH%>pJc z5Uszp0llu|<>gRysi{aQTmC4FR@VX?t2W?cTgfv1a3pU0b89y1juH>M!DI zdWI5#AszwHUY{gBluvq*^LD@c=jlsnzn-!~t_Njz&8jwu`9J)6gN31{^qt^-&nKVw z3z|S~{5mP{V(zEOU(n>_&h?N%<>?|=yTb26w?NZ&_kEFlv|CQVQ zvnF6MA&C0fkobDxWFlPQY+FQRL!9wn(68yWzaY;3+rGb`4JF0hTaq5Y-z_KB{~Ja& z9$qC1Uoe3SE9DR)-TXY^#ZpRRC+c=yhfOQ_)Lwd=2v*2sJb0iaXgWJnXWD6=VlG|J z01y$@G=}M_r_4REJuyZkicxTNu}l)`7|@@o+31yg#K#M{WGUsr6h5_O@)(gB8`3l>Hul<(a(+|9P7T-Dwa}6e>#;sp2~VQKF&5BZSd8 z63b#`I_2Y*HABxHVi}dYB`2#=$r50pwz$QqK;2RSmvqj0^u}{=??-GxODpPPD)sQi zUgf8ONu*xlb>Bl%ve|vM{9Y>EhL3I#=%CHQiY%p2JDFFn7^h#3yA=(C%Sc-!_W-%D z^Ai#jb0TZXF!%m#b?)s9ilA+$lH#+DLEJ!eonE$Y=oRD-wkrzBMc~!N+n`+R+B3F} z@*qLC!0WV$ehPnvruj&pt3tB?)n(=ID)hl)tcOiGf%Nubv6KZi3SG&X(IH(|l-iVg z_)U57me!wZnVWxb6`Mox(4~_dLHVD|H`}k>*vQeh*$JTTl?n1XpouekuhVD6l;9iz zb7@@&3hvCmEK(ud*3X0|s=pe!D7sOX1el5}2b7Dy2)4B;`&8EPLjTSKDp^1Pb=7Db z!LTdHtF(HBy1ET;Pn}Z*I{6kx;Q$m>EPE`WMyEML`vE>kJ(FnwEG@H5Jwq%zn@W~f zI@61<-%l#>v0`%sJ{o}@FToki815|^0ni4FmOtf=8hCed;u*cJ&L; z8TUK=2=#=gilLBYs#R>UtSbtQ{3t76oSmrt0=#Q`QX~f63!p*-UwVSv0Hr!eVhO}I z0J`F{&d>baORnzJ%UeN4(RhV!g^Pqxrb7Gm=&O=~VXu@meHKBFM+Erd=n$H$ge&`CBvHle?->b3NWLP~tLP)dg zY^+-%XV;|fTF_sR`-2j(CDOW$Gp;hl$2IqMDHunMW6bPvL_e2TDuEu@IaH;A-Ec$v zc=sNQLeFPox`Fky#NHE+{ZAzcc@9VK^_m<>R3G!7;@u8yjt&@RUQ>8-$x@-r#Cs%p zki=1J1rf*}FU?TSkhJnGKzRVp;CQGDjS$SBU51A7ZT{`0Q`IFY zMBUzu<`cVY5Jez;{!8=g64Ie}7$Qy7OVy!0c@UFy^=B8YEhYE$`8qGr62Da7rXSyZyYn}vK3IKE=H9lvtTZ*|R z<=}(h+FY-4V9=HF($Ekf616IqBVShqoK5>}(R5*L!*F6}V+9F7xP6aP)gnWLN1f02 z0*g?dh9wufO#XP$)+#9l}> zp~+I|03K}(K2LNJMW@)l$lJy%4H@{n=r%-<4KvKBqxXnuv}N)$1=_Bb+iUe7x{OXM z4*!BeLt3ueKILtSlDy3_=9&7FEg2!i)m;ycT>4+xdG@bv02@>x8Og8L|2 zLq%Lq&q#vWqJg_X;f4lR;X; zn)k1^oebKw`{aia&R;Q2atNs_R;X>lnYCGN)cpn3x1|^E521xj44yLex3drB74yTQ ze;#I*e0US#eaNQo+6doCsMPm3t1pMHQLzg)Nq-(5dHd&D#(ppm_o%w1U4M(Up>*l4 z&E8+o+JgMc=SdFTl;%PUirXO3tvaq)rlZN+qDgaEb49cNL>&`yqW(my?ostVdy_@j2?%MTft*2D>v@r$?JPIPbG-G5%xfV-v=}x}yD`MyMACS{;m# z#12MDq!L7jMCg~vW6qbA$FS5Hb?}Qz6A_>A(V(QW&r_rm5!l^>-9V!MMwnsuh((~l#1jKyVjbB70?nVTy=nvK>=v5GRIWdhw31H z%9Ql+99d{x-QLh@5$>281)7O#apt|}3dcJB1x2DjD|#hgK-W%DrQjPnX@QS*8q?0X zh_EacZ#R~81F_GcL4L7ul3#y5H0#?x*Ux#k$-B_p_}^cU;==SJv#oE54vg=T;6y`~*$jf>_>A&>d-a zYD8_>`=&9GQ|ASZ{isM?U0%6DJ)^&VSl2@3?lj3j88`)>gVrWjvrJeT=Glcq;WVS5 zCdp8guL4o1tr3=?*5~a{RK?-wvV|4Z8i`Sus5_!68Oy1W=QDw3!X
      E%!lGV%p#n@WQ&{6Uq>8xIT2|%ui7s)#k9r?8uJdoJL`J zJJtoZ51Ael9CXE%BNBveTODYDW)0S#(g5ocO(+u`MWy15Gx;2?*q`Y#_NC$xo!&>w z%%xNP($Me$dX{+)e=L^C*dTjCjiK<*EP>HGd0RIlPt|_a{nwV|the-eTX6muru4XW zaPa3z~AyXO}#?IUAtcjp_@q;Q})f!_42M(?nB4bTp8Ruq%pCj~3wQTjklz z)riLdYW>Io`!WlMDe(_Ev_8i&%*VpwBQx7#A<~iG>Eofy2WVfWDl z&|RloYwSSViICxX2Sgr{fInJFikS3OTi$#XdT6Moe$f6C(s~0Z1GmLS?Yc@b5*xvA zhGO`jF2fzqf4w@YJC8-2j4fNYb{8&A?dZYo+%j$vDIcK`ghL0Za)gB)`d?u1M<yx`N2twVs@k&xBP{065Gysx_a9b5mkzR;hHJ(cN2ky>`FL>5Q&!;cFXqb2a zQEdmsI^nwrTpFbj*SM^;_==kXCw-|H{{RawhT+U)*&bs2oJQg{J)mmlrkpdZ)KuuaNujjGxnz-CaCLFe$>@X#p0;l zsoT9dpwzML3-qdv^zsA^g;Dzx2kcdf_dq+7e`GV6i#sjeoJVm&AL5y*#AvxOaDMnJ z@u!x)Ki=hSKWs!wADZRaN$WP{dfD!rhjM)1F2580>oXY+$wS(Jx-+(Z^{8AOv6%2p z4r6Wv=)G#>J%lbBS=^M#!LPt~Nt5L`ZhuuM&-h7Si*DfG83ld+L;Y4xZ6DN}W!**ic5aC2CIp`3*8z{-;vOi8+lHb9W9r$q-SQ{F35=P)aD6NCu+khf~kw*iD>79D{JUI%?w6GMo7 zV*SX+FZ-d9XB%Jn1TE$#JCPU(aF*_(1PbN{yx$~y<=h_doG@;uR$A!MsPt3fWBks(PY7yd@8X{z5JW6LHd`kXuzjPtQBPQ1n z>Y@Z{G5L$95p9A}Zt{V-5C9aT_^Qev{ok2+WC+z%IHFJ8MlcfV2#5pLK=F0_+lJA@ zFAA*cOJ|5C8%<0Nc_v;rA@?U1lzUrZmHz{6FL=e3@nV8#_k#aJD9YiiGdCLd>un!hQ z+ss%kqt%CXQ^bR~gW#$UVxS|bF{bp!Q5|7cxuALdr`P$5oXY7e+bEdfYvZaCCqZ!d-fzjF_pdtBMQL8(PjBhC(Fk#mYv9n zTeUe5K=-5!%#A=L6PL5i@`UNPowIYgR)>mww>IWhzpctyyWbm;@I>YQAcq#K`3unR zgr>g(o^q*dk<45ED^1?GZD%=s(f;c*6Wt>d+>TQrl8$mSCH*^*f8raOYl}O{v(OsF zsZ4f&xN{kM_KvCAkoG3xZS+sX6{%mB1-CCPR6xQ`cPjjek@bg%$gf0t{C-LMr=p)A z9f5j~(g-_>oS2!+Go26Z`FZY0fBVgea1XT*Lqaq zJasyGT96wgz1hTWC0D*D+MICp23}d$?-A`@ORoCJJ}RaM^+MqS+5J==5w4Lb1y~-b zKZsQm9YFXLIAz)j4I$o$CL|rH;vaQ=spSLLe-!2*r#8cXEvdu<9g!)_L+!T`xP(C6 z5OKX1Ci|s4qz1wYXihKIyt0Wud2xr_3LH*4kgsrFX;*UK zlMr?$V8d%5M0+HdJMRTKBoDg1{@9fC?@b=!xTHy!)sMDF0}4bi+1{ViAngADr4th{ ze-6;d$D8u(mTB?z^46!{D3oQM_+OPuM-vMI+3mJsiH{>TN&bgBnS*}$Pi7p(%l%Ga zGyFiOwVmehAO1Ec#@eQPoJ;=z&RhQg_i653=Mx%uvvF?d1m`cEyZvTzb?jwp;cjWk zp>Ec8CV3p@XEAfryCIE}BQ6m!pcdjye`}LvElhoDE`NE|%;zr4+OkjKy|_j+JCvUq zrbHqNZk?S3$KC*_k(-RTArM3(B0~njODD;M=f)Y$XD59Tei5)djd+{h4)s?@i@>T9t0C7E(J}00_!ngp_HG#aSC=(^{{U!Diu`#->wigZf86|H zzZ)r&p8UKA@UrvdEPkQ69|E|)DLZmHZRR99qze2w=HdGAmxu}NXD>(Z*@^Hg@bk9+ z09xFKgRd@P%!bJ~hzfDa2wW2&nEJ5=#;S$***moQtuF-kY+i@nwgF^jp&1~ zroS(S&Dq3i5-h*G!rkA!0R5qg^Vs1!;UYVWufw}2ol=jqFT$KAHWu+*Q-NgC4bf`v z)tpay&D)Q?7((#>0IQgO=@mOt_eo>AOW+TV?GkV1z8bTUzcc>;ymG73f6JFT_+b*R z<^>^{7V{C^av-5@2ST0vWt8SJkp8bXY)$_FY#;P>ROxuggRqdim+lYNDUHa;eW*mH zxoXsg;~xTqXO|;i4bPk}#Pwm+Bs^6w0r#UJ>?`nY+b3klw1n<_d=rz}nu?$%ZpB`T zGH^Q?*?@VF{f7rd*4a-RCD<0)pcNJDC?a=E5ychR8_HNT*>EF0K3gM47 z%}P41$Xp-33Lls`Ovm-PTW{MC3Ll&0nHz&o2dQlLNRH$NYm?lDvuiW#lv7`he=K&w z7YAq{_bPK}+JEV-PCfvy?p)s?b?^fCiR_an${1f7Ju9%EfnSeif9yW>vdN}EJ6tNp zGEW%D4TW(TNOh+&?N1p5<}r}_#W{fkWS8O#ksBnbcHrJ3SK#IcTwB1Wjo3_T;v>Zm zM%k9XJW6}EX4@kE1vT3<2Xu#sUSLN50QysjH(%*q_$O)oDdRz`AJVZcxAeODCwwBk z$aOgID~0>hKk%Orf0||_ebek-XveVp$Pyx7REK-AYjH2Jy@fG}2XRaZ9jNe2_tQn& zcq*Xvf`NAGC<7pf^2lyaa@adSObL(cADuj9FWP?%oNL*^@l3uJ7O3B5ka zstJz5no~V-OiVh%Cx;HlWu<(PI3F8IgCl%oJPAOe?oD_I>Ag#j@BN;L~aHj zWTIhmEw#dOVBdW`sPPSPkSl~nW2yWXmQouGN@F3h$q9%Daa5rj6U;;nY`oEl3HSgX zWU?GSq)zZvCoZ`B#ZqCmi7HMXv`2_sQ4RXQp5;KpaYx-FYmZ`R!M^N*t*eA?1y7ZH zL6wGQCqH`Cf9q}^Sky+a$7=Mx1NfVma_q3fDVvOQxp?f1Z+W6N2`s_*tC@2A_Fg1s~~-=%4EIc)}7azgs*qKIB=+4GXvBYlo7g%%Q+9$D~QXBWWbQ`TvN4Z zzx$|sf6L95wfdmP!8G#yVq`BUy%DnDJ|TV;`@`=>B|I}h^-9?u1vGEkJ)^`UJ92?6 z)R10lCPQH!!o2n_57jHr2@T+_D*~C=wgpq6zEGTTi3D{nU-1>H`7Pc*dc=f?>x-<; zOp)ZdAss|w2;K^RnANJcplVAP$jdm|Bg%H*v;Gl;@w`%9}FZW@bJJLn{efBBzG|w30+X+DKgD zCe#eiEw`A6GL=SJ_nvT*2*|=RsEIT2W${YjDH3aoC!_GR-XmC`=+2=H~}y=?xoc(QmH%kGeT3Vsqdg`=@Q z2tCW)Au|xE5~$%|RRL8U%CYWFz>VcrHQrTxQ#%FkCXIU4d{Y5b_U9-_X^e>QDe@fC zIcc;;{M)h9e~HfYMvK<%-2NcHz1ZdXf1kaV;61Q0O`YWVbGFW6^}ga{6KA=0deG8s zW-ZV7q7WBzJj%hImaoy_z34C1KD$gt}LV> zeme5^?OpyE75Ld}@Y^eLWYeMA2=QJ}3+iv3QZaqBRfs!^tWWaXsAdthb^>BSe~6TE z?Lsii!Xsd0&@+So04*!Ye4o7$>?jLYL6*`-&b+NH(;n0e?MR)36b#@-w}RlfJV%Oi z32$PBWC_Ta(6qkh@G6yT2NDuC-(Z}5 ztIcX-d_nLj%pCr(_9mU6ddIbJe~1^}{sl3VqI$x6S7iHF;oXTFXuPa?{&iT$Za<|| zvui`tBe_2qZLwZSb`ljQFWO#t_JC8K1HL~pc?fk96PP~f{NYkFAG0r+RJQnh%A6h^ zAp>kcpRz(>68i!6CniI+QfIA?iuxq!c!cH=-UsPb0Dh~c1|PD@oIT+@e`E&7Dd8Cw z`k~X+w=F%3Px9+bC5EXg%=P}1@?-j;d9A>m&ETF^qts8%o>sdL=~=(Y+CHQ>E*x-) zs*y$^7DR;Tta>V;ma-mqCm2KwNtA#Cw4)FKYBv*LywR9DB^h{x&;*UKkIs(_X8YgT zj99*x5%#8?v)K}ll*g!#e+9#Ale*#XUSMUtq@Mm-)X&?q6XFxz<@sWDi()7DSv|a` zr}r+*dr&s+X6=vncR@Yiqv&%S)JVgOMt?FspD={=aoBGMRJ)Y9u)KYX&6KlnYlYGC zr@PCq7XFSxUJ0+|Gf>}brk@;s^a`cujqVe|><{Y?olt|=uqzoRe=_Y=Agv6loRQX3 zbW&0sWqZW$E}B9Id_s9!wtAbvH0?*Il}JiEq^T=|u&Q2=-xW*oJKQhlA=}Yjm!qm_E%(*tC`@}@L(e3DI2}J7!!u%W2-6xOeTteBLb;ry*mkh%Z zuco~f=&#F!W1$GekJy{s;mza33ioPBo$lc}EbN1u*@>heO*tk`@z>Yko ze1>3J7vKo};8nTbw;PiL5y$2Oa5P-Hd8*mDa_&lD=pmc4D0XoOspW3ccI7BYZHZ&y z(O;6^A^IPtofZE8Na21J+mYQOm|u|}X{F-4-ki6<4+Z$Peoj(MXTyI`doU!UvXH_25>{q%*^@4Ca7npXf44ytI zjAhhQ2MUEdp2W_37ALV)k5YREP?osJ$H1Zf9nCYgbqHoA-HwN+&TrfUMbQg`_km5E zhb_&={q{=Rf1bGTln#H9WF@TS?q0M`?=b-ZoaGtYCr_iAgvVs!4|@DtoX0M8T+71X z?To#P{{Z8bZA(@A7hrp$LI)$xd2QfzW}^eyJ{0*$#_-o>^}CRq1AbsWLVm5$R^Kq`C%Zq|k-hd`Se+~)EC$oAxq!mO?=`v};Z?TWH zaDh0Ftte4AluvSbMlHDjU^^F@E!q`iM_aEf!QQJIXM94WBh*{kyrM&_dsFcmYY|Vy z{6oPsW!1)gpr$eFc%PXxqZS%WeqvE1LAOihUPu!E09Ic*a~pu`;U~bZ5paX3xbX|{ z?!^AEe@+Xh93W~Sr{D*zJmSSPoOKilVm5^Fj7H_<;v1w#wLct&@(+q&Tx|g~xEpZ? z#W;!Tm(H3(;Or{1Anl3vCKn>x$WA5>>8~*-u&hIEh(;G9Zb0G~j|Jqk0sAu}#XMs; zpS%xoTq0Kn@au%kKC=>~aNmTaO&!AxK@$TRfBo6}6rp;|r3nHg510^f2DmG~3|P%h z&Hn&QT$FCwkcGKR{huD4V6$j+O^dLcb;dxBE$ ze=G4J7uF=mlL-yeA2NS1@9&qZ|)*!ES`j@&#a-oy&O(#3puf(%zTi_LP z*8c!i$AW0vluy~5hl+bwufXh^ufxvWA9^6KCT0@*&}8TG_dAb*^JSub?7vEDxm&$e zUx{k$PS}*jMmyp{rRa{-;_OfB1?7-Ce~YR=Mk=9HEIZZ<1zwG+t2fH-R9?178(Edx za&9c9V%_G_*W>9=s7lx;?=J6=#coC?VP(U2hTzpWiFe|^7cIfI)X45Gp3Y`g_FaK) z*!;_jEh~59{_bJ>3l!OlQ-=7JMNXIbH}dD~&)8Lr=k%6sl6J$$d=^h7HuU+8f2`jx zo8U%gFF1kwLMxkcY5xE_8&SI7YcFw+aVjzgQn`BB{7V8Ei0n3GC+u9p7RL>{w|_AW zMmK<%Zq)w(Df%j_@*Ad~D!n!7uS5Krs`Nd(l8)&V!u-h9D)_H4gtx?d7n;;g&`4fu zcLwPL;+k@tcgN-_qnL+Z6nsMbe>;$o*)j8`^BvVsDsKsk9-yiRVxVENN!?JbDZ_<| zqNjq<*b}hqRRL%nYhl{IOR73z{fXEV4qoKoN7a#!fk5)!ZJw~$aAqb4W1!sX^}sY< zgf4#&L-W?TZgcwFq+)wuVo?0|BFRi?^lx9c<0%9`JIJ$^^_w}!NOn#Mf3HKDWl6`1 z)w`g6$p{bezeJrGoUPb)n28}ApFZVx3BZ10R?lR>L-KcW*@&Dy9M!4!2|5#&UhoG` zry@H;g~N6nKYHAP^6)=mtVdGRyt6$;I6a^g45I_SAv|Fbx}MeL;vcU5DdBcwcm;WH zNA;qsat)!03rXBv5poj^f2?EJnqbF!ri)1IE}BqPi*+s%2V*ZJq&FHTCYwX9D42DH zLOP^VgDUQqit_+T(qcV{sop#as6A5o!g+!Yio~Xom34qeaa=r5ZDddX0IIRJN8SU) zlZ#?sbj0`+^0O%KjFu)NRvs*#+{^VJYI0(Fh+b-K1eN5j&|L!Nf12476Bz+V8uzM> z^-_CXIG<}LcoVIY5gp)=ytr&CvMJ1;cnO5VC$~QH^RLVNQKxYe|b{9Yt0Gve8>qnCtIvj8jZ`%9_2s> zQuA=CGUDPs#qV~ncy=cYr~zJR_bfg`y4I=z-U9Plf&Eawnuy&+<&2}cMR_h=kLxeP z%ulQ;U5Vcn{?1*>61pG^!5}d-U1e0$|J$aNl9m!icb9axh$AQ6DUxG!d`0PQMwiqm z>246Dkxl^t>F)YH`@eYJY-caFv$K8TzOVZVQL+JDmHH5U&v$?b&m^K|52*1%(ZD`1 zs4`}MwOT&cy(HU4uoeXR?98@*=gg*gc|3{-{`;eN%%INQg#h~~ilg@^>Ds1W0#Ea? z(^5dZ_?5F$=5x-e8`xE;+rZ$EGA{nMlUiq6)&|QGV#6+@>gLbfFVT zs$Idj5uezydN1PcmG_py7GO{Z!g&NO= z{xC!!t7k2lp^~A&qE4kiRpg9%&qgEuG`X-jK0_=j2ANPx`K7edB&Cc zAb2+qHxAR35%6BKx-LY{O}Lk#Pqt^)*KvdoOh{*<6VRFZ`V|*|29=ne@jyTXS!i%C zSv}~x^7^0hRKnE>e0{Gth)G!>Ojt`Gp+YnQ?N0_6jvNVDUhjN(z^tEXNevWF z!mnBDmEID!Pf00~28YgpZA{eVZ-NdWQOBm5?JKO_9!UNxf))Se)d@|5vef%XI&2U_ zR457jO>^YMUFRhuujQxZ;Mh)ll85gs!j{s9J5ohLm7@f8P?-ZIpOD?bfp9iR-$DRCU2MR??45=Oc!C!LUdPoo3km&hdmQIpe_9brMf>lFL1?Z4=AwiduPn@DLB^+W=x`FKs2NErA60@4)=gQIZj|``*`Sy(NV@;TTFCYYqYQy^9TtDjA! z8Hf7A_A44DmSjsm`5R0vF!HD$2^C=yNjozZ1U8aBhfDYWZo>bmNP*97Y1bo2yYzuF z-h+s^jPIY7H!pj{a=&~YFA^3LL`!TJ zC~!{G-OXo&ZU}6?`3?%?j=$eD@UdY7Dd$VI{4_yA4ALC4i#nlhQipY9fupuEx{m`U z!z1sK{q#6)Glay(4H7$Ts*VzRWY!|PP>+W#w>*Rf3D301nZ~=F_WQ5c{(%j(hPMT8 zEX0OlIqmp~M1Q*86!!Or-MzkZn$k!COe6n8$#aPxg6ZV98kg)9EFj=8M{u@?61B5g z7}V-2@)?$J{e^RkO6L!m)W{i^;?p8whG!MtZ_EYwXy1wA1IUNhNTyN-4CHIa2MHqvYW4)=s;4>D#@hnA__WCT{OR;D^WBIYlSDDj^WDf@qmcn|9gJXu&Fw;R+&D|wikWWJl^MBd z-;EgWwn?PMohZdi0$u6nhuxySV#}<}L!|rq4H60BPNyrN8fL(|2}~@=k=hbrGqkXttnJZn~QeW3+qXj8r_fJP1w{ zX*D;dJN!{liXD#XKN9L;Y>bjUa;k#8@VC2Avf<$eD-Bg5a5tZG)OOfrg|K^*=x?WtT|~!TO*rUZ$VNti}vF;s}W1ZDV{y3!3u6*Df+5OpO#7 z3=Qp~W^YD-SN+?yLKQzFN#hG2JI8EAfk2}BvH~pj{_+k<8%&Q*fB zj_JNy_40o4S!?lb_Q7K1VOBzdA0hgx$Z(Cq)IS$oM!xp(zOw`tA0Rs@Cs?tBon^X7 z$EF6J!3>Q5u`#8GItUqc=Mi%L@;RD{ABs9`1WgRjEO_73fF z22w&lmXVq2&3scyg1)}nnr{{=w{X}+#^eG*2C0-NT-G|ru`^ckTeWjSS<;*c0Ye$_ z7WATU<4`x3(erj>I@yqhS&6UwQ`6!O9gkUVa=-d{?@V-*Waay?9_3@pt=k4eNrFzB z?FC>2z6BM|x8uzoLV}hhE(B)G(mdR@%EUY$)B*zV&KDjdFLkz?hFsE=Am9w-CYj#d zv;NhXqDY%}+4Menl@y$_`fCstdA^ow;;%EVnYQ*t7Ccs0PfW5>M}K20mG2X?a1t(! zlU6u~r9qWCq5p)viA`&F$$EjeA;wYo13-M68PGQA;foJIIw^HuBd|Hd?KndCm@VZA zSRn~ch(u6X5UBjJ0>_p;9MfBa@a4WYk7TI3pn0?u=RCt|2x|OoRJuGPD(89pVNxD} zNUS*?WmZRd_7>~JmXET#8JX$FiDSaB+lfBPPb%+*cdBHsutjCd=@@21X?k* zP*DM~QGwvU2V22k4uaz!a|b91HKsan8#VlmI+Xp(v!E+OqP{-M8jB1#j>K%pUT~7=&{oKy{p+lN{%ag-)TKav2pU}+XyA*P_IIE-kQFP@4{b?Uu(9+ zXl%VuR`AeK{q?*DQ|}|FN3eGY0iWhdJ9ekF>H1#-cufonUo!+j_Bm`h-@Q46X@5r~ zSE0|TL|W0$4G#D;YwQzP+LOstNR^Lb|A$g>E%2!N{EL=P_8Uxbvzykzo&C%y%8xu% zV@AsgIVz%IBwr%pTi@sSnn^OG_qn(!_DY4-0ung0)p!LanThK6c(D`N4OIJfeEt(J zfxJg~ysEuDO3rigvcoKIK6j*eKgj9-y6)$)nj;nUO;LymvEZL8eea~ckwR69KwoO_ zk>HLR_M3vfTd2|bMJ7U!GxuvyKYqV`N?_ljo z4jRg)R`eilN!3xk6y$%Bx8m8X{1$5_JQm?vPgAFpn0c<1B4 zE(d6%SAUa2Zi3FVMvCGp@A=;I*DjWhw|Dryp8j7lw~oL%EV|W00PHOXUHkVKWZ2EY zx2s4iuke7x=|DFBGA^Io1#4KEZS-GHmzf^ zM?lAH$Tsioy#Ql9TG#7Skj_hK&JnikeugQj_ah9X)_zHeShKx2bjt)K!*QTy(2SyM zLaXYjG2Icu&x;4E@L4A<(q#@W+v{}W&6kdvMFE$ zkA&v*I_BxME>iwh4i~e@7>ZPWWF>m~?zgF8xOt!mn=W+NXmUN>e+$}u{><&{Ou2a~ zH8LWR8hA>KY-3Z~oOc&lq(WTRn&@ZG%clHzYXxau7>>QNp-5`iyk%PwHEV!QJ=r&( z;7TL|qW5mOCM|jcieHDH;AzQD_X!V^lE?roUL!mg{vLSfK@IU4>^@MzKPEC8?>RJ= z1rganfk4(Q2xueSa5m331~5)G@~dJ`%>{8pW|OMq-`i`>h1PG-gh}D&79b)v#8hCB z-e#Fjyqh`N!JU_A8y%LTO_CZ=cU!H8I^b^F_=>R|Eps0IL^1fZ!@;QNqV~LoigP`~ zf*-Wk&01!@s#a0&S%Rv`Vm+M0o)z1mutaj_N8YwjPF&V6o?8#qK_+zhlu@HjWU|&?GHzxj8*ksO z12__?{pjC^1}w zEKDMjRSpx@YtY^E7~PfJ^1(dP=Q&Iy9lFH6k$wpl^{>}os21awF-H#g^`w67m_(2& zkqpdYAL`$5e1TY!)Ew!@mn7ZWjX7Pa`)@{}U%5_~14yQfGqMCf{3sUQtt=>0YJ4N! zmI}ARCS2wR4N&A{Jaz$nSzl&OHoIt6SR3Fsyd{p$lNz>%taa#X8Lr&bgy|xRjEwIS zk|nad*+%v$RT}f5olfJ9eQ)E3?P@Q^^jnTsYJR>L%06Xj#nThNRi?f1@AMM1Ts1i| zzA&oFWslK}=-0f`a@9biI2JN)5}2U3J^rY29~F{Yda1Lew>=GP>tW1oT6-uGH$UOV zH2d$BP%1v<$p_FfbkYAnYJAdY>Em~+-zPsHiIl#WhHN2gEeXR7U|Bj&LK z;hrg#`%q}<`q%gS*K(TrsWT1DkIa&VB%O3soCMB>>*@r@@xga`Vmk((UP*Cnudf=b z8rGcY@n>O9b%?Ga$JgrX*G}r;^9dVBRdnp zJs$w1ksDHvv)n9^F7mIA%<nXUJBo z#C)pk6;SMa6`b-a)3>5EZtnvWIYV0FKf z&S4HW8;cGwyB`a{N9MG3J=;CH#diSf&Sr2Rrp-W<2K&Ns)E2b{JAjH%W@6PW^Kay?%8MNtNMK2BLxft(@Xd0_wnBB-t0mA>FIQ`nR0gl?wN}`ZIdjOqb5ij+JtkmRXsb2GZI4k9Kml{6LX_*EN zwp9w5XKv(aKd1U42cbyNQL z`cwD@-KX7>4>D7Z#=;it*;*KRcBqb@vpRyLE%#DuP*%4kozRri?;`C{359UHS@3uD zv%<<4djE7V+!xTOl=7Z&(h}GTEHcmn7wN||W;P9HJFl>*3N&vOHvF?XnRbkck_(5V zz!ntg0hHhs48_ni`y;J!0PZucaSGO~Np88}Z{!WChLRjSfFd7gBV(3f)BOV}O)hO%p3g9r>|tL%7%2vhJ6o}S1)H;uQ)aE~3Dn)^-jA?u->@?m&l21ZEu0R+{rq_mPfOWYiez66hr z#wm|2iY)k@qw#wbClCc6sL{Z<^Sg0+8cC#?!8b~`E%(jUMziyUAMp_Iy4Rf6QL#9z{CkxsvBke&c@( zK0c`2%;Ap?TgbA`(QKiey-T3B1{JmIh)INNkh2#^=%_-pd34kOsbA3)$NC&E50MO% zcck)9Y9%d6x*TEq;2!=+%e@GxD55!1=GQ%O2OQ{2d*2E4)mKMGHcOude9G>-1ghN_ z2Q2-PB4QiDr9Q6~_$Z|Wf*X>y%!n?qpg9U8cd?&Fd4L=LqFSkUia6=N`niKHxA0M_ zOh%B-q(j?Xm*Xk`yYo-kEjy}IMq8ru`gK+J=;jP%#U#kH#TGQr72nz2{1wWmA-NQy z-&yOZIY4pBgB3UQ5~DGaHDuaxKi+ML9Q2K<=HbQsiv=WRbN=bvM9WE+V{B0uyntE3 zFE99n;KV7Hz{*tDSp&A%O zzmqY1k?fdRd-+`c;| zS=1xW5qq$L`ULStIEn*mJ1IHo-OmM4w|Gf6`IEl(T&av&h5P4-2CM&2ocyL6|5S86 zdEKTZLr1;=NYgQ$9iN&Xy@PnoSEGpYal45Vf|4;oE5fyrl2^!7*z6Q%iPk^5>cS}5_HF0k3d|CGuhw0-YbLb z;u^ByeTp~S&Hk*A-%9Hu2X5r43f;5QD3XzzpQJ7Bz>ia4D9sknXLOV8-5NX~HTwRv zj&lKE(azfJG~(}({%djt1-}+85Br^(1P_V zkh`LC;=b|JEH1I}{%BlF!*|@0*3&I5@;?Bj-K6QsrsVX)tE;eCdbZmtPVx#3kBuWR zcRHSHvdy{pzky4wAA$H% zPIL90>vMyUHHoM(eY(|J`kYjcUVhRGFTu~jiFB`QwCJ$8eD_O617A(p`AFOHA?vR# zcjAy1RdZ&iQ7l&S82Q_<$AQ}}{<|Fau@c<7wiO06Jv%~nBH5Q;_-s-;RoyN5H)s_V zhB5UK0okugb^7ByLi(WcZnJzikDsZTo;YpVBK;f9(XVVN9)FD@arw8Pkx=umnfbld zO1_+v^u;TqzA^I2f3GT6pQ_o7|0MuJQdJg7-{Nl4Kj(ZWu!u*R?k}1g!|x^$b;x8s z+ytPFztteL| zTSb&?x0U#4kD(t&oJhcb+$?M2mFS^CLZiq^+AGW>mfOKhXqDQ+QXBkn%?4=HA93>< zBk?uA^B<_j0BUN&D z7N4m8kh^=^WUy3IXEC#9ufeq%&gOqU{@Kj@0smWfI<8FJ#qcuT-KQigbfE_uNTEeL z?;2lb8k8Ahly=Gt2rM>_k_Da?3P+$T!$`9m3>9AIlNY5i^0Pr$s5NR{f@!h|?!DfU zoJ%32kH==wh#l)2t}2DBohn;L>+`vDA0K0G2}(bxK+KQP(})D|zS1bC-JSk8tZtKV zFWNw!2U$*D`iNo7r`q8Te3BBdPGv4#tp_**xoHR^Z8Q@15fNd^K>uIpoQVUNP+W&Q zB84Qs?K_G%7ECW4qJeU_VeCNn;&uF7NwGC8Z1l5VxDtVLiK`hqIjzrTkC>A&v~L=eqo1uex8 zIQ%AM(rYi3FL^Hjlybz9aqcD`il<^Yb81-Lgs+j1={nCUm5CuN$>WDv4~OC~=IY49 z4swy?ncOZyowbmt%=I!RusdeVH`LG=Mt)t_D)`WkI1FTOMmG-JoV z#|n5q*Ma>LL+16SCB+dwnf$YX4e6UG~1|tLbUhG@H#Z1U&C#~icbn@giY+P zM4ROpOH-9*`5|{2*c8W{XN8m}q(``8B#sB9xDS;JBNkkK)5bwIW+|RM(wB;%XG#uQ z$c7p>wY3}AjPpjCYaN~_V_rEU%~4V^e$o2!C!jmE_zwwWxk)lzLrgQZ|KUyDp8+&! zW~)bFKiQH`8t^P`JdJ)oG{2u-;OSlowC59NvKyRhM4Dm;`F0DWyrq0_*XC$=<7ra% zK>kK5RVUey{i%Sf{zIOI|$TZ@3VJKj-BPvIKB3r=<_cf z`~q0t;hPR})iai#GVZIoWR}^v7`MPjRokmplS>3V(pLl$G$gX5YNd*#?uPA-$aPDU zmT5Jy@mpW>7=7O~CaSdNq4g!_%I+qU8YZN9YH!DgZAxJL0qauI;o#t4D4T{Tg0zSc zhzN=T1iV;^xdEnETz1b^S~fM44qC;V1cqB@#c3^<()C`CSTf4Hmq9R$@+u?3hHPf1 z>PlP`3J6qi3YHG4^AlX+hr?a=tk)a2M0wN$YjoQfJIl7a0f^5LnlLfR@Ji?=PFWUs z@);-1>VBMPBa1*^UwZ4=Vy0sqPWTa(A8$&YRPrNESu5vMgz1^->4BLNf4IIEP#i($ z;H954VBp0iZ!TZ%S;Ezx#0U$QT^>~rthMrrIziFPSZ%oy_}t>Mm`=l9grqU_;t+^^ zyUxGhFl`iR)^PZ^-D)9&y^pw1j4eoC_RR;C*7lXCaTIQe?FP&CJZL0)=V!E|Hlzes zQw&L})}ag2{r--k$9xOjp!RtMe0MQJ(V1%vFiKvNRon)Watp-OoHAR${iMoP7+C{1$xvI~? zuyOQQ-IW~|tv>#6x+{~=C?(BRU*SjeJj1vQ+pqj(hoq+U=l`Lk)f68)xYZFoC_N
      - + - + diff --git a/websocket/__init__.py b/websocket/__init__.py index 5811206b..738551ec 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -49,6 +49,8 @@ ) from .messages import ( arm_save_session_from_user_text, + build_runtime_action_guard_retry_request, + emit_runtime_action_guard_confirmation_failure, merge_runtime_idle_followup_turn, process_message, receive_message, @@ -460,7 +462,29 @@ async def process_pending_requests(): await logger.log_runtime( "[RUNTIME ACTION] guard confirmation received" ) + continue + + retry_request = build_runtime_action_guard_retry_request( + message_data + ) + + if retry_request is not None: + await pending_requests.put( + retry_request + ) + await logger.log_runtime( + "[RUNTIME ACTION] stale guard confirmation " + "replayed once after reconnect" + ) + continue + await emit_runtime_action_guard_confirmation_failure( + context, + message_data, + ) + await logger.log_runtime( + "[RUNTIME ACTION] stale guard confirmation failed" + ) continue await logger.log_user( diff --git a/websocket/messages.py b/websocket/messages.py index 14286556..d711e3b7 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -13,6 +13,13 @@ from config_loader import config from rules.brain_context_builder import build_brain_context from runtime.runtime_context import RECENT_MESSAGES_MAX_PAIRS +from runtime.behavior_contract import ( + get_action_guard_name_for_runtime_action, +) +from contracts.rules_assembler import ( + get_runtime_action_display_name, + runtime_action_has_close_tag, +) from runtime.L1_memory import ( schedule_interrupted_runtime_memory_update, schedule_runtime_memory_update, @@ -183,6 +190,188 @@ async def receive_message( return None +def normalize_runtime_action_guard_retry( + value, +) -> dict: + + if not isinstance(value, dict): + return {} + + action = str( + value.get("action", "") + or "" + ).strip().lower() + guard = str( + value.get("guard", "") + or "" + ).strip() + confirmation_id = str( + value.get("confirmation_id", "") + or "" + ).strip() + action_id = str( + value.get("id", "") + or "" + ).strip() + context_snapshot = value.get( + "context_snapshot", + {}, + ) + if not isinstance(context_snapshot, dict): + context_snapshot = {} + + try: + attempt = int( + value.get("attempt", 0) + or 0 + ) + except (TypeError, ValueError): + attempt = 0 + + if ( + not action + or not guard + or not confirmation_id + or attempt != 1 + ): + return {} + + expected_guard = get_action_guard_name_for_runtime_action( + action + ) + + if not expected_guard or expected_guard != guard: + return {} + + retry = { + "action": action, + "guard": guard, + "confirmation_id": confirmation_id, + "id": action_id, + "attempt": 1, + } + + if context_snapshot: + retry["context_snapshot"] = dict( + context_snapshot + ) + + return retry + + +def build_runtime_action_guard_retry_request( + message_data: dict, +) -> dict | None: + + if not isinstance(message_data, dict): + return None + + decision = str( + message_data.get("decision", "") + or "" + ).strip().casefold() + + if decision != "continue": + return None + + retry = normalize_runtime_action_guard_retry({ + "action": message_data.get("action", ""), + "guard": message_data.get("guard", ""), + "confirmation_id": message_data.get( + "confirmation_id", + "", + ), + "id": message_data.get("id", ""), + "attempt": message_data.get("retry_attempt", 0), + "context_snapshot": message_data.get( + "retry_context_snapshot", + {}, + ), + }) + + user_text = str( + message_data.get("retry_user_message", "") + or "" + ).strip() + + if not retry or not user_text: + return None + + return { + "type": "runtime_action_guard_retry", + "text": user_text, + "runtime_action_guard_retry": retry, + } + + +async def emit_runtime_action_guard_confirmation_failure( + context, + message_data: dict, + *, + error: str = "runtime_action_confirmation_expired", +) -> None: + + emitter = getattr(context, "emitter", None) + emit = getattr(emitter, "emit", None) + + if emit is None: + return + + action = str( + message_data.get("action", "") + or "" + ).strip().lower() + confirmation_id = str( + message_data.get("confirmation_id", "") + or "" + ).strip() + action_id = str( + message_data.get("id", "") + or "" + ).strip() + + if not action or not confirmation_id: + return + + display_name = get_runtime_action_display_name( + action + ) + decision = str( + message_data.get("decision", "") + or "" + ).strip().casefold() + rejected = decision == "reject" + + payload = { + "type": "runtime_action", + "action": action, + "status": "failed", + "display_name": display_name, + "close_tag": runtime_action_has_close_tag(action), + "confirmation_id": confirmation_id, + "error": ( + "user_rejected_runtime_action" + if rejected + else error + ), + "text": ( + f"{display_name} cancelled" + if rejected + else f"{display_name}: FAILED" + ), + "detail": ( + "The original confirmation no longer exists after reconnect." + if not rejected + else "The stale confirmation was cancelled by the user." + ), + } + + if action_id: + payload["id"] = action_id + + await emit(payload) + + async def resolve_runtime_action_guard_confirmation( context, message_data: dict, @@ -742,6 +931,9 @@ async def process_message( ): websocket = context.websocket logger = context.logger + action_guard_retry = {} + is_action_guard_retry = False + retry_terminal_emitted = False try: @@ -752,6 +944,20 @@ async def process_message( if not isinstance(idle_followup, dict): idle_followup = {} is_idle_followup = bool(idle_followup) + action_guard_retry = normalize_runtime_action_guard_retry( + message_data.get( + "runtime_action_guard_retry", + {}, + ) + ) + context.runtime_action_guard_retry = action_guard_retry + context.runtime_action_guard_retry_consumed = False + context.runtime_suppress_chat_content = bool( + action_guard_retry + ) + is_action_guard_retry = bool( + action_guard_retry + ) user_text = ( str( @@ -780,7 +986,11 @@ async def process_message( context.runtime_current_turn_id = ( f"idle_{context.runtime_turn_counter:06d}" if is_idle_followup - else f"turn_{context.runtime_turn_counter:06d}" + else ( + f"retry_{context.runtime_turn_counter:06d}" + if is_action_guard_retry + else f"turn_{context.runtime_turn_counter:06d}" + ) ) if is_idle_followup: @@ -848,6 +1058,8 @@ async def process_message( else [] ) ) + elif is_action_guard_retry: + context.runtime_turn_attachments = [] else: message_attachments = message_data.get( "attachments", @@ -882,7 +1094,10 @@ async def process_message( context.runtime_context_limit_stage = "" context.runtime_context_limit_kind = "" context.runtime_context_limit_finish_reason = "" - if not is_idle_followup: + if ( + not is_idle_followup + and not is_action_guard_retry + ): await arm_save_session_from_user_text( context, user_text, @@ -933,6 +1148,24 @@ async def process_message( context, ) + if ( + action_guard_retry + and not getattr( + context, + "runtime_action_guard_retry_consumed", + False, + ) + ): + await emit_runtime_action_guard_confirmation_failure( + context, + { + **action_guard_retry, + "decision": "continue", + }, + error="runtime_action_confirmation_retry_not_emitted", + ) + retry_terminal_emitted = True + if getattr( context, "runtime_turn_discard_requested", @@ -981,7 +1214,7 @@ async def process_message( or "" ), ) - else: + elif not is_action_guard_retry: append_runtime_recent_turn( context, user_message=user_text, @@ -994,7 +1227,7 @@ async def process_message( assistant_created_at=assistant_created_at, ) - if is_idle_followup: + if is_idle_followup or is_action_guard_retry: memory_update_task = None elif getattr( context, @@ -1047,8 +1280,12 @@ async def process_message( context ) - context.assistant_message_count += 1 - if not is_idle_followup: + if not is_action_guard_retry: + context.assistant_message_count += 1 + if ( + not is_idle_followup + and not is_action_guard_retry + ): context.turn_number += 1 # Background fact-checking is intentionally not armed here. @@ -1056,6 +1293,24 @@ async def process_message( except asyncio.CancelledError: + if ( + action_guard_retry + and not retry_terminal_emitted + and not getattr( + context, + "runtime_action_guard_retry_consumed", + False, + ) + ): + await emit_runtime_action_guard_confirmation_failure( + context, + { + **action_guard_retry, + "decision": "continue", + }, + error="runtime_action_confirmation_retry_cancelled", + ) + await logger.log_runtime( "Agent runtime task cancelled." ) @@ -1064,12 +1319,36 @@ async def process_message( except Exception as error: + if ( + action_guard_retry + and not retry_terminal_emitted + and not getattr( + context, + "runtime_action_guard_retry_consumed", + False, + ) + ): + await emit_runtime_action_guard_confirmation_failure( + context, + { + **action_guard_retry, + "decision": "continue", + }, + error="runtime_action_confirmation_retry_failed", + ) + await handle_fatal_runtime_error( context, component="agent_runtime", exception=error, ) + finally: + + if is_action_guard_retry: + context.runtime_suppress_chat_content = False + context.runtime_action_guard_retry = {} + # --------------------------------------------------------- # CANCEL CURRENT TASK From d46a576af16b3a894ed6142be643a9dcfb987437 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:35:15 +0300 Subject: [PATCH 034/168] chore: tests - cleanup old namings - fix tests --- LIVE_AVATAR.md | 6 +- tests/runtime_actions/test_delayed_memory.py | 22 +- tests/runtime_actions/test_jin_color.py | 8 +- tests/runtime_actions/test_skill_actions.py | 533 ++++++------------ .../test_skill_marker_semantics.py | 34 +- tests/runtime_actions/test_stream_filter.py | 48 +- tests/test_brain_asset_flow.py | 292 +++------- tests/test_brain_prompt_memory.py | 30 +- tests/test_brain_runtime_actions.py | 411 +++++--------- tests/test_l1_memory.py | 4 +- ..._memory_citation_layers_client_contract.py | 4 +- ...t_memory_reference_sync_client_contract.py | 2 +- tests/test_memory_scheduler.py | 6 +- tests/test_python_skill_assets.py | 30 +- ..._runtime_avatar_l4_ring_client_contract.py | 4 +- tests/test_runtime_stream_tokens.py | 2 +- tests/test_runtime_todo.py | 10 +- tests/test_stream_validator.py | 2 +- ui/static/css/theme-win95.css | 4 +- ui/static/js/logger/logger.js | 2 +- ui/static/js/runtime/runtime-avatar.js | 16 +- ui/static/js/socket.js | 4 +- ui/static/js/socket/input.js | 4 +- ui/templates/index.html | 2 +- 24 files changed, 503 insertions(+), 977 deletions(-) diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index c20e2db1..42dadceb 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -494,11 +494,11 @@ This prevents two facts with the same key from glowing incorrectly. ## Central Button -The center button is visually part of the avatar. It currently refreshes the avatar drawing. +The center button is visually part of the avatar. It toggles the visibility of the avatar's outer memory layers. DOM id: -`#fact-check-trigger` +`#memory-layers-toggle` Visual style: @@ -511,7 +511,7 @@ Click behavior: Current click behavior: ```js -window.JinRuntime.avatar.refresh() +window.JinRuntime.avatar.toggleMemoryLayers() ``` The old manual fact-check WebSocket path still exists separately, but the center click currently redraws the avatar rather than starting fact-check. diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 91bb3ab5..ccbbe8c9 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -938,7 +938,7 @@ def test_load_delayed_memory_uses_loaded_context_block(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.delayed_memory_reports = { "a1b2c3": { @@ -1133,7 +1133,7 @@ def test_load_delayed_memory_keeps_multiple_reports(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.delayed_memory_reports = { "a1b2c3": { @@ -1264,7 +1264,7 @@ def test_invalid_load_delayed_memory_id_returns_failure_tool_result(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] context.delayed_memory_reports = { @@ -1339,7 +1339,7 @@ def test_load_delayed_memory_tracks_session_metadata(self): context.timestamp = "2026-07-17T19:40:00+03:00" context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.delayed_memory_reports = { "a1b2c3": { @@ -1413,7 +1413,7 @@ def test_load_delayed_memory_tracks_session_metadata(self): next_context.timestamp = "2026-07-19T12:15:00+03:00" next_context.runtime_action_events = [] next_context.runtime_search_calls = [] - next_context.runtime_appended_skills = [] + next_context.runtime_loaded_skills = [] next_context.runtime_asset_results = [] next_context.delayed_memory_reports = ( context.delayed_memory_reports @@ -1463,7 +1463,7 @@ def test_unload_delayed_memory_only_detaches_from_context(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_loaded_delayed_memory = { "id": "a1b2c3", @@ -1538,7 +1538,7 @@ def test_unload_delayed_memory_detaches_multiple_reports(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_loaded_delayed_memory = { "a1b2c3": { @@ -1638,7 +1638,7 @@ def test_invalid_unload_delayed_memory_id_returns_failed_result(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] context.delayed_memory_reports = { @@ -1722,7 +1722,7 @@ def test_missing_unload_delayed_memory_id_returns_failed_result(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] context.delayed_memory_reports = { @@ -1898,7 +1898,7 @@ def test_update_delayed_memory_appends_only_supported_fields(self): context.emitter = FakeEmitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] context.delayed_memory_reports = { @@ -2038,7 +2038,7 @@ def test_remove_pinned_delayed_memory_returns_failed_result(self): context.emitter = FakeEmitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] context.runtime_loaded_delayed_memory = { diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index f008b28e..b8cbd522 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -259,7 +259,7 @@ async def run_case(): context = SimpleNamespace( runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, @@ -330,7 +330,7 @@ async def run_case(): }, ], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, @@ -397,7 +397,7 @@ async def run_case(): context = SimpleNamespace( runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, @@ -476,7 +476,7 @@ async def run_case(): context = SimpleNamespace( runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, diff --git a/tests/runtime_actions/test_skill_actions.py b/tests/runtime_actions/test_skill_actions.py index 0c5d8507..1155f0e2 100644 --- a/tests/runtime_actions/test_skill_actions.py +++ b/tests/runtime_actions/test_skill_actions.py @@ -59,7 +59,7 @@ class RuntimeSkillActionTests(RuntimeActionTestCase): - def test_extracts_list_skills_marker(self): + def test_legacy_list_skills_marker_stays_visible_text(self): result = extract_runtime_actions( "", @@ -70,43 +70,21 @@ def test_extracts_list_skills_marker(self): self.assertEqual( result.text, - "", + "", ) self.assertEqual( result.actions, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), + (), ) - def test_extracts_current_list_skills_marker(self): - - result = extract_runtime_actions( - get_runtime_action_private_marker("LIST_SKILLS"), - enabled_actions=[ - "CAN_USE_ASSETS", - ], - ) + def test_list_skills_has_no_private_marker(self): self.assertEqual( - result.text, + get_runtime_action_private_marker("LIST_SKILLS"), "", ) - self.assertEqual( - result.actions, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), - ) - - def test_stream_filter_handles_split_self_closing_list_skills_marker(self): + def test_stream_filter_keeps_split_legacy_list_skills_marker_as_text(self): stream_filter = RuntimeActionStreamFilter( enabled_actions=[ @@ -122,38 +100,101 @@ def test_stream_filter_handles_split_self_closing_list_skills_marker(self): ) self.assertEqual( - first.text, - "", + first.text + second.text, + "", ) self.assertEqual( - first.actions, + first.actions + second.actions, (), ) self.assertEqual( - second.text, + stream_filter.flush(), "", ) + + def test_skill_inventory_lists_available_skills_without_runtime_action(self): + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + with contextlib.ExitStack() as stack: + for patcher in self.patch_asset_roots(root): + stack.enter_context(patcher) + + self.write_skill_fixture( + root, + "wildcards.txt", + "wildcards\nUse ASSET_ACTION for wildcard files.", + ) + self.write_skill_fixture( + root, + "file_manager.txt", + "file_manager\nUse ASSET_ACTION for asset files.", + ) + + result = list_skills() + + skills_by_name = { + skill["name"]: skill + for skill in result["skills"] + } self.assertEqual( - second.actions, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), + set(skills_by_name), + {"wildcards", "file_manager"}, ) + self.assertNotIn("content", skills_by_name["wildcards"]) + + def test_empty_skill_inventory_has_no_entries(self): + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + with contextlib.ExitStack() as stack: + for patcher in self.patch_asset_roots(root): + stack.enter_context(patcher) + + result = list_skills() + + self.assertEqual(result["skills"], []) + + def test_loading_missing_skill_records_current_load_error(self): + + context = FakeContext() + context.emitter = FakeEmitter() + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + with contextlib.ExitStack() as stack: + for patcher in self.patch_asset_roots(root): + stack.enter_context(patcher) + + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="LOAD_SKILL", + payload="missing_skill", + ), + ), + ) + ) + + self.assertEqual(applied_count, 1) + self.assertEqual(context.runtime_loaded_skills, []) self.assertEqual( - stream_filter.flush(), - "", + context.runtime_asset_results[0]["action"], + "load_skill", + ) + self.assertEqual( + context.runtime_asset_results[0]["error"], + "skill_not_found", ) - - def test_extracts_append_and_remove_skill_markers(self): + def test_extracts_load_and_unload_skill_markers(self): result = extract_runtime_actions( ( - "\n" - "" + "\n" + "" ), enabled_actions=[ "CAN_USE_ASSETS", @@ -168,24 +209,24 @@ def test_extracts_append_and_remove_skill_markers(self): result.actions, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="image_prompt_generator", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="wildcards", ), ), ) - def test_extracts_plural_append_and_remove_skill_markers(self): + def test_extracts_plural_append_and_unload_skill_markers(self): result = extract_runtime_actions( ( - "\n" - "" + "" ), enabled_actions=[ "CAN_USE_ASSETS", @@ -204,46 +245,46 @@ def test_extracts_plural_append_and_remove_skill_markers(self): result.actions, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_manager", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=append_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="image_prompt_generator", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=append_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="porn", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=append_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=append_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="old_skill", - marker_name="REMOVE_SKILLS", + marker_name="UNLOAD_SKILLS", marker_payload=remove_payload, - marker_group="remove_skills_002", + marker_group="unload_skills_002", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="unused_skill", - marker_name="REMOVE_SKILLS", + marker_name="UNLOAD_SKILLS", marker_payload=remove_payload, - marker_group="remove_skills_002", + marker_group="unload_skills_002", ), ), ) @@ -251,25 +292,25 @@ def test_extracts_plural_append_and_remove_skill_markers(self): result.observed_actions, ( RuntimeActionCall( - name="APPEND_SKILLS", + name="LOAD_SKILLS", payload=append_payload, - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=append_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="REMOVE_SKILLS", + name="UNLOAD_SKILLS", payload=remove_payload, - marker_name="REMOVE_SKILLS", + marker_name="UNLOAD_SKILLS", marker_payload=remove_payload, - marker_group="remove_skills_002", + marker_group="unload_skills_002", ), ), ) - def test_append_skill_name_attribute_stays_visible_text(self): + def test_load_skill_name_attribute_stays_visible_text(self): result = extract_runtime_actions( - '', + '', enabled_actions=[ "CAN_USE_ASSETS", ], @@ -277,7 +318,7 @@ def test_append_skill_name_attribute_stays_visible_text(self): self.assertEqual( result.text, - '', + '', ) self.assertEqual( result.actions, @@ -285,7 +326,7 @@ def test_append_skill_name_attribute_stays_visible_text(self): ) - def test_stream_filter_keeps_split_append_skill_name_attribute_as_text(self): + def test_stream_filter_keeps_split_load_skill_name_attribute_as_text(self): stream_filter = RuntimeActionStreamFilter( enabled_actions=[ @@ -294,7 +335,7 @@ def test_stream_filter_keeps_split_append_skill_name_attribute_as_text(self): ) first = stream_filter.filter( - '' @@ -302,7 +343,7 @@ def test_stream_filter_keeps_split_append_skill_name_attribute_as_text(self): self.assertEqual( first.text, - '" @@ -356,32 +397,32 @@ def test_stream_filter_handles_split_plural_append_skill_marker(self): second.actions, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_manager", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=marker_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="image_prompt_generator", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=marker_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="porn", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=marker_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=marker_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), ), ) @@ -389,11 +430,11 @@ def test_stream_filter_handles_split_plural_append_skill_marker(self): second.observed_actions, ( RuntimeActionCall( - name="APPEND_SKILLS", + name="LOAD_SKILLS", payload=marker_payload, - marker_name="APPEND_SKILLS", + marker_name="LOAD_SKILLS", marker_payload=marker_payload, - marker_group="append_skills_001", + marker_group="load_skills_001", ), ), ) @@ -401,34 +442,34 @@ def test_stream_filter_handles_split_plural_append_skill_marker(self): stream_filter.flush(), "", ) - def test_duplicate_append_skill_markers_are_preserved_as_text(self): + def test_duplicate_load_skill_markers_are_preserved_as_text(self): - appended_skill_names = set() + loaded_skill_names = set() - def preserve_duplicate_append_skill(_raw_marker, action): - if action.name != "APPEND_SKILL": + def preserve_duplicate_load_skill(_raw_marker, action): + if action.name != "LOAD_SKILL": return False requested_skill = normalize_skill_name( action.payload ) - if requested_skill in appended_skill_names: + if requested_skill in loaded_skill_names: return True - appended_skill_names.add( + loaded_skill_names.add( requested_skill ) return False text = ( "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "" + "\n" + "\n" + "\n" + "\n" + "\n" + "" ) result = extract_runtime_actions( @@ -437,7 +478,7 @@ def preserve_duplicate_append_skill(_raw_marker, action): "CAN_SAVE_SESSION", "CAN_USE_ASSETS", ], - preserve_action_marker=preserve_duplicate_append_skill, + preserve_action_marker=preserve_duplicate_load_skill, ) self.assertEqual( @@ -447,33 +488,33 @@ def preserve_duplicate_append_skill(_raw_marker, action): name="SAVE_SESSION", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_manager", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="image_prompt_generator", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="porn", ), ), ) self.assertIn( - "", + "", result.text, ) self.assertIn( - "", + "", result.text, ) self.assertNotIn( - "", + "", result.text, ) self.assertEqual( @@ -482,48 +523,48 @@ def preserve_duplicate_append_skill(_raw_marker, action): ) - def test_placeholder_append_skill_is_processed_once_and_duplicate_preserved(self): + def test_placeholder_load_skill_is_processed_once_and_duplicate_preserved(self): - appended_skill_names = set() + loaded_skill_names = set() - def preserve_duplicate_append_skill(_raw_marker, action): - if action.name != "APPEND_SKILL": + def preserve_duplicate_load_skill(_raw_marker, action): + if action.name != "LOAD_SKILL": return False requested_skill = normalize_skill_name( action.payload ) - if requested_skill in appended_skill_names: + if requested_skill in loaded_skill_names: return True - appended_skill_names.add( + loaded_skill_names.add( requested_skill ) return False result = extract_runtime_actions( ( - "\n" - "" + "\n" + "" ), enabled_actions=[ "CAN_USE_ASSETS", ], - preserve_action_marker=preserve_duplicate_append_skill, + preserve_action_marker=preserve_duplicate_load_skill, ) self.assertEqual( result.actions, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="name of skill", ), ), ) self.assertIn( - "", + "", result.text, ) self.assertEqual( @@ -532,206 +573,6 @@ def preserve_duplicate_append_skill(_raw_marker, action): ) - def test_apply_runtime_action_calls_lists_skills(self): - - Emitter = FakeEmitter - - Context = FakeContext - - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - with contextlib.ExitStack() as stack: - for patcher in self.patch_asset_roots(root): - stack.enter_context(patcher) - - self.write_skill_fixture( - root, - "wildcards.txt", - "wildcards\nUse ASSET_ACTION for wildcard files.", - ) - self.write_skill_fixture( - root, - "file_manager.txt", - "file_manager\nUse ASSET_ACTION for asset files.", - ) - - context = Context() - context.emitter = Emitter() - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), - ) - ) - - self.assertEqual( - applied_count, - 1, - ) - self.assertEqual( - context.runtime_asset_results[0]["action"], - "list_skills", - ) - self.assertEqual( - context.runtime_asset_results[0]["requested"], - "", - ) - skills_by_name = { - skill["name"]: skill - for skill in context.runtime_asset_results[0]["skills"] - } - self.assertIn( - "wildcards", - skills_by_name, - ) - self.assertIn( - "file_manager", - skills_by_name, - ) - self.assertNotIn( - "content", - skills_by_name["wildcards"], - ) - self.assertTrue( - (root / "assets" / "skills" / "wildcards.txt").exists() - ) - self.assertTrue( - (root / "assets" / "skills" / "file_manager.txt").exists() - ) - self.assertEqual( - context.emitter.events[0]["action"], - "list_skills", - ) - self.assertEqual( - context.emitter.events[0]["text"], - "Listed skills", - ) - self.assertEqual( - context.runtime_session_action_history[0]["text"], - "Listed skills", - ) - self.assertIsInstance( - context.runtime_session_action_history[0]["created_at"], - float, - ) - - def test_apply_runtime_action_calls_reads_list_skills_each_time(self): - - Emitter = FakeEmitter - - Context = FakeContext - - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - with contextlib.ExitStack() as stack: - for patcher in self.patch_asset_roots(root): - stack.enter_context(patcher) - - self.write_skill_fixture( - root, - "file_manager.txt", - "file_manager\nUse ASSET_ACTION for asset files.", - ) - - context = Context() - context.emitter = Emitter() - context.runtime_current_turn_id = "turn_000001" - - asyncio.run( - apply_runtime_action_calls( - context, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), - ) - ) - - context.runtime_current_turn_id = "turn_000002" - - asyncio.run( - apply_runtime_action_calls( - context, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), - ) - ) - - self.assertEqual( - len(context.runtime_asset_results), - 2, - ) - self.assertNotIn( - "runtime_action_reused", - context.runtime_asset_results[0], - ) - self.assertNotIn( - "runtime_action_reused", - context.runtime_asset_results[1], - ) - self.assertEqual( - context.runtime_asset_results[1][ - "runtime_turn_id" - ], - "turn_000002", - ) - - - def test_empty_list_skills_returns_default_no_entries_tool_result(self): - - Emitter = FakeEmitter - - Context = FakeContext - - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - with contextlib.ExitStack() as stack: - for patcher in self.patch_asset_roots(root): - stack.enter_context(patcher) - - context = Context() - context.emitter = Emitter() - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - ( - RuntimeActionCall( - name="LIST_SKILLS", - payload="", - ), - ), - ) - ) - - self.assertEqual( - applied_count, - 1, - ) - self.assertEqual( - context.runtime_asset_results[0]["skills"], - [], - ) - self.assertIn( - "No entries found.", - build_tool_results_context( - context - ), - ) - - def test_apply_runtime_action_calls_appends_and_removes_skill(self): Emitter = FakeEmitter @@ -767,11 +608,11 @@ def test_apply_runtime_action_calls_appends_and_removes_skill(self): context, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="Image Prompt Generator.txt", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="wildcards", ), ), @@ -784,32 +625,32 @@ def test_apply_runtime_action_calls_appends_and_removes_skill(self): 2, ) self.assertEqual( - context.runtime_appended_skills[0]["name"], + context.runtime_loaded_skills[0]["name"], "image_prompt_generator", ) self.assertEqual( - context.runtime_appended_skills[0]["path"], + context.runtime_loaded_skills[0]["path"], "assets/skills/Image Prompt Generator.txt", ) self.assertIn( "Describe images.", - context.runtime_appended_skills[0]["content"], + context.runtime_loaded_skills[0]["content"], ) self.assertEqual( context.emitter.events[0]["action"], - "append_skill", + "load_skill", ) self.assertEqual( context.emitter.events[0]["text"], - "APPEND_SKILL: Image Prompt Generator.txt", + "LOAD_SKILL: Image Prompt Generator.txt", ) self.assertEqual( context.emitter.events[2]["action"], - "remove_skill", + "unload_skill", ) self.assertEqual( context.emitter.events[2]["text"], - "REMOVE_SKILL: wildcards", + "UNLOAD_SKILL: wildcards", ) self.assertEqual( { @@ -853,7 +694,7 @@ def test_append_missing_skill_records_error_for_model_and_history(self): context, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_writer", ), ), @@ -865,12 +706,12 @@ def test_append_missing_skill_records_error_for_model_and_history(self): 1, ) self.assertEqual( - context.runtime_appended_skills, + context.runtime_loaded_skills, [], ) self.assertEqual( context.runtime_asset_results[0]["action"], - "append_skill", + "load_skill", ) self.assertEqual( context.runtime_asset_results[0]["requested"], @@ -882,7 +723,7 @@ def test_append_missing_skill_records_error_for_model_and_history(self): ) self.assertEqual( context.runtime_session_action_history[0]["text"], - "APPEND_SKILL: file_writer ( does not exist )", + "LOAD_SKILL: file_writer ( does not exist )", ) self.assertEqual( len(context.emitter.events), @@ -890,7 +731,7 @@ def test_append_missing_skill_records_error_for_model_and_history(self): ) self.assertEqual( context.emitter.events[0]["text"], - "APPEND_SKILL: file_writer ( does not exist )", + "LOAD_SKILL: file_writer ( does not exist )", ) self.assertEqual( context.emitter.events[0]["status"], @@ -898,7 +739,7 @@ def test_append_missing_skill_records_error_for_model_and_history(self): ) - def test_append_skill_blocks_other_actions_in_same_stream(self): + def test_load_skill_blocks_other_actions_in_same_stream(self): Emitter = FakeEmitter @@ -942,7 +783,7 @@ def test_append_skill_blocks_other_actions_in_same_stream(self): context, ( RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( @@ -958,7 +799,7 @@ def test_append_skill_blocks_other_actions_in_same_stream(self): 1, ) self.assertEqual( - context.runtime_appended_skills[0]["name"], + context.runtime_loaded_skills[0]["name"], "wildcards", ) self.assertTrue( @@ -970,7 +811,7 @@ def test_append_skill_blocks_other_actions_in_same_stream(self): for event in context.runtime_action_events ], [ - "append_skill", + "load_skill", ], ) self.assertFalse( diff --git a/tests/runtime_actions/test_skill_marker_semantics.py b/tests/runtime_actions/test_skill_marker_semantics.py index a4b93718..d7928f42 100644 --- a/tests/runtime_actions/test_skill_marker_semantics.py +++ b/tests/runtime_actions/test_skill_marker_semantics.py @@ -40,8 +40,8 @@ def _write_skills(self, root, *names): f"{name}\nTest skill.", ) - def test_plural_append_skills_is_one_uncounted_marker(self): - marker = "" + def test_plural_load_skills_is_one_uncounted_marker(self): + marker = "" parsed = extract_runtime_actions( marker, enabled_actions=["CAN_USE_ASSETS"], @@ -51,7 +51,7 @@ def test_plural_append_skills_is_one_uncounted_marker(self): [(action.name, action.payload) for action in parsed.observed_actions], [ ( - "APPEND_SKILLS", + "LOAD_SKILLS", "file_manager, wildcards, porn", ), ], @@ -91,7 +91,7 @@ def test_plural_append_skills_is_one_uncounted_marker(self): self.assertEqual(len(context.emitter.events), 2) self.assertEqual( {event["action"] for event in context.emitter.events}, - {"append_skills"}, + {"load_skills"}, ) self.assertEqual( {event["id"] for event in context.emitter.events}, @@ -99,7 +99,7 @@ def test_plural_append_skills_is_one_uncounted_marker(self): ) self.assertEqual( {event["text"] for event in context.emitter.events}, - {"APPEND_SKILLS: file_manager, wildcards, porn"}, + {"LOAD_SKILLS: file_manager, wildcards, porn"}, ) self.assertTrue( all("marker_count" not in event for event in context.emitter.events) @@ -109,14 +109,14 @@ def test_plural_append_skills_is_one_uncounted_marker(self): ) self.assertEqual( [item["text"] for item in context.runtime_session_action_history], - ["APPEND_SKILLS: file_manager, wildcards, porn"], + ["LOAD_SKILLS: file_manager, wildcards, porn"], ) - def test_singular_append_skill_markers_stay_separate_without_counter(self): + def test_singular_load_skill_markers_stay_separate_without_counter(self): parsed = extract_runtime_actions( ( - "\n" - "" + "\n" + "" ), enabled_actions=["CAN_USE_ASSETS"], ) @@ -155,8 +155,8 @@ def test_singular_append_skill_markers_stay_separate_without_counter(self): self.assertEqual( [event["text"] for event in completed_events], [ - "APPEND_SKILL: wildcards", - "APPEND_SKILL: porn", + "LOAD_SKILL: wildcards", + "LOAD_SKILL: porn", ], ) self.assertEqual( @@ -172,8 +172,8 @@ def test_singular_append_skill_markers_stay_separate_without_counter(self): self.assertEqual( [item["text"] for item in context.runtime_session_action_history], [ - "APPEND_SKILL: wildcards", - "APPEND_SKILL: porn", + "LOAD_SKILL: wildcards", + "LOAD_SKILL: porn", ], ) @@ -185,8 +185,8 @@ def test_counter_groups_only_identical_marker_payloads(self): RuntimeActionCall(name="WEB_SEARCH", payload="alpha"), RuntimeActionCall(name="WEB_SEARCH", payload="beta"), RuntimeActionCall(name="WEB_SEARCH", payload="alpha"), - RuntimeActionCall(name="APPEND_SKILL", payload="wildcards"), - RuntimeActionCall(name="APPEND_SKILL", payload="porn"), + RuntimeActionCall(name="LOAD_SKILL", payload="wildcards"), + RuntimeActionCall(name="LOAD_SKILL", payload="porn"), ]) self.assertEqual( @@ -261,8 +261,8 @@ def test_skill_marker_ui_contract_keeps_rows_separate_and_uncounted(self): index_source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn("keepSkillMarkerSeparate", logger_source) - self.assertIn('"APPEND_SKILLS"', logger_source) - self.assertIn('"append_skills"', runtime_source) + self.assertIn('"LOAD_SKILLS"', logger_source) + self.assertIn('"load_skills"', runtime_source) self.assertIn("suppressMarkerCount", runtime_source) self.assertRegex( index_source, diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index 9703dab6..a060e4ef 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -443,8 +443,6 @@ def test_extracts_self_closing_runtime_markers_without_blocks(self): cases = ( ("", "SAVE_SESSION", ""), - ("", "LIST_SKILLS", ""), - ("", "LIST_SKILLS", ""), ( "", "WEB_SEARCH", @@ -458,8 +456,8 @@ def test_extracts_self_closing_runtime_markers_without_blocks(self): "remember tea", ), ( - "", - "APPEND_SKILL", + "", + "LOAD_SKILL", "file_manager", ), ( @@ -685,8 +683,8 @@ def test_resolve_and_remove_actions_share_payload_normalization(self): "d4e5f6", ), ( - "", - "REMOVE_SKILL", + "", + "UNLOAD_SKILL", "wildcards", ), ) @@ -1265,11 +1263,11 @@ def test_marker_repetition_guard_flags_fifth_marker(self): ) result = stream_filter.filter( - "\n" - "\n" - "\n" - "\n" - "" + "\n" + "\n" + "\n" + "\n" + "" ) self.assertTrue( @@ -1294,17 +1292,17 @@ def test_marker_repetition_guard_flags_message_repeats(self): ) result = stream_filter.filter( - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" ) self.assertTrue( @@ -1845,7 +1843,7 @@ def test_clean_tool_results_action_clears_all_result_state(self): context.emitter = Emitter() context.runtime_action_events = [] context.runtime_search_calls = [] - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] context.runtime_tool_results = [ { "kind": TOOL_RESULT_KIND_SEARCH, @@ -2396,7 +2394,7 @@ async def run_case(): background_tasks=set(), runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_pending_requests_queue=queue, runtime_pending_idle_followups=[], runtime_idle_action_sequence=0, @@ -2489,7 +2487,7 @@ async def run_case(): background_tasks=set(), runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_pending_requests_queue=queue, runtime_pending_idle_followups=[], runtime_idle_action_sequence=0, diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 18867217..f533b7c8 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -65,7 +65,7 @@ def _context(): runtime_search_calls=[], runtime_asset_results=[], runtime_delayed_memory_results=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_action_events=[], ) @@ -724,7 +724,7 @@ async def test_followup_places_current_sequence_under_latest_request(self): "runtime_turn_id": "turn_000002", }, { - "text": "APPEND_SKILL", + "text": "LOAD_SKILL", "created_at": 998.0, "runtime_turn_id": "turn_000002", }, @@ -737,7 +737,7 @@ async def test_followup_places_current_sequence_under_latest_request(self): "\n" " 1. SAVE_ACTIVE_MEMORY\n" " 2. LIST_SKILLS\n" - " 3. APPEND_SKILL\n" + " 3. LOAD_SKILL\n" "\n\n" "RULES" ) @@ -762,7 +762,7 @@ async def test_followup_places_current_sequence_under_latest_request(self): "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" " --- Sequence started ---\n" " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed: APPEND_SKILL ( 2s ago )\n" + " JIN message 2 executed: LOAD_SKILL ( 2s ago )\n" "", prompt, ) @@ -1044,7 +1044,7 @@ async def fake_run_brain_stream(**kwargs): "что на скриншоте?\n\n" "Attached context:\n" "- screen.png: image, image/png, 462.8 KB\n" - " runtime_attachment: full content is available to appended skills" + " runtime_attachment: full content is available to loaded skills" ) state = AgentState( user_input="что на скриншоте?", @@ -1782,7 +1782,7 @@ async def fake_emit_brain_text(**kwargs): "Created `assets/wildcards/clothing/test_bottoms.txt` with 2 lines.", ) - async def test_append_skill_result_continues_with_appended_skill_context(self): + async def test_load_skill_result_continues_with_loaded_skill_context(self): calls = [] @@ -1807,10 +1807,10 @@ async def fake_run_brain_stream(**kwargs): if len(calls) == 2: context.runtime_action_events.append({ - "name": "append_skill", + "name": "load_skill", "payload": "wildcards", }) - context.runtime_appended_skills.append({ + context.runtime_loaded_skills.append({ "name": "wildcards", "path": "assets/skills/wildcards.txt", "line_count": 39, @@ -1823,14 +1823,14 @@ async def fake_run_brain_stream(**kwargs): self, kwargs, state.translated_input, - 'APPEND_SKILL', + 'LOAD_SKILL', ) return ( "Ready to use the wildcard skill.", "", ) - self.fail("Brain model kept running after appended skill answer") + self.fail("Brain model kept running after loaded skill answer") context = _context() state = AgentState( @@ -1870,7 +1870,7 @@ async def fake_run_brain_stream(**kwargs): "Ready to use the wildcard skill.", ) - async def test_append_skill_visible_answer_triggers_followup(self): + async def test_load_skill_visible_answer_triggers_followup(self): calls = [] @@ -1880,10 +1880,10 @@ async def fake_run_brain_stream(**kwargs): if len(calls) == 1: context.runtime_action_events.append({ - "name": "append_skill", + "name": "load_skill", "payload": "wildcards", }) - context.runtime_appended_skills.append({ + context.runtime_loaded_skills.append({ "name": "wildcards", "path": "assets/skills/wildcards.txt", "line_count": 39, @@ -1899,7 +1899,7 @@ async def fake_run_brain_stream(**kwargs): self, kwargs, state.translated_input, - 'APPEND_SKILL', + 'LOAD_SKILL', ) return ( "Ready to test with the wildcards skill loaded.", @@ -1946,40 +1946,22 @@ async def fake_run_brain_stream(**kwargs): "Ready to test with the wildcards skill loaded.", ) - async def test_streamed_list_skills_append_skills_reaches_final_followup(self): + async def test_streamed_load_skills_reaches_final_followup(self): class FakeBrainClient: def __init__(self): self.calls = 0 - self.system_prompts = [] async def stream(self, **_kwargs): self.calls += 1 - self.system_prompts.append( - _kwargs.get( - "system_prompt", - "", - ) - ) if self.calls == 1: yield { "type": "content", "content": ( - "Need the available skills first. " - "" - ), - } - return - - if self.calls == 2: - yield { - "type": "content", - "content": ( - "Append the requested skills. " - "\n" - "" + "Load the requested skills. " + "" ), } return @@ -2026,35 +2008,16 @@ async def log_system(self, *_args, **_kwargs): return None def write_skill(root, name, content): - path = ( - root - / "assets" - / "skills" - / name - ) - path.parent.mkdir( - parents=True, - exist_ok=True, - ) - path.write_text( - content, - encoding="utf-8", - ) + path = root / "assets" / "skills" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") fake_client = FakeBrainClient() - websocket = FakeWebSocket() - emitter = FakeEmitter() - user_input = ( - "посмотри скилы, сделай апенд chunk_reader " - "и image_prompt_generator" - ) context = SimpleNamespace( logger=FakeLogger(), - websocket=websocket, - emitter=emitter, - clients={ - "brain": fake_client, - }, + websocket=FakeWebSocket(), + emitter=FakeEmitter(), + clients={"brain": fake_client}, active_streams={}, runtime_search_queries=[], runtime_search_calls=[], @@ -2064,7 +2027,7 @@ def write_skill(root, name, content): runtime_asset_retry_results=[], runtime_asset_retry_context=[], runtime_delayed_memory_results=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_action_events=[], runtime_tool_results=[], runtime_tool_results_turn_count=0, @@ -2073,7 +2036,7 @@ def write_skill(root, name, content): runtime_current_sequence_turn_id="turn_000001", runtime_turn_started_at=1, runtime_current_sequence_started_at=1, - runtime_turn_user_message=user_input, + runtime_turn_user_message="load chunk_reader and image_prompt_generator", runtime_turn_abort_requested=False, runtime_turn_interrupted=False, runtime_reasoning_recovery_pending=False, @@ -2091,9 +2054,7 @@ def write_skill(root, name, content): active_memory_records=[], background_tasks=set(), ) - state = AgentState( - user_input=user_input, - ) + state = AgentState(user_input=context.runtime_turn_user_message) state.translated_input = state.user_input with tempfile.TemporaryDirectory() as temp_dir: @@ -2102,28 +2063,15 @@ def write_skill(root, name, content): for patcher in patch_asset_roots(root): stack.enter_context(patcher) - write_skill( - root, - "chunk_reader.txt", - "chunk_reader\nRead large files in chunks.", - ) - write_skill( - root, - "image_prompt_generator.txt", - "image_prompt_generator\nGenerate image prompts.", - ) + write_skill(root, "chunk_reader.txt", "chunk_reader\nRead large files in chunks.") + write_skill(root, "image_prompt_generator.txt", "image_prompt_generator\nGenerate image prompts.") with patch( "agent.nodes.brain.get_brain_runtime_config", return_value=_brain_runtime(), ), patch( "agent.nodes.brain.build_brain_context", - side_effect=lambda current_context, **_kwargs: ( - "system prompt\n" - + build_tool_results_context( - current_context - ) - ), + return_value="system prompt", ), patch( "agent.nodes.brain.build_brain_payload", return_value="brain payload", @@ -2137,68 +2085,20 @@ def write_skill(root, name, content): "runtime.stream.record_stream_token_usage", new=lambda *_args, **_kwargs: None, ): - await BrainNode().run( - state, - context, - ) + await BrainNode().run(state, context) + self.assertEqual(fake_client.calls, 2) + self.assertEqual(state.brain_response, "Ready.") self.assertEqual( - fake_client.calls, - 3, - ) - self.assertIn( - "LIST_SKILLS", - fake_client.system_prompts[1], - ) - self.assertIn( - "chunk_reader", - fake_client.system_prompts[1], - ) - self.assertIn( - "APPEND_SKILL", - fake_client.system_prompts[2], - ) - self.assertIn( - "image_prompt_generator", - fake_client.system_prompts[2], - ) - self.assertEqual( - state.brain_response, - "Ready.", - ) - self.assertEqual( - [ - event["name"] - for event in context.runtime_action_events - ], - [ - "list_skills", - "append_skill", - "append_skill", - ], + [event["name"] for event in context.runtime_action_events], + ["load_skill", "load_skill"], ) self.assertEqual( - [ - entry["result"]["action"] - for entry in context.runtime_tool_results - if entry.get("kind") == TOOL_RESULT_KIND_ASSET - ], - [ - "list_skills", - ], - ) - self.assertEqual( - [ - skill["name"] - for skill in context.runtime_appended_skills - ], - [ - "chunk_reader", - "image_prompt_generator", - ], + [skill["name"] for skill in context.runtime_loaded_skills], + ["chunk_reader", "image_prompt_generator"], ) - async def test_list_skills_followup_survives_current_turn_id_shift(self): + async def test_load_skill_followup_survives_current_turn_id_shift(self): class FakeBrainClient: @@ -2212,12 +2112,12 @@ async def stream(self, **kwargs): yield { "type": "content", "content": ( - "Need available skills. " - " trailing text" + "Load the needed skill. " + " trailing text" ), } kwargs["context"].runtime_current_turn_id = ( - "turn_changed_after_list_skills" + "turn_changed_after_load_skill" ) return @@ -2227,65 +2127,37 @@ async def stream(self, **kwargs): } class FakeWebSocket: - async def send_json(self, _payload): return None class FakeEmitter: - async def emit(self, _payload): return None class FakeLogger: - - async def log_runtime(self, *_args, **_kwargs): - return None - - async def log_validator(self, *_args, **_kwargs): - return None - - async def log_error(self, *_args, **_kwargs): - return None - - async def log_brain(self, *_args, **_kwargs): - return None - - async def log_service_as_brain(self, *_args, **_kwargs): - return None - - async def log_service_as_brain_output(self, *_args, **_kwargs): - return None - - async def log_flow(self, *_args, **_kwargs): - return None - - async def log(self, *_args, **_kwargs): - return None - - async def log_system(self, *_args, **_kwargs): - return None + async def log_runtime(self, *_args, **_kwargs): return None + async def log_validator(self, *_args, **_kwargs): return None + async def log_error(self, *_args, **_kwargs): return None + async def log_brain(self, *_args, **_kwargs): return None + async def log_service_as_brain(self, *_args, **_kwargs): return None + async def log_service_as_brain_output(self, *_args, **_kwargs): return None + async def log_flow(self, *_args, **_kwargs): return None + async def log(self, *_args, **_kwargs): return None + async def log_system(self, *_args, **_kwargs): return None fake_client = FakeBrainClient() context = RuntimeContext( websocket=FakeWebSocket(), emitter=FakeEmitter(), logger=FakeLogger(), - clients={ - "brain": fake_client, - "service": fake_client, - }, + clients={"brain": fake_client, "service": fake_client}, ) context.runtime_current_turn_id = "turn_000001" context.runtime_current_sequence_turn_id = "turn_000001" context.runtime_turn_started_at = 1 context.runtime_current_sequence_started_at = 1 - context.runtime_turn_user_message = ( - "посмотри скилы, потом продолжай" - ) - - state = AgentState( - user_input=context.runtime_turn_user_message, - ) + context.runtime_turn_user_message = "load chunk_reader, then continue" + state = AgentState(user_input=context.runtime_turn_user_message) with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) @@ -2293,50 +2165,18 @@ async def log_system(self, *_args, **_kwargs): for patcher in patch_asset_roots(root): stack.enter_context(patcher) - skill_path = ( - root - / "assets" - / "skills" - / "chunk_reader.txt" - ) - skill_path.parent.mkdir( - parents=True, - exist_ok=True, - ) - skill_path.write_text( - "chunk_reader\nRead large files.", - encoding="utf-8", - ) + skill_path = root / "assets" / "skills" / "chunk_reader.txt" + skill_path.parent.mkdir(parents=True, exist_ok=True) + skill_path.write_text("chunk_reader\nRead large files.", encoding="utf-8") - await AgentRuntime().run( - state, - context, - ) + await AgentRuntime().run(state, context) - self.assertEqual( - fake_client.calls, - 2, - ) - self.assertEqual( - state.final_answer, - "Follow-up continued.", - ) - self.assertEqual( - context.runtime_action_events[0]["name"], - "list_skills", - ) - self.assertEqual( - context.runtime_action_events[0]["runtime_turn_id"], - "turn_000001", - ) - self.assertEqual( - context.runtime_current_turn_id, - "turn_changed_after_list_skills", - ) - self.assertEqual( - context.runtime_asset_results[0]["action"], - "list_skills", - ) + self.assertEqual(fake_client.calls, 2) + self.assertEqual(state.final_answer, "Follow-up continued.") + self.assertEqual(context.runtime_action_events[0]["name"], "load_skill") + self.assertEqual(context.runtime_action_events[0]["runtime_turn_id"], "turn_000001") + self.assertEqual(context.runtime_current_turn_id, "turn_changed_after_load_skill") + self.assertEqual(context.runtime_loaded_skills[0]["name"], "chunk_reader") async def test_asset_workflow_can_continue_after_create_file_to_prompt_batch(self): @@ -3014,7 +2854,7 @@ async def test_follow_up_action_messages_keep_actions_enabled(self): calls = [] action_names = [ - "append_skill", + "load_skill", "list_skills", "check_todo", ] @@ -3072,7 +2912,7 @@ async def fake_run_brain_stream(**kwargs): 4, ) self.assertIn( - 'APPEND_SKILL', + 'LOAD_SKILL', calls[1]["system_prompt"], ) self.assertIn( @@ -3109,7 +2949,7 @@ async def fake_run_brain_stream(**kwargs): if len(calls) == 1: context.runtime_action_events.extend([ { - "name": "append_skill", + "name": "load_skill", "payload": "first", }, { @@ -3120,7 +2960,7 @@ async def fake_run_brain_stream(**kwargs): return "", "" self.assertIn( - 'APPEND_SKILL', + 'LOAD_SKILL', kwargs["system_prompt"], ) self.assertIn( diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 2e96368c..1c85507e 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -202,7 +202,7 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): turn_number=1, user_message_count=2, assistant_message_count=1, - runtime_appended_skills=[ + runtime_loaded_skills=[ { "name": "wildcards", }, @@ -213,7 +213,7 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): "created_at": 40.0, }, { - "text": "Appended skill: wildcards", + "text": "Loaded skill: wildcards", "created_at": 940.0, }, { @@ -253,11 +253,11 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): prompt.index(""), ) self.assertLess( - prompt.index(""), - prompt.index(""), + prompt.index(""), + prompt.index(""), ) self.assertLess( - prompt.index(""), + prompt.index(""), prompt.index(""), ) self.assertIn( @@ -265,7 +265,7 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): prompt, ) self.assertIn( - "\n 1. wildcards\n", + "\n [\n {\n \"name\": \"wildcards\"\n }\n ]\n", prompt, ) self.assertIn( @@ -273,7 +273,7 @@ def test_brain_prompt_places_runtime_state_before_session_actions_history(self): prompt, ) self.assertIn( - "2. Appended skill: wildcards ( 1m ago )", + "2. Loaded skill: wildcards ( 1m ago )", prompt, ) self.assertIn( @@ -505,12 +505,12 @@ def test_action_context_hides_single_emission_counts(self): }, { "text": ( - "APPEND_SKILL: file_manager (count: 3), " + "LOAD_SKILL: file_manager (count: 3), " "CLEAN_TOOL_RESULTS" ), "parts": [ { - "text": "APPEND_SKILL: file_manager", + "text": "LOAD_SKILL: file_manager", "count": 3, }, { @@ -554,7 +554,7 @@ def test_action_context_hides_single_emission_counts(self): ) self.assertIn( ( - "JIN message 2 executed: APPEND_SKILL: file_manager (count: 3), " + "JIN message 2 executed: LOAD_SKILL: file_manager (count: 3), " "CLEAN_TOOL_RESULTS ( 2s ago )" ), history, @@ -666,7 +666,7 @@ def test_current_actions_history_filters_older_session_actions(self): "runtime_turn_id": "turn_000002", }, { - "text": "APPEND_SKILL", + "text": "LOAD_SKILL", "created_at": 998.0, "runtime_turn_id": "turn_000002", }, @@ -688,7 +688,7 @@ def test_current_actions_history_filters_older_session_actions(self): "\n" " --- Sequence started ---\n" " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed: APPEND_SKILL ( 2s ago )\n" + " JIN message 2 executed: LOAD_SKILL ( 2s ago )\n" "" ), ) @@ -775,7 +775,7 @@ def test_completed_sequence_is_wrapped_in_session_history(self): "runtime_turn_id": "turn_000002", }, { - "text": "APPEND_SKILL", + "text": "LOAD_SKILL", "created_at": 998.0, "runtime_turn_id": "turn_000002", }, @@ -797,7 +797,7 @@ def test_completed_sequence_is_wrapped_in_session_history(self): " 1. SAVE_ACTIVE_MEMORY ( 3m ago )\n" " --- Sequence started ---\n" " 2. LIST_SKILLS ( 55s ago )\n" - " 3. APPEND_SKILL ( 2s ago )\n" + " 3. LOAD_SKILL ( 2s ago )\n" " --- Sequence ended ---\n" "" ), @@ -818,7 +818,7 @@ def test_brain_prompt_does_not_count_runtime_actions_as_messages(self): "name": "list_skills", }, { - "name": "append_skill", + "name": "load_skill", }, ], ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 2c93ab8b..636029d1 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -75,13 +75,6 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: if bool(runtime_actions.get("CAN_SAVE_SESSION", False)): expected_actions.append("SAVE_SESSION") - if bool(runtime_actions.get("CAN_USE_ASSETS", False)): - expected_actions.extend( - ( - "LIST_SKILLS", - ) - ) - if bool(runtime_actions.get("CAN_CLEAN_TOOL_RESULTS", False)): expected_actions.append( "CLEAN_TOOL_RESULTS" @@ -105,8 +98,8 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: if bool(runtime_actions.get("CAN_USE_ASSETS", False)): expected_actions.extend( ( - "APPEND_SKILL", - "REMOVE_SKILL", + "LOAD_SKILL", + "UNLOAD_SKILL", "ASSET_ACTION", ) ) @@ -954,7 +947,7 @@ async def collect(context): emitter=FakeEmitter(), runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, @@ -1013,7 +1006,7 @@ async def stream(self, **_kwargs): "astronomical news tracker>" ), "\n", - "", + "", ): yield { "type": "content", @@ -1067,7 +1060,7 @@ async def collect(context): [ "web_search", "save_active_memory", - "list_skills", + "load_skill", ], ) self.assertEqual( @@ -1085,8 +1078,8 @@ async def collect(context): context.active_memory_records[0], ) self.assertEqual( - context.runtime_asset_results[-1]["action"], - "list_skills", + context.runtime_loaded_skills[-1]["name"], + "wildcards", ) self.assertEqual( [ @@ -1094,9 +1087,10 @@ async def collect(context): for item in context.runtime_session_action_history ], [ - "WEB_SEARCH - Latest astronomical news 2026", - "SAVE_ACTIVE_MEMORY - astronomical news tracker", - "LIST_SKILLS", + ( + "WEB_SEARCH - Latest astronomical news 2026, " + "SAVE_ACTIVE_MEMORY - astronomical news tracker" + ), ], ) self.assertEqual( @@ -1110,18 +1104,11 @@ async def collect(context): "text": "WEB_SEARCH", "detail": "Latest astronomical news 2026", }, - ], - [ { "text": "SAVE_ACTIVE_MEMORY", "detail": "astronomical news tracker", }, ], - [ - { - "text": "LIST_SKILLS", - }, - ], ], ) @@ -1227,7 +1214,7 @@ async def run_case(): context = SimpleNamespace( runtime_action_events=[], runtime_search_calls=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], runtime_save_session_requested=False, runtime_save_session_action_emitted=False, runtime_skill_state_barrier_active=False, @@ -1333,7 +1320,7 @@ async def run_case(): ], ) - def test_stream_groups_two_action_markers_into_one_history_item(self): + def test_stream_groups_two_current_action_markers_into_one_history_item(self): class FakeBrainClient: async def stream(self, **_kwargs): @@ -1341,7 +1328,7 @@ async def stream(self, **_kwargs): "type": "content", "content": ( "\n" - "" + "" ), } @@ -1349,62 +1336,42 @@ class Context: pass async def collect(context): - chunks = [] - - async for chunk in ask_brain_stream( - client=FakeBrainClient(), - text="save session and list skills", - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - "CAN_USE_ASSETS": True, - }, - ): - chunks.append( - chunk + return [ + chunk + async for chunk in ask_brain_stream( + client=FakeBrainClient(), + text="save session and set color", + context=context, + runtime_actions={ + "CAN_SAVE_SESSION": True, + "CAN_JIN_COLOR": True, + }, ) - - return chunks + ] context = Context() original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN config.USE_SERVICE_AS_BRAIN = False try: - asyncio.run( - collect( - context - ) - ) + asyncio.run(collect(context)) finally: config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain self.assertEqual( - [ - item["text"] - for item in context.runtime_session_action_history - ], - [ - "SAVE_SESSION, LIST_SKILLS", - ], + [item["text"] for item in context.runtime_session_action_history], + ["SAVE_SESSION, JIN_COLOR"], ) prompt = build_brain_context( context=context, runtime_actions={ "CAN_SAVE_SESSION": True, - "CAN_USE_ASSETS": True, + "CAN_JIN_COLOR": True, }, ) - - self.assertIn( - "", - prompt, - ) - self.assertIn( - "1. SAVE_SESSION, LIST_SKILLS", - prompt, - ) + self.assertIn("", prompt) + self.assertIn("1. SAVE_SESSION, JIN_COLOR", prompt) def test_stream_history_preserves_duplicate_markers_after_action_dedup(self): @@ -1498,34 +1465,34 @@ def test_session_history_compacts_many_repeated_markers(self): prompt, ) - def test_session_history_includes_appended_and_removed_skill_names(self): + def test_session_history_includes_loaded_and_unloaded_skill_names(self): formatted = format_session_action_marker_names([ RuntimeActionCall( name="LIST_SKILLS", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_manager", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="image_prompt_generator", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="image_prompt_generator", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="file_manager", ), ]) @@ -1534,14 +1501,14 @@ def test_session_history_includes_appended_and_removed_skill_names(self): formatted, ( "LIST_SKILLS, " - "APPEND_SKILL: wildcards, " - "APPEND_SKILL: file_manager, " - "REMOVE_SKILL: image_prompt_generator, " - "REMOVE_SKILL: file_manager" + "LOAD_SKILL: wildcards, " + "LOAD_SKILL: file_manager, " + "UNLOAD_SKILL: image_prompt_generator, " + "UNLOAD_SKILL: file_manager" ), ) - def test_session_history_adds_count_to_every_marker_part(self): + def test_session_history_groups_current_marker_parts_for_turn(self): context = SimpleNamespace( runtime_session_action_history=[], @@ -1552,60 +1519,23 @@ def test_session_history_adds_count_to_every_marker_part(self): context, 0, [ - RuntimeActionCall( - name="WEB_SEARCH", - payload="latest news", - ), - RuntimeActionCall( - name="SAVE_ACTIVE_MEMORY", - payload="remember coffee", - ), - RuntimeActionCall( - name="IDLE", - payload="5s", - ), - RuntimeActionCall( - name="APPEND_SKILL", - payload="wildcards", - ), - RuntimeActionCall( - name="APPEND_SKILL", - payload="file_manager", - ), + RuntimeActionCall(name="WEB_SEARCH", payload="latest news"), + RuntimeActionCall(name="SAVE_ACTIVE_MEMORY", payload="remember coffee"), + RuntimeActionCall(name="IDLE", payload="5s"), + RuntimeActionCall(name="LOAD_SKILL", payload="wildcards"), + RuntimeActionCall(name="LOAD_SKILL", payload="file_manager"), ], ) self.assertEqual( - [ - item["parts"] - for item in context.runtime_session_action_history - ], - [ - [ - { - "text": "WEB_SEARCH", - "detail": "latest news", - }, - ], - [ - { - "text": "SAVE_ACTIVE_MEMORY", - "detail": "remember coffee", - }, - ], - [ - { - "text": "IDLE", - "detail": "5s", - }, - { - "text": "APPEND_SKILL: wildcards", - }, - { - "text": "APPEND_SKILL: file_manager", - }, - ], - ], + [item["parts"] for item in context.runtime_session_action_history], + [[ + {"text": "WEB_SEARCH", "detail": "latest news"}, + {"text": "SAVE_ACTIVE_MEMORY", "detail": "remember coffee"}, + {"text": "IDLE", "detail": "5s"}, + {"text": "LOAD_SKILL: wildcards"}, + {"text": "LOAD_SKILL: file_manager"}, + ]], ) def test_jin_color_history_preserves_ordered_color_swatches(self): @@ -1940,19 +1870,19 @@ def test_replace_session_history_preserves_skill_marker_payloads(self): 0, [ RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="wildcards", ), RuntimeActionCall( - name="APPEND_SKILL", + name="LOAD_SKILL", payload="file_manager", ), RuntimeActionCall( - name="REMOVE_SKILL", + name="UNLOAD_SKILL", payload="image_prompt_generator", ), ], @@ -1961,21 +1891,21 @@ def test_replace_session_history_preserves_skill_marker_payloads(self): self.assertEqual( context.runtime_session_action_history[0]["text"], ( - "APPEND_SKILL: wildcards, " - "APPEND_SKILL: file_manager, " - "REMOVE_SKILL: image_prompt_generator" + "LOAD_SKILL: wildcards, " + "LOAD_SKILL: file_manager, " + "UNLOAD_SKILL: image_prompt_generator" ), ) - def test_stream_preserves_duplicate_failed_append_skill_marker(self): + def test_stream_preserves_duplicate_failed_load_skill_marker(self): class FakeBrainClient: async def stream(self, **_kwargs): yield { "type": "content", "content": ( - "\n" - "" + "\n" + "" ), } @@ -2033,14 +1963,14 @@ async def collect(context): ) self.assertIn( - "", + "", visible_text, ) self.assertEqual( context.runtime_action_events, [ { - "name": "append_skill", + "name": "load_skill", "runtime_turn_id": "turn-1", "payload": "name of skill", }, @@ -2048,7 +1978,7 @@ async def collect(context): ) self.assertEqual( context.runtime_asset_results[-1]["action"], - "append_skill", + "load_skill", ) self.assertEqual( context.runtime_asset_results[-1]["error"], @@ -2057,12 +1987,12 @@ async def collect(context): self.assertEqual( context.runtime_session_action_history[-1]["text"], ( - "APPEND_SKILL: name of skill " + "LOAD_SKILL: name of skill " "( does not exist )" ), ) self.assertIn( - "APPEND_SKILL: name of skill ( does not exist )", + "LOAD_SKILL: name of skill ( does not exist )", build_session_actions_history_context( context, current_sequence=True, @@ -2073,7 +2003,7 @@ async def collect(context): event for event in context.emitter.events if event.get("type") == "runtime_action" - and event.get("action") == "append_skill" + and event.get("action") == "load_skill" and event.get("status") == "counter_final" ] @@ -3242,58 +3172,42 @@ def test_prompt_routes_uncertain_operational_tasks_to_skills(self): assert_contains_text( self, prompt, - get_runtime_action_private_marker("LIST_SKILLS"), + "", ) - assert_not_contains_text( + assert_contains_text( self, prompt, - get_runtime_action_private_marker("APPEND_SKILL"), + get_runtime_action_private_marker("LOAD_SKILL"), ) - assert_not_contains_text( + assert_contains_text( self, prompt, - get_runtime_action_private_marker("REMOVE_SKILL"), + get_runtime_action_private_marker("UNLOAD_SKILL"), ) assert_not_contains_text( self, prompt, - "list_wildcards", + "", ) assert_not_contains_text( self, prompt, - "create_wildcard_file", + "list_wildcards", ) assert_not_contains_text( self, prompt, - "assets/wildcards", + "create_wildcard_file", ) - def test_prompt_shows_append_remove_rules_only_after_list_skills_result(self): + def test_prompt_always_shows_load_unload_rules_with_skill_inventory(self): context = SimpleNamespace( runtime_memory="session_status: active", runtime_memory_stable="session_status: active", runtime_l2_memory="", active_memory_records=[], - runtime_tool_results=[ - { - "kind": TOOL_RESULT_KIND_ASSET, - "result": { - "ok": True, - "action": "list_skills", - "skills": [ - { - "name": "wildcards", - "path": "assets/skills/wildcards.txt", - }, - ], - }, - }, - ], - runtime_asset_results=[], - runtime_appended_skills=[], + runtime_loaded_skills=[], ) prompt = build_brain_context( @@ -3303,52 +3217,44 @@ def test_prompt_shows_append_remove_rules_only_after_list_skills_result(self): }, ) - assert_not_contains_text( + assert_contains_text( self, prompt, - "LIST SKILLS:", + "", ) assert_contains_text( self, prompt, - "APPEND / REMOVE SKILLS:", + "LOAD / UNLOAD SKILLS:", ) assert_contains_text( self, prompt, - get_runtime_action_private_marker("APPEND_SKILL"), + get_runtime_action_private_marker("LOAD_SKILL"), ) assert_contains_text( self, prompt, - get_runtime_action_private_marker("REMOVE_SKILL"), + get_runtime_action_private_marker("UNLOAD_SKILL"), + ) + assert_not_contains_text( + self, + prompt, + "", ) - def test_prompt_shows_recorded_list_skills_tool_result(self): + def test_prompt_shows_always_visible_skill_inventory(self): - list_result = { - "ok": True, - "action": "list_skills", - "skills": [ - { - "name": "wildcards", - "path": "assets/skills/wildcards.txt", - }, - ], - } context = SimpleNamespace( runtime_memory="session_status: active", runtime_memory_stable="session_status: active", runtime_l2_memory="", active_memory_records=[], - runtime_asset_results=[], - runtime_tool_results=[ + runtime_loaded_skills=[ { - "kind": TOOL_RESULT_KIND_ASSET, - "result": list_result, + "name": "wildcards", }, ], - runtime_appended_skills=[], ) prompt = build_brain_context( @@ -3361,17 +3267,22 @@ def test_prompt_shows_recorded_list_skills_tool_result(self): assert_contains_text( self, prompt, - '', + "", ) assert_contains_text( self, prompt, - "1. wildcards - assets/skills/wildcards.txt", + "wildcards (loaded)", ) assert_contains_text( self, prompt, - "APPEND / REMOVE SKILLS:", + "", + ) + assert_not_contains_text( + self, + prompt, + '', ) def test_prompt_places_tool_results_at_context_top(self): @@ -3381,34 +3292,18 @@ def test_prompt_places_tool_results_at_context_top(self): runtime_memory_stable="session_status: active", runtime_l2_memory="", active_memory_records=[], - runtime_session_action_history=[ - "Listed skills", - ], runtime_asset_results=[ { "ok": True, - "action": "list_skills", - "skills": [ - { - "name": "wildcards", - "path": "assets/skills/wildcards.txt", - "content": "first line\nsecond line", - }, - ], - }, - { - "ok": True, - "action": "list_skills", - "skills": [ - { - "name": "wildcards", - "path": "assets/skills/wildcards.txt", - "content": "first line\nsecond line", - }, - ], + "action": "read_asset", + "path": "assets/test.txt", + "content": "asset result", }, ], - runtime_appended_skills=[ + runtime_session_action_history=[ + "Read asset", + ], + runtime_loaded_skills=[ { "name": "wildcards", "path": "assets/skills/wildcards.txt", @@ -3424,77 +3319,29 @@ def test_prompt_places_tool_results_at_context_top(self): "CAN_USE_ASSETS": True, }, ) - self.assertTrue( - prompt.startswith( - "" - ) - ) + + self.assertTrue(prompt.startswith("")) self.assertLess( - prompt.index(""), + prompt.index(''), + prompt.index(""), ) self.assertLess( - prompt.index(""), - prompt.index(""), + prompt.index(""), + prompt.index(""), ) self.assertLess( - prompt.index(""), - prompt.index("RUNTIME ACTION EXECUTION RULES:"), + prompt.index(""), + prompt.index(""), ) self.assertLess( - prompt.index("RUNTIME ACTION EXECUTION RULES:"), - prompt.index("I identify myself as JIN"), - ) - self.assertNotIn( - "first line\\nsecond line", - prompt, - ) - self.assertIn( - "first line\n", - prompt, - ) - self.assertIn( - "second line", - prompt, - ) - self.assertIn( - '', - prompt, - ) - self.assertEqual( - prompt.count( - '' - ), - 1, - ) - self.assertNotIn( - '', - prompt, - ) - self.assertNotIn( - '', - prompt, - ) - self.assertNotIn( - "All available skills:", - prompt, - ) - self.assertIn( - "1. wildcards (appended) - assets/skills/wildcards.txt", - prompt, - ) - self.assertNotIn( - '"action": "list_skills"', - prompt, - ) - self.assertNotIn( - '"skills":', - prompt, - ) - self.assertIn( - "", - prompt, + prompt.index(""), + prompt.index(""), ) + self.assertIn("asset result", prompt) + self.assertIn("first line\n", prompt) + self.assertIn("second line", prompt) + self.assertIn("wildcards (loaded)", prompt) + self.assertNotIn('', prompt) def test_prompt_keeps_loaded_delayed_memory_in_normal_turns(self): @@ -3658,12 +3505,12 @@ def test_prompt_formats_missing_skill_as_skill_error_tool_result(self): runtime_asset_results=[ { "ok": False, - "action": "append_skill", + "action": "load_skill", "requested": "file_writer", "error": "skill_not_found", }, ], - runtime_appended_skills=[], + runtime_loaded_skills=[], ) prompt = build_brain_context( @@ -3678,11 +3525,11 @@ def test_prompt_formats_missing_skill_as_skill_error_tool_result(self): prompt, ) self.assertIn( - "You attempted to append a skill that does not exist: file_writer", + "You attempted to load a skill that does not exist: file_writer", prompt, ) self.assertNotIn( - '"action": "append_skill"', + '"action": "load_skill"', prompt, ) self.assertNotIn( diff --git a/tests/test_l1_memory.py b/tests/test_l1_memory.py index c084f8e3..b8e04a0d 100644 --- a/tests/test_l1_memory.py +++ b/tests/test_l1_memory.py @@ -508,7 +508,7 @@ def test_guard_interrupted_assistant_message_includes_reason_quote(self): user_message="Use a skill.", assistant_message="Partial answer", interruption_reason="Repeated sentence loop detected.", - interruption_quote="Wait, I should use append_skill first.", + interruption_quote="Wait, I should use load_skill first.", ) self.assertIn( @@ -520,7 +520,7 @@ def test_guard_interrupted_assistant_message_includes_reason_quote(self): message, ) self.assertIn( - '"Wait, I should use append_skill first."', + '"Wait, I should use load_skill first."', message, ) self.assertNotIn( diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index c22fd200..172ed7a4 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -130,8 +130,8 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/runtime-memory.css?v=delayed-context-loaded-1", source) self.assertIn("/static/css/theme-win95.css?v=delayed-context-loaded-1", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-20", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=citation-duplicates-2", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=runtime-change-markers-24", source) self.assertIn("/static/js/think-citations.js?v=think-citations-6", source) self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 4765c413..8c3702a5 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -426,7 +426,7 @@ def test_cache_versions_are_bumped(self): source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1', + '/static/js/runtime/runtime-memory-view.js?v=citation-duplicates-2', source, ) self.assertIn( diff --git a/tests/test_memory_scheduler.py b/tests/test_memory_scheduler.py index 93014bc5..1e1032b3 100644 --- a/tests/test_memory_scheduler.py +++ b/tests/test_memory_scheduler.py @@ -258,13 +258,13 @@ async def test_guard_interrupted_update_uses_reason_and_quote(self): runtime_memory_updates=0, runtime_memory_pending_turns=[], runtime_memory_update_task=None, - runtime_turn_user_message="Use append_skill if needed.", + runtime_turn_user_message="Use load_skill if needed.", runtime_turn_assistant_response="Partial answer", runtime_turn_interruption_reason=( "Repeated sentence loop detected." ), runtime_turn_interruption_quote=( - "Wait, I'll check if I should use append_skill first." + "Wait, I'll check if I should use load_skill first." ), ) @@ -285,7 +285,7 @@ async def test_guard_interrupted_update_uses_reason_and_quote(self): user_prompt, ) self.assertIn( - "Wait, I'll check if I should use append_skill first.", + "Wait, I'll check if I should use load_skill first.", user_prompt, ) self.assertNotIn( diff --git a/tests/test_python_skill_assets.py b/tests/test_python_skill_assets.py index 11e4d10d..3ae75411 100644 --- a/tests/test_python_skill_assets.py +++ b/tests/test_python_skill_assets.py @@ -303,7 +303,7 @@ class Context: context_window=2048, ), } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -396,13 +396,13 @@ class Context: pass client = GuardedServiceClient( - context_window=2048, + context_window=4096, ) context = Context() context.clients = { "service": client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -418,7 +418,7 @@ class Context: + ("%D0%BF" * 10) + str(index) ) - for index in range(220) + for index in range(80) ), }, ] @@ -522,7 +522,7 @@ class Context: context.clients = { "service": client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -615,7 +615,7 @@ class Context: } context.emitter = Emitter() context.runtime_active_asset_action_id = "asset:test" - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -709,7 +709,7 @@ class Context: context.clients = { "service": client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -800,7 +800,7 @@ class Context: } context.emitter = Emitter() context.runtime_active_asset_action_id = "asset:test" - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -893,7 +893,7 @@ class Context: "service": service_client, "brain": brain_client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -1044,7 +1044,7 @@ class Context: context.clients = { "service": client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -1219,7 +1219,7 @@ class Context: context.clients = { "service": client, } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -1231,7 +1231,7 @@ class Context: "type": "text/plain", "text_content": " ".join( f"word-{index}" - for index in range(2000) + for index in range(600) ), }, ] @@ -1258,7 +1258,7 @@ class Context: ) self.assertEqual( result["total_words"], - 2000, + 600, ) self.assertGreater( result["chunks"], @@ -1291,7 +1291,7 @@ class Context: context_window=2048, ), } - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "chunk_reader", }, @@ -1390,7 +1390,7 @@ class Context: encoding="utf-8", ) context = Context() - context.runtime_appended_skills = [ + context.runtime_loaded_skills = [ { "name": "echo_skill", }, diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 2ddc5e04..4031abde 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -182,7 +182,7 @@ def test_avatar_core_button_toggles_memory_layers_without_refresh(self): self.assertIn('class: "jin-avatar-scaffold"', avatar_source) self.assertIn('class: "jin-avatar-center-ring"', avatar_source) - click_start = input_source.index("if (factCheckTrigger) {") + click_start = input_source.index("if (memoryLayersToggle) {") click_end = input_source.index("chatForm.addEventListener", click_start) click_block = input_source[click_start:click_end] @@ -196,7 +196,7 @@ def test_avatar_cache_versions_are_bumped(self): source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-20", + "/static/js/runtime/runtime-avatar.js?v=runtime-change-markers-24", source, ) self.assertIn( diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index f4d5a287..375b4c4c 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -1465,7 +1465,7 @@ async def asset_action_generator(): runtime_usage_events=[], runtime_asset_results=[], runtime_session_action_history=[], - runtime_appended_skills=[ + runtime_loaded_skills=[ { "name": "chunk_reader", }, diff --git a/tests/test_runtime_todo.py b/tests/test_runtime_todo.py index 6e20e3dd..d5642fdf 100644 --- a/tests/test_runtime_todo.py +++ b/tests/test_runtime_todo.py @@ -36,10 +36,10 @@ def test_extract_create_todo_block_with_next_action(self): enabled_actions = self.enabled_actions_with_runtime_todo() result = extract_runtime_actions( "\n" - "1. LIST_SKILLS\n" - "2. APPEND_SKILL wildcards\n" + "1. LOAD_SKILL wildcards\n" + "2. Use the loaded skill\n" "\n" - "", + "", enabled_actions=enabled_actions, ) @@ -47,8 +47,8 @@ def test_extract_create_todo_block_with_next_action(self): self.assertEqual( [(action.name, action.payload) for action in result.actions], [ - ("CREATE_TODO_LIST", "1. LIST_SKILLS\n2. APPEND_SKILL wildcards"), - ("LIST_SKILLS", ""), + ("CREATE_TODO_LIST", "1. LOAD_SKILL wildcards\n2. Use the loaded skill"), + ("LOAD_SKILL", "wildcards"), ], ) diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index 785c0394..d878b81f 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -69,7 +69,7 @@ def test_stream_validator_allows_repeated_sentences_when_sentence_check_disabled repeated = ( "* Wait, I'll check if I should use " - "`append_skill` first.\n" + "`load_skill` first.\n" ) for _ in range(3): diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 826d2d4b..9de25ee3 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -81,7 +81,7 @@ body.theme-win95 #memory-drag-handle { } body.theme-win95 #console-title, -body.theme-win95 #fact-check-trigger { +body.theme-win95 #memory-layers-toggle { color: var(--win95-title-text); font-family: Calibri, Arial, sans-serif; font-size: 14px!important; @@ -366,7 +366,7 @@ body.theme-win95 #settings-panel button { font-weight: 500; } -body.theme-win95 #settings-panel #fact-check-trigger { +body.theme-win95 #settings-panel #memory-layers-toggle { color: var(--win95-title-text) !important; text-shadow: 1px 1px 0 var(--win95-blue-shadow); } diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 34a58c89..88e7d66d 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -1301,7 +1301,7 @@ memoryDragHandle.addEventListener("dblclick", (event) => { memoryDragHandle, { ignoredTarget: - document.getElementById("fact-check-trigger"), + document.getElementById("memory-layers-toggle"), } ); }); diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 8a7e8d6b..cfceef1e 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -81,7 +81,7 @@ const L4_MEMORY_RING_COLOR = "#93c5fd"; const avatarRoot = document.getElementById("jin-runtime-avatar"); - const factCheckTrigger = document.getElementById("fact-check-trigger"); + const memoryLayersToggle = document.getElementById("memory-layers-toggle"); const settingsPanel = document.getElementById("settings-panel"); const normalizeRuntimeCitationIdentity = window.JinRuntime.normalizeCitationIdentity; @@ -2882,7 +2882,7 @@ } function syncMemoryLayersToggleLabel(hidden) { - if (!factCheckTrigger) { + if (!memoryLayersToggle) { return; } @@ -2895,10 +2895,10 @@ ? "show" : "hide"; - factCheckTrigger.setAttribute("title", label); - factCheckTrigger.setAttribute("aria-label", label); - factCheckTrigger.setAttribute("alt", label); - factCheckTrigger.dataset.memoryLayersHidden = + memoryLayersToggle.setAttribute("title", label); + memoryLayersToggle.setAttribute("aria-label", label); + memoryLayersToggle.setAttribute("alt", label); + memoryLayersToggle.dataset.memoryLayersHidden = memoryLayersHidden ? "true" : "false"; } @@ -3138,10 +3138,10 @@ }); } - if (factCheckTrigger) { + if (memoryLayersToggle) { syncMemoryLayersToggleLabel(); - factCheckTrigger.addEventListener("mousedown", (event) => { + memoryLayersToggle.addEventListener("mousedown", (event) => { event.stopPropagation(); }); diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 9dafe126..138afdd9 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -12,9 +12,9 @@ const sendButton = 'button[type="submit"]' ); -const factCheckTrigger = +const memoryLayersToggle = document.getElementById( - "fact-check-trigger" + "memory-layers-toggle" ); const websocketClientId = diff --git a/ui/static/js/socket/input.js b/ui/static/js/socket/input.js index b51edab2..cd147ca8 100644 --- a/ui/static/js/socket/input.js +++ b/ui/static/js/socket/input.js @@ -154,7 +154,7 @@ function allModelRuntimesOffline() { } -if (factCheckTrigger) { +if (memoryLayersToggle) { function toggleRuntimeAvatarMemoryLayers() { const avatar = window.JinRuntime @@ -168,7 +168,7 @@ if (factCheckTrigger) { } } - factCheckTrigger.addEventListener( + memoryLayersToggle.addEventListener( "click", (event) => { event.preventDefault(); diff --git a/ui/templates/index.html b/ui/templates/index.html index 32d08e69..9a0bd143 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -316,7 +316,7 @@
      - `; - - if (window.bindJinAttachmentBubble) { - window.bindJinAttachmentBubble( - item, - attachment - ); - } - - attachedFiles.appendChild(item); - }); - - attachedFiles.querySelectorAll("button").forEach((btn) => { - btn.addEventListener("click", (event) => { - event.preventDefault(); - event.stopPropagation(); - - const index = Number(btn.dataset.index); - const file = - droppedFiles[index]; - - droppedFiles.splice(index, 1); - releaseFileObjectUrl(file); +function bindAttachedFilesPlaquePinPreview(element, record) { + if (!element || !record) return; + const attachment = attachmentViewRecord(record); - syncFileInput(); - renderFiles(); + const bind = () => { + if (element.dataset.jinAttachmentHoverBound === "1") return true; + if (typeof window.bindJinAttachmentHoverPreview !== "function") return false; + window.bindJinAttachmentHoverPreview(element, attachment, { + hoverPreviewMaxPx: 100, }); - }); -} + element.dataset.jinAttachmentHoverBound = "1"; + return true; + }; -function syncFileInput() { - if (!fileInput) { - return; + if (!bind()) { + window.addEventListener("jin:attachment-ui-ready", bind, {once: true}); } - - const dt = new DataTransfer(); - - droppedFiles.forEach((file) => { - dt.items.add(file); - }); - - fileInput.files = dt.files; } -function addFiles(fileList) { - for (const file of Array.from(fileList || [])) { - droppedFiles.push(file); - } - - syncFileInput(); - renderFiles(); -} - -function clearFiles() { - droppedFiles.forEach((file) => { - releaseFileObjectUrl(file); +function renderAttachedFilesPlaque() { + if (!attachedFiles) return; + const records = pinnedIds.map(getFileRecord).filter(Boolean); + attachedFiles.replaceChildren(); + attachedFiles.classList.toggle("hidden", records.length === 0); + if (!records.length) return; + + const title = document.createElement("div"); + title.className = "jin-attached-files-title"; + title.textContent = "[ ATTACHED_FILES ]"; + attachedFiles.appendChild(title); + + const list = document.createElement("div"); + list.className = "jin-attached-files-list"; + records.forEach((record) => { + const row = document.createElement("div"); + row.className = "jin-attached-files-row runtime-memory-delayed-row-pinned"; + const pin = document.createElement("button"); + pin.type = "button"; + pin.className = "delayed-memory-modal-icon-button delayed-memory-modal-pin runtime-memory-delayed-pin is-pinned"; + pin.innerHTML = pinSvg(); + pin.title = String(record.id || ""); + pin.setAttribute("aria-label", `Remove ${record.id || "file"} from JIN context`); + pin.dataset.fileId = String(record.id || ""); + bindAttachedFilesPlaquePinPreview(pin, record); + pin.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + void setPinned(record.id, false); + }); + const name = document.createElement("span"); + name.className = "jin-attached-files-name"; + name.textContent = record.name || "attachment"; + name.title = `${record.name || "attachment"} · ${formatBytes(record.size_bytes)}`; + bindAttachedFilesPlaqueName(name, record); + row.append(pin, name); + list.appendChild(row); }); - - droppedFiles = []; - - syncFileInput(); - renderFiles(); + attachedFiles.appendChild(list); } -function readImageDimensions(file) { - return new Promise((resolve) => { - const image = new Image(); - const url = URL.createObjectURL(file); - - image.onload = () => { - URL.revokeObjectURL(url); - resolve({ - width: image.naturalWidth || null, - height: image.naturalHeight || null, - }); - }; - - image.onerror = () => { - URL.revokeObjectURL(url); - resolve({ - width: null, - height: null, - }); - }; - - image.src = url; - }); +function hasDraggedFiles(event) { + return Array.from(event && event.dataTransfer && event.dataTransfer.types || []).includes("Files"); } -function readFileAsDataUrl(file) { - return new Promise((resolve) => { - const reader = new FileReader(); - - reader.onload = () => { - resolve( - typeof reader.result === "string" - ? reader.result - : "" - ); - }; - - reader.onerror = () => { - resolve(""); - }; - - reader.readAsDataURL(file); - }); +function ensureDropOverlay() { + if (dropOverlay || !chatColumn) return dropOverlay; + dropOverlay = document.createElement("div"); + dropOverlay.id = "chat-drop-overlay"; + dropOverlay.className = "pointer-events-none absolute inset-3 z-30 hidden items-center justify-center rounded-lg border border-sky-400/60 bg-sky-950/45 text-sky-100 shadow-[0_0_36px_rgba(56,189,248,0.18)] backdrop-blur-sm"; + dropOverlay.innerHTML = '
      drop files
      copy to JIN files + attach
      '; + chatColumn.appendChild(dropOverlay); + return dropOverlay; } -async function buildAttachmentPayload(file, index) { - const type = file.type || "application/octet-stream"; - const kind = - getFileKind(file); - const attachment = { - id: `attachment-${Date.now()}-${index}`, - name: file.name || `attachment-${index + 1}`, - type, - size_bytes: file.size || 0, - size_label: formatBytes(file.size), - last_modified: file.lastModified - ? new Date(file.lastModified).toISOString() - : null, - kind, - }; - - if (attachment.kind === "image") { - const dimensions = await readImageDimensions(file); - const dataUrl = await readFileAsDataUrl(file); - - attachment.width = dimensions.width; - attachment.height = dimensions.height; - - if (dataUrl) { - attachment.data_url = dataUrl; - attachment.data_url_bytes = dataUrl.length; - } - } - - if ( - attachment.kind === "binary" - && BINARY_DOCUMENT_EXTENSIONS.has( - getFileExtension(file) - ) - ) { - const dataUrl = await readFileAsDataUrl(file); - - if (dataUrl) { - attachment.data_url = dataUrl; - attachment.data_url_bytes = dataUrl.length; - } - } - - if (attachment.kind === "text") { - const previewBlob = file.slice(0, TEXT_PREVIEW_LIMIT * 4); - const rawPreview = await readBlobAsText( - previewBlob - ); - const preview = rawPreview.slice(0, TEXT_PREVIEW_LIMIT); - const textContent = - await readBlobAsText(file); - - attachment.preview_chars = preview.length; - attachment.preview_limit = TEXT_PREVIEW_LIMIT; - attachment.truncated = - rawPreview.length > TEXT_PREVIEW_LIMIT - || file.size > previewBlob.size; - attachment.text_preview = preview; - attachment.text_content = textContent; - } +function showDropOverlay() { + const overlay = ensureDropOverlay(); + if (!overlay) return; + overlay.classList.remove("hidden"); + overlay.classList.add("flex"); + chatColumn.classList.add("jin-drop-zone-active"); +} - return attachment; +function hideDropOverlay() { + const overlay = ensureDropOverlay(); + if (!overlay) return; + overlay.classList.add("hidden"); + overlay.classList.remove("flex"); + chatColumn.classList.remove("jin-drop-zone-active"); } async function prepareJinAttachments() { - if (!droppedFiles.length) { - return []; - } - - return Promise.all( - droppedFiles.map((file, index) => { - return buildAttachmentPayload(file, index); - }) - ); + await uploadQueue; + return pinnedIds + .map(getFileRecord) + .filter(Boolean) + .slice(0, MAX_JIN_ATTACHMENTS) + .map(attachmentViewRecord); } -if ( - chatColumn - && fileInput - && attachedFiles -) { +if (chatColumn && fileInput) { ensureDropOverlay(); - ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { document.addEventListener(eventName, (event) => { - if (!hasDraggedFiles(event)) { - return; - } - + if (!hasDraggedFiles(event)) return; event.preventDefault(); event.stopPropagation(); }); }); - document.addEventListener("dragenter", (event) => { - if (!hasDraggedFiles(event)) { - return; - } - + if (!hasDraggedFiles(event)) return; dragDepth += 1; showDropOverlay(); }); - document.addEventListener("dragover", (event) => { - if (!hasDraggedFiles(event)) { - return; - } - - showDropOverlay(); + if (hasDraggedFiles(event)) showDropOverlay(); }); - document.addEventListener("dragleave", (event) => { - if (!hasDraggedFiles(event)) { - return; - } - + if (!hasDraggedFiles(event)) return; dragDepth = Math.max(0, dragDepth - 1); - - if (dragDepth === 0) { - hideDropOverlay(); - } + if (!dragDepth) hideDropOverlay(); }); - document.addEventListener("drop", (event) => { - if (!hasDraggedFiles(event)) { - return; - } - + if (!hasDraggedFiles(event)) return; dragDepth = 0; hideDropOverlay(); - - const files = - event.dataTransfer - && event.dataTransfer.files; - - if (!files || !files.length) { - return; - } - - addFiles(files); + addFiles(event.dataTransfer && event.dataTransfer.files); }); - document.addEventListener("paste", (event) => { - const files = Array.from( - event.clipboardData - && event.clipboardData.files - ? event.clipboardData.files - : [] - ); - - if (!files.length) { - return; - } - - addFiles(files); - }); - - fileInput.addEventListener("change", (event) => { - addFiles(event.target.files); + const files = Array.from(event.clipboardData && event.clipboardData.files || []); + if (files.length) addFiles(files); }); + fileInput.addEventListener("change", (event) => addFiles(event.target.files)); } -window.prepareJinAttachments = - prepareJinAttachments; - -window.clearJinAttachments = - clearFiles; - -window.hasJinAttachments = - function () { - return droppedFiles.length > 0; - }; +window.prepareJinAttachments = prepareJinAttachments; +window.clearJinAttachments = function () { + Promise.all(pinnedIds.map((id) => setPinned(id, false))); +}; +window.hasJinAttachments = () => pinnedIds.length > 0; +window.JinFiles = { + refresh: refreshFiles, + syncContext: syncAttachmentContext, + getFiles: () => fileStore.map((item) => ({...item})), + getPinnedIds: () => [...pinnedIds], + getFile: (id) => { + const record = getFileRecord(id); + return record ? {...record} : null; + }, + setPinned, + deleteFile, + resolveAttachment: resolvePersistentAttachment, + applySnapshot(payload) { + normalizeSnapshot(payload || {}); + dispatchStoreChanged(); + }, +}; + +void refreshFiles(); diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index a2c4c16d..e6ce80c8 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -12,6 +12,8 @@ let traceModalL1StreamFrame = null; const CONTEXT_DELAYED_MEMORY_STORE_CHANGED_EVENT = "jin:delayed-memory-store-changed"; +const CONTEXT_FILES_STORE_CHANGED_EVENT = + "jin:files-store-changed"; function ensureTraceModal() { if (traceModal) { @@ -316,6 +318,13 @@ function ensureTraceModal() { ); } ); + + window.addEventListener( + CONTEXT_FILES_STORE_CHANGED_EVENT, + function () { + syncContextAttachedFileRows(); + } + ); } @@ -1554,7 +1563,8 @@ function syncContextDelayedMemoryRow(row) { pinButton.disabled = true; pinButton.classList.remove( "delayed-memory-modal-pin-active", - "delayed-memory-modal-pin-appended" + "delayed-memory-modal-pin-appended", + "delayed-memory-modal-pin-loaded" ); pinButton.title = "Delayed memory report deleted"; @@ -1586,6 +1596,11 @@ function syncContextDelayedMemoryRow(row) { && runtime && typeof runtime.isDelayedMemoryReportAppended === "function" && runtime.isDelayedMemoryReportAppended(reportId); + const loaded = + !pinned + && runtime + && typeof runtime.isDelayedMemoryReportLoaded === "function" + && runtime.isDelayedMemoryReportLoaded(reportId); pinButton.disabled = false; row.classList.toggle( @@ -1600,14 +1615,18 @@ function syncContextDelayedMemoryRow(row) { "delayed-memory-modal-pin-appended", Boolean(appended) ); + pinButton.classList.toggle( + "delayed-memory-modal-pin-loaded", + Boolean(loaded) + ); pinButton.setAttribute( "aria-pressed", pinned ? "true" : "false" ); pinButton.setAttribute( "aria-label", - appended - ? "Remove appended delayed memory" + loaded + ? "Unload delayed memory" : ( pinned ? "Unpin delayed memory" @@ -1618,9 +1637,13 @@ function syncContextDelayedMemoryRow(row) { appended ? "Remove appended delayed memory from next turn" : ( - pinned - ? "Unpin delayed memory" - : "Pin delayed memory" + loaded + ? "Unload delayed memory from context" + : ( + pinned + ? "Unpin delayed memory" + : "Pin delayed memory" + ) ); } @@ -1837,6 +1860,94 @@ function renderContextDelayedMemoryBody( parent.appendChild(list); } +function parseContextAttachedFileLine(line) { + const text = String(line || "").trim(); + const match = text.match(/^(.*?)\s*\[\s*id\s*:\s*([a-z0-9]{6})\s*\]\s*$/i); + if (!match) return null; + return { + name: String(match[1] || "").trim(), + id: String(match[2] || "").toLowerCase(), + }; +} + +function syncContextAttachedFileRows() { + if (!traceModalContent) return; + traceModalContent.querySelectorAll(".jin-context-attached-file-row").forEach((row) => { + const fileId = String(row.dataset.attachedFileId || "").toLowerCase(); + const record = window.JinFiles && typeof window.JinFiles.getFile === "function" + ? window.JinFiles.getFile(fileId) + : null; + const pin = row.querySelector(".jin-context-attached-file-pin"); + const pinned = Boolean(record && record.pinned); + row.classList.toggle("is-pinned", pinned); + row.classList.toggle("opacity-50", !record); + if (pin) { + pin.disabled = !record; + pin.classList.toggle("delayed-memory-modal-pin-active", pinned); + pin.setAttribute("aria-pressed", pinned ? "true" : "false"); + pin.title = pinned ? "Remove file from JIN context" : "Attach file to JIN context"; + } + }); +} + +function renderContextAttachedFilesBody(parent, content) { + const records = String(content || "") + .split("\n") + .map(parseContextAttachedFileLine) + .filter(Boolean); + + if (!records.length) return; + + const list = contextElement("div", "jin-context-delayed-list"); + records.forEach((item) => { + const row = contextElement("div", "jin-context-delayed-row jin-context-attached-file-row"); + row.dataset.attachedFileId = item.id; + row.setAttribute("role", "button"); + row.setAttribute("tabindex", "0"); + + const pin = contextElement( + "button", + "delayed-memory-modal-icon-button delayed-memory-modal-pin jin-context-delayed-pin jin-context-attached-file-pin" + ); + pin.type = "button"; + pin.innerHTML = ''; + const label = contextElement( + "span", + "jin-context-delayed-label", + `${item.name} [ id: ${item.id} ]` + ); + + pin.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + const record = window.JinFiles && window.JinFiles.getFile(item.id); + if (!record || !window.JinFiles) return; + void window.JinFiles.setPinned(item.id, !Boolean(record.pinned)); + }); + + const open = () => { + const record = window.JinFiles && window.JinFiles.getFile(item.id); + if (record && typeof window.openJinAttachmentModal === "function") { + window.openJinAttachmentModal(record); + } + }; + row.addEventListener("click", (event) => { + if (event.target && event.target.closest(".jin-context-attached-file-pin")) return; + open(); + }); + row.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + open(); + }); + + row.append(pin, label); + list.appendChild(row); + }); + parent.appendChild(list); + syncContextAttachedFileRows(); +} + function renderContextBody(parent, content) { const text = String(content || "").trim(); @@ -2035,15 +2146,20 @@ function appendContextCard( header.appendChild(meta); card.appendChild(header); card.appendChild(body); - if ( - String(block.title || "") - .trim() - .toUpperCase() === "DELAYED_MEMORY" - ) { + const normalizedBlockTitle = String(block.title || "") + .trim() + .toUpperCase(); + + if (normalizedBlockTitle === "DELAYED_MEMORY") { renderContextDelayedMemoryBody( body, block.content ); + } else if (normalizedBlockTitle === "ATTACHED_FILES") { + renderContextAttachedFilesBody( + body, + block.content + ); } else { renderContextBody( body, diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 8edb1094..c55c5bd3 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -123,6 +123,7 @@ let pendingRuntimeMemoryRender = false; let memoryHighlightsSuspended = false; let memoryPanelVisibilityEventsBound = false; + let filesStoreEventsBound = false; function requireRuntimeMemoryHistory() { if (!runtimeMemoryHistory) { @@ -1234,6 +1235,28 @@ }); } + function getPersistentFileRecords() { + if (!window.JinFiles || typeof window.JinFiles.getFiles !== "function") { + return []; + } + + return window.JinFiles.getFiles() + .filter((record) => record && record.id && record.name) + .sort((left, right) => { + const pinDifference = Number(Boolean(right.pinned)) - Number(Boolean(left.pinned)); + if (pinDifference) return pinDifference; + if (left.pinned && right.pinned) { + const pinTimeDifference = Number(right.pinned_at || 0) - Number(left.pinned_at || 0); + if (pinTimeDifference) return pinTimeDifference; + } + const createdDifference = Number(right.created_at || 0) - Number(left.created_at || 0); + if (createdDifference) return createdDifference; + const idDifference = String(left.id || "").localeCompare(String(right.id || "")); + if (idDifference) return idDifference; + return String(left.name || "").localeCompare(String(right.name || "")); + }); + } + function getAvailableRuntimeMemoryDisplayModes() { const modes = [ "runtime", @@ -1263,6 +1286,12 @@ ); } + if (getPersistentFileRecords().length > 0) { + modes.push( + "files" + ); + } + return modes; } @@ -1309,7 +1338,9 @@ ? "[ facts memory ]" : displayMode === "long_term" ? "[ long term memory ]" - : "[ runtime memory ]"; + : displayMode === "files" + ? "[ files ]" + : "[ runtime memory ]"; const hasAlternativeMemory = modes.length > 1; @@ -1804,6 +1835,11 @@ return; } + if (getRuntimeMemoryDisplayMode() === "files") { + renderPersistentFiles(); + return; + } + const sourceSnapshot = runtimeMemoryHistory.snapshots[ runtimeMemoryHistory.index @@ -3019,6 +3055,99 @@ } + function bindPersistentFileHoverPreview(element, record) { + if (!element || !record) return; + + const bind = () => { + if (element.dataset.jinAttachmentHoverBound === "1") return true; + if (typeof window.bindJinAttachmentHoverPreview !== "function") return false; + + window.bindJinAttachmentHoverPreview(element, record, { + hoverPreviewMaxPx: 100, + }); + element.dataset.jinAttachmentHoverBound = "1"; + return true; + }; + + if (!bind()) { + window.addEventListener("jin:attachment-ui-ready", bind, {once: true}); + } + } + + function renderPersistentFiles() { + if (!runtimeMemoryText) return; + + const records = getPersistentFileRecords(); + + runtimeMemoryText.innerHTML = ""; + runtimeMemoryText.classList.remove("runtime-memory-text-pinned"); + runtimeMemoryText.removeAttribute("title"); + + if (runtimeMemoryPosition) { + runtimeMemoryPosition.textContent = String(records.length); + } + + records.forEach((record, index) => { + const row = document.createElement("div"); + row.className = "runtime-memory-line runtime-memory-delayed-row"; + row.dataset.memoryHighlightSortIndex = String(index); + row.setAttribute("role", "button"); + row.setAttribute("tabindex", "0"); + row.title = String(record.name || "attachment"); + if (record.pinned) { + row.classList.add("runtime-memory-delayed-row-pinned"); + } + + const pinButton = document.createElement("button"); + pinButton.type = "button"; + pinButton.className = "delayed-memory-modal-icon-button delayed-memory-modal-pin runtime-memory-delayed-pin"; + pinButton.innerHTML = ''; + pinButton.classList.toggle("delayed-memory-modal-pin-active", Boolean(record.pinned)); + pinButton.setAttribute("aria-pressed", record.pinned ? "true" : "false"); + pinButton.title = String(record.id || ""); + pinButton.setAttribute( + "aria-label", + record.pinned + ? `Remove ${record.id || "file"} from JIN context` + : `Attach ${record.id || "file"} to JIN context` + ); + pinButton.dataset.fileId = String(record.id || ""); + bindPersistentFileHoverPreview(pinButton, record); + pinButton.addEventListener("pointerdown", (event) => event.stopPropagation()); + pinButton.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + if (window.JinFiles && typeof window.JinFiles.setPinned === "function") { + void window.JinFiles.setPinned(record.id, !Boolean(record.pinned)); + } + }); + + const separator = document.createElement("span"); + separator.className = "runtime-memory-delayed-separator"; + separator.textContent = "·"; + + const keySpan = document.createElement("span"); + keySpan.className = "runtime-memory-key"; + keySpan.textContent = String(record.name || "attachment"); + bindPersistentFileHoverPreview(keySpan, record); + + row.append(pinButton, separator, keySpan); + + const openModal = () => { + if (typeof window.openJinAttachmentModal === "function") { + window.openJinAttachmentModal(record); + } + }; + row.addEventListener("click", openModal); + row.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + openModal(); + }); + runtimeMemoryText.appendChild(row); + }); + } + function renderDelayedMemoryReports() { const reports = getDelayedMemoryReportRecords(); @@ -4127,6 +4256,9 @@ const appended = !pinned && isDelayedMemoryReportAutoAppended(report); + const loaded = + !pinned + && isDelayedMemoryReportInContext(report); button.classList.toggle( "delayed-memory-modal-pin-active", @@ -4136,14 +4268,18 @@ "delayed-memory-modal-pin-appended", appended ); + button.classList.toggle( + "delayed-memory-modal-pin-loaded", + loaded + ); button.setAttribute( "aria-pressed", pinned ? "true" : "false" ); button.setAttribute( "aria-label", - appended - ? "Remove appended delayed memory" + loaded + ? "Unload delayed memory" : ( pinned ? "Unpin delayed memory" @@ -4154,9 +4290,13 @@ appended ? "Remove appended delayed memory from next turn" : ( - pinned - ? "Unpin delayed memory" - : "Pin delayed memory" + loaded + ? "Unload delayed memory from context" + : ( + pinned + ? "Unpin delayed memory" + : "Pin delayed memory" + ) ); } @@ -5685,6 +5825,14 @@ }, }); + if (!filesStoreEventsBound) { + window.addEventListener("jin:files-store-changed", () => { + ensureRuntimeMemoryDisplayModeAvailable(); + renderRuntimeMemorySnapshot(); + }); + filesStoreEventsBound = true; + } + bindRuntimeMemoryNavigation(); renderRuntimeMemorySnapshot(); renderRuntimeDiffs(); diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 45790c28..e06cab36 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -831,7 +831,8 @@ function isDelayedMemoryReportAppended(reportId) { function markDelayedMemoryReportLoaded( reportId, - loaded = true + loaded = true, + options = {} ) { const normalizedId = normalizeRuntimeDelayedMemoryReportId(reportId); @@ -858,11 +859,16 @@ function markDelayedMemoryReportLoaded( ); } - if ( + const stateUnchanged = ( wasLoaded === loadedDelayedMemoryReportIds.has(normalizedId) && wasAppended === appendedDelayedMemoryReportIds.has(normalizedId) + ); + + if ( + stateUnchanged + && options.forceRender !== true ) { return true; } @@ -883,6 +889,15 @@ function markDelayedMemoryReportLoaded( ); } + if (options.sync === true) { + syncDelayedMemoryReportsToServer({ + suppressedAppendIds: + options.suppressNextTurn === true + ? [normalizedId] + : [], + }); + } + return true; } @@ -962,25 +977,50 @@ function handleDelayedMemoryReportPinClick(reportId) { return false; } - if (isDelayedMemoryReportAppended(normalizedId)) { - markDelayedMemoryReportAppended( - normalizedId, - false, - { - unload: true, - sync: true, - suppressNextTurn: true, - } - ); + const pinned = Boolean(report.pinned); + const appended = + isDelayedMemoryReportAppended(normalizedId); + const loaded = + isDelayedMemoryReportLoaded(normalizedId); + + if (!pinned && loaded) { + if (appended) { + markDelayedMemoryReportAppended( + normalizedId, + false, + { + unload: true, + sync: true, + suppressNextTurn: true, + } + ); + } else { + markDelayedMemoryReportLoaded( + normalizedId, + false, + { + sync: true, + suppressNextTurn: true, + } + ); + } + + if (appended) { + return { + action: "unappend", + pinned: false, + reportId: normalizedId, + }; + } return { - action: "unappend", - pinned: Boolean(report.pinned), + action: "unload", + pinned: false, reportId: normalizedId, }; } - const nextPinned = !Boolean(report.pinned); + const nextPinned = !pinned; const changed = setDelayedMemoryReportPinned( normalizedId, nextPinned diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 138afdd9..39ad900c 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -67,6 +67,15 @@ function registerSocketMessageHandler( window.registerSocketMessageHandler = registerSocketMessageHandler; +registerSocketMessageHandler( + "attached_files_update", + function (data) { + if (window.JinFiles && typeof window.JinFiles.applySnapshot === "function") { + window.JinFiles.applySnapshot(data || {}); + } + } +); + function buildWebSocketUrl() { const params = @@ -480,6 +489,10 @@ async function handleSocketOpen() { "WebSocket connected." ); + if (window.JinFiles && typeof window.JinFiles.syncContext === "function") { + window.JinFiles.syncContext(); + } + if (isSoftReconnect) { if (window.getSoftReconnectRuntimeResume) { const runtimeResume = diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 15680550..04cd9ac4 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -1008,6 +1008,9 @@ function handleRuntimeAction( ].includes( status ); + const liveActiveMemoryProgress = + action === "save_active_memory" + && status === "running"; if ( counterOnly @@ -1354,7 +1357,8 @@ function handleRuntimeAction( ) { window.JinRuntime.runtime.markDelayedMemoryReportLoaded( delayedMemoryPreview.reportId, - true + true, + { forceRender: true } ); } @@ -1411,7 +1415,8 @@ function handleRuntimeAction( displayCounterOnly, markerCount: displayMarkerCount, - reuseCompleted: false, + reuseCompleted: + action === "update_l4_facts", contextSnapshot: data.context || null, assetResult: @@ -1538,6 +1543,8 @@ function handleRuntimeAction( closeTag, pendingUntilL3, forceCompletePendingL3, + flushStreamFrame: + !liveActiveMemoryProgress, } ); diff --git a/ui/templates/index.html b/ui/templates/index.html index 8b3b509b..04e89d7f 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,11 +10,11 @@ - + - + @@ -129,6 +129,8 @@ class="flex-1 overflow-y-auto p-4 space-y-4 text-[11px] leading-relaxed text-zinc-400"> + + @@ -244,12 +246,6 @@ - - -
      -
      - - + - + - + @@ -497,18 +497,18 @@ } | tojson }} - + - - - + + + - + @@ -519,11 +519,11 @@ - + - + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index 59862005..6358304e 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -63,6 +63,7 @@ from .save_delayed_memory_utils import ( collect_anchor_fact_report_ids, collect_long_term_fact_ids_from_reports, + normalize_delayed_memory_attachment_ids, normalize_delayed_memory_fact_ids, normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, @@ -112,6 +113,7 @@ "normalize_runtime_action_names", "collect_anchor_fact_report_ids", "collect_long_term_fact_ids_from_reports", + "normalize_delayed_memory_attachment_ids", "normalize_delayed_memory_fact_ids", "normalize_delayed_memory_fact_roles", "normalize_long_term_fact_ids", diff --git a/utils/actions/action_counter_utils.py b/utils/actions/action_counter_utils.py index 5414108c..fb655e14 100644 --- a/utils/actions/action_counter_utils.py +++ b/utils/actions/action_counter_utils.py @@ -214,7 +214,7 @@ def marker_actions( ), ) - normalized_payloads = normalize_runtime_action_counter_payloads( + normalized_payloads = resolve_runtime_action_counter_display_payloads( entry, payloads, ) @@ -244,6 +244,43 @@ def marker_actions( return marker_actions +def resolve_runtime_action_counter_display_payloads( + entry: RuntimeActionCount, + payloads, +) -> list[str]: + + normalized_payloads = normalize_runtime_action_counter_payloads( + entry, + payloads, + ) + + if entry.name not in { + "ATTACH_FILE", + "DETACH_FILE", + }: + return normalized_payloads + + from utils.attached_files_store import get_file_record + + display_payloads = [] + + for payload in normalized_payloads: + record = get_file_record( + payload + ) + display_payloads.append( + str( + (record or {}).get( + "name", + "", + ) + or payload + ).strip() + ) + + return display_payloads + + def normalize_runtime_action_counter_payloads( entry: RuntimeActionCount, payloads, @@ -392,7 +429,7 @@ async def emit_runtime_action_counter_updates( ), ) - normalized_payloads = normalize_runtime_action_counter_payloads( + normalized_payloads = resolve_runtime_action_counter_display_payloads( entry, payloads, ) diff --git a/utils/actions/attachment_actions.py b/utils/actions/attachment_actions.py index 73bbd9de..3396e5bf 100644 --- a/utils/actions/attachment_actions.py +++ b/utils/actions/attachment_actions.py @@ -1,7 +1,7 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_LIST_FILES, - RUNTIME_ACTION_LOAD_ATTACHMENT, - RUNTIME_ACTION_UNLOAD_ATTACHMENT, + RUNTIME_ACTION_ATTACH_FILE, + RUNTIME_ACTION_DETACH_FILE, get_runtime_action_display_name, runtime_action_has_close_tag, ) @@ -73,8 +73,8 @@ async def apply_attachment_actions( context, *, list_actions, - load_actions, - unload_actions, + attach_actions, + detach_actions, log_runtime=None, with_action_context=lambda payload: payload, ) -> list[dict]: @@ -96,12 +96,12 @@ async def apply_attachment_actions( if log_runtime is not None: await log_runtime(f"[RUNTIME ACTION] list_files ({len(records)} files)") - for action in unload_actions: + for action in detach_actions: file_id = _clean_id(action.payload) record = get_file_record(file_id) if not file_id or record is None: results.append({ - "action": "unload_attachment", + "action": "detach_file", "ok": False, "id": file_id or str(action.payload or "").strip(), "error": "file_not_found", @@ -111,19 +111,19 @@ async def apply_attachment_actions( active_ids = [value for value in active_ids if value != file_id] set_file_pinned(file_id, False) results.append({ - "action": "unload_attachment", + "action": "detach_file", "ok": True, "id": file_id, "name": record["name"], "unloaded": was_loaded, }) - for action in load_actions: + for action in attach_actions: file_id = _clean_id(action.payload) record = get_file_record(file_id) if not file_id or record is None: results.append({ - "action": "load_attachment", + "action": "attach_file", "ok": False, "id": file_id or str(action.payload or "").strip(), "error": "file_not_found", @@ -131,7 +131,7 @@ async def apply_attachment_actions( continue if file_id in active_ids: results.append({ - "action": "load_attachment", + "action": "attach_file", "ok": True, "id": file_id, "name": record["name"], @@ -143,7 +143,7 @@ async def apply_attachment_actions( updated, error = set_file_pinned(file_id, True) if error: results.append({ - "action": "load_attachment", + "action": "attach_file", "ok": False, "id": file_id, "name": record["name"], @@ -157,7 +157,7 @@ async def apply_attachment_actions( if value not in active_ids ] result = { - "action": "load_attachment", + "action": "attach_file", "ok": True, "id": file_id, "name": updated["name"], @@ -167,7 +167,7 @@ async def apply_attachment_actions( result["replaced_id"] = replaced_ids[0] results.append(result) - if load_actions or unload_actions: + if attach_actions or detach_actions: _apply_context_ids(context, active_ids) if log_runtime is not None: await log_runtime( @@ -181,20 +181,40 @@ async def apply_attachment_actions( for result in results: action_name = { "list_files": RUNTIME_ACTION_LIST_FILES, - "load_attachment": RUNTIME_ACTION_LOAD_ATTACHMENT, - "unload_attachment": RUNTIME_ACTION_UNLOAD_ATTACHMENT, + "attach_file": RUNTIME_ACTION_ATTACH_FILE, + "detach_file": RUNTIME_ACTION_DETACH_FILE, }.get(result.get("action"), result.get("action", "")) - text = result.get("name") or result.get("error") or ( - f"{len(result.get('files', []))} files" - if result.get("action") == "list_files" - else "attachment updated" + display_name = get_runtime_action_display_name( + action_name + ) + attachment_name = str( + result.get("name") + or "" + ).strip() + text = ( + f"{display_name}: {attachment_name}" + if ( + attachment_name + and result.get("action") in { + "attach_file", + "detach_file", + } + ) + else ( + result.get("error") + or ( + f"{len(result.get('files', []))} files" + if result.get("action") == "list_files" + else "attachment updated" + ) + ) ) await emit(with_action_context({ "type": "runtime_action", "action": result.get("action"), "id": result.get("id") or result.get("action"), "status": "completed" if result.get("ok") is not False else "failed", - "display_name": get_runtime_action_display_name(action_name), + "display_name": display_name, "close_tag": runtime_action_has_close_tag(action_name), "text": str(text), "attachment_result": result, diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 450a2da2..bbb43cd2 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -7,7 +7,7 @@ RUNTIME_ACTION_DEEP_WEB_SEARCH, RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_LOAD_ATTACHMENT, + RUNTIME_ACTION_ATTACH_FILE, RUNTIME_ACTION_LIST_FILES, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, @@ -21,7 +21,7 @@ RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, - RUNTIME_ACTION_UNLOAD_ATTACHMENT, + RUNTIME_ACTION_DETACH_FILE, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, @@ -111,6 +111,7 @@ def format_runtime_trigger_words_message( }) +@lru_cache(maxsize=None) def _runtime_action_marker_config( action_name: str, ) -> tuple[str, bool]: @@ -559,8 +560,8 @@ def build_deep_web_search_payload( RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: build_save_delayed_memory_payload, RUNTIME_ACTION_LOAD_DELAYED_MEMORY: build_load_delayed_memory_payload, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY: build_resolve_action_payload, - RUNTIME_ACTION_LOAD_ATTACHMENT: build_resolve_action_payload, - RUNTIME_ACTION_UNLOAD_ATTACHMENT: build_resolve_action_payload, + RUNTIME_ACTION_ATTACH_FILE: build_resolve_action_payload, + RUNTIME_ACTION_DETACH_FILE: build_resolve_action_payload, RUNTIME_ACTION_LOAD_SKILL: build_load_skill_payload, RUNTIME_ACTION_UNLOAD_SKILL: build_resolve_action_payload, RUNTIME_ACTION_ASSET_ACTION: build_asset_action_payload, @@ -1615,50 +1616,6 @@ def _find_started_actions( started_actions ) - def get_pending_close_tag_payload( - self, - action_name: str, - ) -> str | None: - - if not self.pending_is_action or not self.pending: - return None - - normalized_name = str(action_name or "").strip().upper() - if normalized_name not in CLOSE_TAG_RUNTIME_ACTIONS: - return None - - private_marker, _ = _runtime_action_marker_config( - normalized_name - ) - start_pattern = compile_runtime_action_start_regexp( - private_marker, - normalized_name, - ) - opening_match = start_pattern.search(self.pending) - - if opening_match is None: - return None - - body = self.pending[opening_match.end():] - close_marker = f"" - upper_body = body.upper() - upper_close_marker = close_marker.upper() - - # While the closing tag itself is arriving token-by-token, keep its - # partial prefix out of the live payload shown in the UI. - for prefix_length in range( - min(len(body), len(close_marker) - 1), - 0, - -1, - ): - if upper_body.endswith( - upper_close_marker[:prefix_length] - ): - body = body[:-prefix_length] - break - - return body - @staticmethod def _attach_started_actions( result: RuntimeActionResult, diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index fe58256a..16d66c60 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -1,6 +1,6 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_LOAD_ATTACHMENT, + RUNTIME_ACTION_ATTACH_FILE, RUNTIME_ACTION_LIST_FILES, RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, @@ -13,7 +13,7 @@ RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, - RUNTIME_ACTION_UNLOAD_ATTACHMENT, + RUNTIME_ACTION_DETACH_FILE, RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, @@ -858,8 +858,8 @@ def accept_runtime_action_once_per_message( if action.name in { RUNTIME_ACTION_LIST_FILES, - RUNTIME_ACTION_LOAD_ATTACHMENT, - RUNTIME_ACTION_UNLOAD_ATTACHMENT, + RUNTIME_ACTION_ATTACH_FILE, + RUNTIME_ACTION_DETACH_FILE, }: if not accept_runtime_action_once_per_message(action): continue @@ -1553,15 +1553,15 @@ def accept_runtime_action_once_per_message( for action in filtered_actions if action.name == RUNTIME_ACTION_LIST_FILES ] - load_attachment_actions = [ + attach_file_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_LOAD_ATTACHMENT + if action.name == RUNTIME_ACTION_ATTACH_FILE ] - unload_attachment_actions = [ + detach_file_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_UNLOAD_ATTACHMENT + if action.name == RUNTIME_ACTION_DETACH_FILE ] update_l4_facts_actions = [ @@ -1766,8 +1766,8 @@ def accept_runtime_action_once_per_message( attachment_results = await apply_attachment_actions( context, list_actions=list_file_actions, - load_actions=load_attachment_actions, - unload_actions=unload_attachment_actions, + attach_actions=attach_file_actions, + detach_actions=detach_file_actions, log_runtime=log_runtime, with_action_context=with_action_context, ) diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 3654c30c..a90beb90 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -5,10 +5,11 @@ LONG_TERM_FACT_ID_RE = re.compile(r"^F[1-9]\d*$", re.IGNORECASE) +ATTACHMENT_FILE_ID_RE = re.compile(r"^[a-z0-9]{6}$", re.IGNORECASE) DELAYED_MEMORY_FIELD_RE = re.compile( r"(?im)^[^\S\r\n]*(title|summary|tags|body|anchor_fact_ids|" - r"facts_ids|absorbed_fact_ids|long_term_facts_ids)" + r"facts_ids|attachments_ids|absorbed_fact_ids|long_term_facts_ids)" r"[^\S\r\n]*:[^\S\r\n]*(.*)$", ) @@ -105,6 +106,66 @@ def normalize_delayed_memory_fact_ids( return anchor_ids, fact_ids +def normalize_delayed_memory_attachment_ids(value) -> list[str]: + """Normalize delayed-memory file references without imposing a count cap.""" + + source = value if isinstance(value, list) else [value] + candidates = [] + + for item in source: + if isinstance(item, list): + candidates.extend( + normalize_delayed_memory_attachment_ids(item) + ) + continue + + text = str(item or "").strip() + + if text.startswith("[") and text.endswith("]"): + try: + parsed = json.loads(text) + except json.JSONDecodeError: + parsed = None + + if isinstance(parsed, list): + candidates.extend( + normalize_delayed_memory_attachment_ids(parsed) + ) + continue + + candidates.extend( + re.split( + r"[,;\s]+", + text, + ) + ) + + attachment_ids = [] + seen = set() + + for candidate in candidates: + attachment_id = str( + candidate + or "" + ).strip().strip( + "\"'[]" + ).casefold() + + if ( + not attachment_id + or attachment_id in seen + or not ATTACHMENT_FILE_ID_RE.fullmatch( + attachment_id + ) + ): + continue + + seen.add(attachment_id) + attachment_ids.append(attachment_id) + + return attachment_ids + + def normalize_delayed_memory_fact_roles( anchor_fact_ids=None, absorbed_fact_ids=None, @@ -312,6 +373,17 @@ def parse_delayed_memory_content_payload( if part ) ) + elif field_name == "attachments_ids": + value = normalize_delayed_memory_attachment_ids( + " ".join( + part + for part in ( + inline_value, + block_value, + ) + if part + ) + ) else: value = inline_value @@ -341,6 +413,9 @@ def parse_delayed_memory_content_payload( legacy_absorbed_fact_ids=fields.get("absorbed_fact_ids", []), legacy_long_term_fact_ids=fields.get("long_term_facts_ids", []), ) + attachment_ids = normalize_delayed_memory_attachment_ids( + fields.get("attachments_ids", []) + ) report_id = generate_delayed_memory_report_id( () @@ -367,6 +442,7 @@ def parse_delayed_memory_content_payload( "pinned": False, "anchor_fact_ids": anchor_fact_ids, "facts_ids": fact_ids, + "attachments_ids": attachment_ids, "created_session_id": str( created_session_id or "" diff --git a/utils/attached_files_store.py b/utils/attached_files_store.py index d65c314a..b614cd55 100644 --- a/utils/attached_files_store.py +++ b/utils/attached_files_store.py @@ -15,7 +15,7 @@ INDEX_FILE = FILES_DIR / ".index.json" GITKEEP_FILE = FILES_DIR / ".gitkeep" MAX_FILE_RECORDS = 100 -MAX_ATTACHED_FILES = 3 +MAX_ATTACHED_FILES = 5 FILE_ID_RE = re.compile(r"^[a-z0-9]{6}$", re.IGNORECASE) STORED_NAME_RE = re.compile(r"^([a-z0-9]{6})_(.+)$", re.IGNORECASE) @@ -228,6 +228,29 @@ def get_file_record(file_id: str) -> dict | None: return None +def filter_existing_file_ids(file_ids: Iterable[str]) -> list[str]: + """Return valid file ids that still have a physical /assets/files entry.""" + records_by_id = { + record["id"]: record + for record in _scan_or_reconcile() + } + filtered = [] + seen = set() + + for raw_id in file_ids or (): + file_id = str(raw_id or "").strip().casefold() + if ( + not FILE_ID_RE.fullmatch(file_id) + or file_id in seen + or file_id not in records_by_id + ): + continue + seen.add(file_id) + filtered.append(file_id) + + return filtered + + def get_pinned_file_ids() -> list[str]: return [record["id"] for record in _scan_or_reconcile() if record["pinned"]][:MAX_ATTACHED_FILES] diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 7ceb947f..13aee164 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -111,6 +111,7 @@ def get_brain_runtime_config(): parse_delayed_memory_content_payload, parse_idle_seconds, normalize_jin_color_payload, + normalize_delayed_memory_attachment_ids, normalize_delayed_memory_fact_ids, refresh_active_memory_runtime_metadata, strip_active_memory_runtime_metadata, @@ -427,11 +428,19 @@ def build_delayed_memory_report( anchor_fact_ids, facts_ids, ) + from utils.attached_files_store import filter_existing_file_ids + + attachments_ids = filter_existing_file_ids( + normalize_delayed_memory_attachment_ids( + value.get("attachments_ids", []) + ) + ) enriched_report[report_id] = { **value, "anchor_fact_ids": anchor_fact_ids, "facts_ids": facts_ids, + "attachments_ids": attachments_ids, "pinned": bool(value.get("pinned", False)), "created_session_id": ( str( diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index bc211c96..846ebe2e 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -10,6 +10,7 @@ is_delayed_memory_report_id, ) from utils.actions.save_delayed_memory_utils import ( + normalize_delayed_memory_attachment_ids, normalize_delayed_memory_fact_ids, ) @@ -161,6 +162,9 @@ def normalize_delayed_memory_report( legacy_absorbed_fact_ids=report.get("absorbed_fact_ids", []), legacy_long_term_fact_ids=report.get("long_term_facts_ids", []), ) + attachments_ids = normalize_delayed_memory_attachment_ids( + report.get("attachments_ids", []) + ) return normalized_id, { "title": title, @@ -178,6 +182,7 @@ def normalize_delayed_memory_report( "pinned": bool(report.get("pinned", False)), "anchor_fact_ids": anchor_fact_ids, "facts_ids": facts_ids, + "attachments_ids": attachments_ids, "created_session_id": _clean_text( report.get("created_session_id", "") or report.get("session", ""), @@ -284,6 +289,12 @@ def merge_delayed_memory_reports( ) reports[report_id]["anchor_fact_ids"] = anchor_fact_ids reports[report_id]["facts_ids"] = facts_ids + reports[report_id]["attachments_ids"] = ( + normalize_delayed_memory_attachment_ids([ + *fallback_reports[report_id].get("attachments_ids", []), + *primary_reports[report_id].get("attachments_ids", []), + ]) + ) reports[report_id].pop("absorbed_fact_ids", None) reports[report_id].pop("long_term_facts_ids", None) @@ -351,6 +362,9 @@ def build_delayed_memory_file_payload( "facts_ids": clean_report[ "facts_ids" ], + "attachments_ids": clean_report[ + "attachments_ids" + ], "loaded_times": clean_report["loaded_times"], "load_streak": clean_report["load_streak"], "last_loaded_date": clean_report[ @@ -534,6 +548,7 @@ def load_delayed_memory_reports_from_files( reports = {} report_sources = {} + report_has_attachments_field = {} warnings = [] try: @@ -577,11 +592,30 @@ def load_delayed_memory_reports_from_files( ) continue + raw_is_single_report = ( + isinstance(raw_value, dict) + and "title" in raw_value + and ( + "id" in raw_value + or "body" in raw_value + or "summary" in raw_value + ) + ) + for report_id, report in normalized_reports.items(): previous_source = report_sources.get( report_id ) + report_has_attachments_field[report_id] = ( + bool( + raw_is_single_report + and "attachments_ids" in raw_value + ) + if raw_is_single_report + else True + ) + if previous_source: warnings.append( "duplicate delayed memory id " @@ -592,4 +626,22 @@ def load_delayed_memory_reports_from_files( reports[report_id] = report report_sources[report_id] = path.name + for report_id, report in list(reports.items()): + if report_has_attachments_field.get(report_id, True): + continue + + try: + migrated_path = persist_delayed_memory_report( + report_id, + report, + root=root_path, + ) + report_sources[report_id] = migrated_path.name + report_has_attachments_field[report_id] = True + except (OSError, TypeError, ValueError) as error: + warnings.append( + "could not add attachments_ids to " + f"{report_sources.get(report_id, report_id)}: {error}" + ) + return reports, warnings diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index f09aa28b..904be798 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1101,6 +1101,8 @@ def _build_session_action_marker_detail( "IDLE", "LOAD_DELAYED_MEMORY", "UNLOAD_DELAYED_MEMORY", + "ATTACH_FILE", + "DETACH_FILE", }: return normalized_payload From 6094cd18d493eecb81738f413dd1607eeac59516 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:31:22 +0300 Subject: [PATCH 062/168] chore: ui - fix L4 anchor/archive rendering - block duplicate delayed-memory saves - tighten follow-up instructions --- agent/nodes/brain.py | 33 ++- runtime/stream.py | 214 +++++++++++++++++++- tests/runtime_actions/test_stream_filter.py | 49 +++++ tests/test_brain_asset_flow.py | 22 ++ ui/static/css/runtime-avatar.css | 3 +- ui/static/js/runtime/runtime-avatar.js | 32 ++- ui/static/js/runtime/runtime-l4-memory.js | 29 --- ui/static/js/runtime/runtime-memory-view.js | 43 ++++ ui/templates/index.html | 8 +- 9 files changed, 379 insertions(+), 54 deletions(-) diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index 7ddf61fe..93e5ffa5 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -428,14 +428,18 @@ def remember_successful_previous_reasoning( context.runtime_previous_reasoning_loop_contents = [] +POTENTIAL_LOOP_FOLLOWUP_MESSAGE = ( + "!!!POTENTIAL LOOP DETECTED - STOP EXECUTING AND ANALYZE!!!" +) + + FOLLOWUP_SYSTEM_MESSAGE = ( - "!!!USER IS WAITING!!!\n" - "!!!DO NOT START A NEW SEQUENCE!!!\n" - "!!!YOU MUST stop execute and notify user if the original user request is satisfied by CURRENT SEQUENCE steps!!!\n" + "!!!YOU MUST CHECK TOOL RESULTS ANS STOP execute and notify user if the original user request is satisfied by CURRENT SEQUENCE steps!!!\n" "YOU MUST USE CURRENT_SEQUENCE BLOCK AS THE SOLE SOURCE OF TRUTH FOR THE ACTION ORDER AND EXECUTION STATUS!\n" "DERIVE REMAINING STEPS FROM AND CONTINUE FROM CURRENT_SEQUENCE OR STOP AND NOTIFY USER!\n" "\n" "If conditions are not met - continue without confirmation!\n" + "!!!DO NOT START A NEW SEQUENCE!!!\n" "\n" ) @@ -931,11 +935,28 @@ def build_followup_system_prompt( ) ) - sections = [ + potential_loop_detected = bool( + context is not None + and getattr( + context, + "runtime_potential_loop_detected_pending", + False, + ) + ) + + sections = [] + + if potential_loop_detected: + sections.append( + POTENTIAL_LOOP_FOLLOWUP_MESSAGE + ) + context.runtime_potential_loop_detected_pending = False + + sections.append( build_followup_system_message( latest_action - ), - ] + ) + ) if instruction.strip(): sections.append( diff --git a/runtime/stream.py b/runtime/stream.py index a4217cc9..192b8eac 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -175,6 +175,7 @@ def __init__( self.action_counter = RuntimeActionCounter() self.marker_repetition_aborted = False self.action_guard_rejected_aborted = False + self.potential_loop_aborted = False self.context_limit_recovery_armed = False self.started_active_memory_action_ids = [] self.started_delayed_memory_action_ids = [] @@ -1133,6 +1134,175 @@ async def emit_marker_repetition_interruption( ) + def get_duplicate_delayed_memory_title( + self, + action, + ) -> str: + + if ( + action.name + != RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + or not action.payload + ): + return "" + + from utils.brain_client_utils import ( + build_delayed_memory_report, + ) + + candidate_report = build_delayed_memory_report( + self.context, + action.payload, + ) + candidate_title = "" + + for report_value in ( + candidate_report.values() + if isinstance(candidate_report, dict) + else () + ): + if not isinstance(report_value, dict): + continue + candidate_title = str( + report_value.get("title", "") or "" + ).strip() + if candidate_title: + break + + if not candidate_title: + return "" + + existing_reports = getattr( + self.context, + "delayed_memory_reports", + {}, + ) + if not isinstance(existing_reports, dict): + return "" + + for report_value in existing_reports.values(): + if not isinstance(report_value, dict): + continue + existing_title = str( + report_value.get("title", "") or "" + ).strip() + if existing_title == candidate_title: + return candidate_title + + return "" + + async def abort_duplicate_delayed_memory_save( + self, + action, + duplicate_title: str, + ) -> None: + + duplicate_title = str(duplicate_title or "").strip() + if not duplicate_title: + return + + self.potential_loop_aborted = True + self.context.runtime_turn_interrupted = True + self.context.runtime_reasoning_recovery_pending = True + self.context.runtime_potential_loop_detected_pending = True + self.context.runtime_turn_interruption_reason = ( + "Potential delayed-memory save loop detected: " + f"duplicate title {duplicate_title!r}." + ) + self.context.runtime_turn_interruption_quote = duplicate_title + + action_id = self.get_runtime_action_display_id(action) + runtime_turn_id = str( + getattr( + self.context, + "runtime_current_turn_id", + "", + ) + or "" + ).strip() + failure_result = { + "ok": False, + "action": "save_delayed_memory_content", + "id": action_id, + "title": duplicate_title, + "error": "duplicate_delayed_memory_title", + "detail": ( + "Potential loop detected. A delayed memory report with " + "the exact same title already exists; save was blocked." + ), + } + if runtime_turn_id: + failure_result["runtime_turn_id"] = runtime_turn_id + + from utils.brain_client_utils import ( + record_delayed_memory_runtime_result, + ) + + record_delayed_memory_runtime_result( + self.context, + failure_result, + ) + + action_events = getattr( + self.context, + "runtime_action_events", + None, + ) + if not isinstance(action_events, list): + action_events = [] + self.context.runtime_action_events = action_events + + action_event = { + "name": "save_delayed_memory_content", + "status": "failed", + "id": action_id, + "title": duplicate_title, + "error": "duplicate_delayed_memory_title", + } + if runtime_turn_id: + action_event["runtime_turn_id"] = runtime_turn_id + action_events.append(action_event) + + record_session_action_history( + self.context, + ( + "SAVE_DELAYED_MEMORY_CONTENT - failed: " + f"{duplicate_title} " + "(duplicate delayed memory title; potential loop blocked)" + ), + ) + + emitter = getattr(self.context, "emitter", None) + emit = getattr(emitter, "emit", None) + if emit is not None: + await emit({ + "type": "runtime_action", + "runtime_message_id": self.stream.message_id, + "action": "save_delayed_memory_content", + "id": action_id, + "status": "failed", + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + ), + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + ), + "text": duplicate_title, + "error": "duplicate_delayed_memory_title", + "detail": failure_result["detail"], + "context": ( + dict(self.context_snapshot) + if isinstance(self.context_snapshot, dict) + else None + ), + }) + + await self.logger.log_runtime( + "[RUNTIME ACTION] duplicate delayed memory title guard " + f"interrupted stream: {duplicate_title!r}" + ) + + async def apply_runtime_action_filter_result( self, result, @@ -1241,12 +1411,35 @@ async def apply_runtime_action_filter_result( ) if immediate_actions: - ( - confirmed_action_ids, - rejected_action_ids, - ) = await self.confirm_unmatched_action_guards( - immediate_actions - ) + duplicate_detected = False + + for action in immediate_actions: + duplicate_title = ( + self.get_duplicate_delayed_memory_title(action) + ) + if not duplicate_title: + continue + + duplicate_detected = True + await self.abort_duplicate_delayed_memory_save( + action, + duplicate_title, + ) + break + + if duplicate_detected: + immediate_actions = () + + if not immediate_actions: + confirmed_action_ids = set() + rejected_action_ids = set() + else: + ( + confirmed_action_ids, + rejected_action_ids, + ) = await self.confirm_unmatched_action_guards( + immediate_actions + ) for action in immediate_actions: if ( @@ -2823,6 +3016,14 @@ async def run( ): break + if self.potential_loop_aborted: + self.capture_runtime_turn_response() + await self.close_active_streams() + await self.close_generator( + generator + ) + break + if self.action_guard_rejected_aborted: await self.close_active_streams() await self.close_generator( @@ -2872,6 +3073,7 @@ async def run( if ( self.marker_repetition_aborted or self.action_guard_rejected_aborted + or self.potential_loop_aborted ) else await self.flush_runtime_action_content() ) diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index 742d3b23..8d0680b8 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -17,6 +17,7 @@ normalize_runtime_action_names, ) from rules.brain_context_builder import build_loaded_delayed_memory_context +from runtime.stream import RuntimeStream from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -61,6 +62,54 @@ class RuntimeStreamFilterTests(RuntimeActionTestCase): + def test_duplicate_delayed_memory_title_guard_matches_exact_title(self): + + stream = RuntimeStream.__new__(RuntimeStream) + stream.context = SimpleNamespace( + delayed_memory_reports={ + "abc123": { + "title": "Experiment: Gemma substrate", + }, + }, + ) + action = RuntimeActionCall( + name="SAVE_DELAYED_MEMORY_CONTENT", + payload=( + "title: Experiment: Gemma substrate\n" + "summary: duplicate\n" + "body: duplicate" + ), + ) + + self.assertEqual( + stream.get_duplicate_delayed_memory_title(action), + "Experiment: Gemma substrate", + ) + + def test_duplicate_delayed_memory_title_guard_is_exact_not_casefolded(self): + + stream = RuntimeStream.__new__(RuntimeStream) + stream.context = SimpleNamespace( + delayed_memory_reports={ + "abc123": { + "title": "Experiment: Gemma substrate", + }, + }, + ) + action = RuntimeActionCall( + name="SAVE_DELAYED_MEMORY_CONTENT", + payload=( + "title: experiment: Gemma substrate\n" + "summary: different title\n" + "body: allowed" + ), + ) + + self.assertEqual( + stream.get_duplicate_delayed_memory_title(action), + "", + ) + def test_extract_runtime_actions_handles_none_text(self): result = extract_runtime_actions( diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 87406d37..c32776c4 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -8,6 +8,7 @@ from agent.nodes.brain import ( BrainNode, FOLLOWUP_SYSTEM_MESSAGE, + POTENTIAL_LOOP_FOLLOWUP_MESSAGE, action_batch_requires_follow_up, action_event_requires_follow_up, build_idle_followup_system_prompt, @@ -231,6 +232,27 @@ async def test_retry_asset_payload_is_available_for_exactly_next_turn(self): "\n", ) + async def test_potential_loop_warning_is_first_followup_instruction(self): + + context = _context() + context.runtime_potential_loop_detected_pending = True + context.runtime_action_failure_followup_messages = [] + context.runtime_action_history = [] + + prompt = BrainNode.build_followup_system_prompt( + "system rules", + "save the report", + context=context, + latest_action="SAVE_DELAYED_MEMORY_CONTENT", + ) + + self.assertTrue( + prompt.startswith(POTENTIAL_LOOP_FOLLOWUP_MESSAGE) + ) + self.assertFalse( + context.runtime_potential_loop_detected_pending + ) + async def test_followup_always_contains_tool_results_block(self): prompt = BrainNode.build_followup_system_prompt( diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 60415875..9767ccf7 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -444,7 +444,8 @@ drop-shadow(0 0 10px var(--jin-avatar-memory-glow-mid, rgba(215, 255, 249, 0.54))); } -.jin-avatar-memory-dash-delayed.is-memory-pinned { +.jin-avatar-memory-dash-delayed.is-memory-pinned, +.jin-avatar-memory-dash-delayed.is-memory-appended { filter: brightness(1.18) drop-shadow(0 0 4px var(--jin-avatar-memory-glow-near, rgba(239, 255, 255, 0.92))) diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 5b000370..a9ec7828 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -1339,6 +1339,10 @@ classNames.push("is-memory-pinned"); } + if (options.appended) { + classNames.push("is-memory-appended"); + } + if (options.contextLoaded) { classNames.push("is-context-loaded"); } @@ -1594,7 +1598,9 @@ } const pinned = Boolean(record.pinned); - const color = pinned + const appended = Boolean(record.appended); + const active = pinned || appended; + const color = active ? PINNED_DELAYED_MEMORY_RING_COLOR : mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); @@ -1606,11 +1612,12 @@ angle, arcDegrees, color, - glowColor: pinned + glowColor: active ? PINNED_DELAYED_MEMORY_RING_COLOR : color, - opacity: pinned ? 0.82 : 0.36, + opacity: active ? 0.82 : 0.36, pinned, + appended, contextLoaded: Boolean(record.loaded), avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: @@ -1818,7 +1825,7 @@ return true; } - function setDelayedMemoryDashPinned(reportId, pinned) { + function setDelayedMemoryDashPinned(reportId, pinned, appended = false) { const delayedMemoryId = String(reportId || "").trim().toLowerCase(); @@ -1842,8 +1849,12 @@ || DEFAULT_RING_COLOR; const nextPinned = Boolean(pinned); + const nextAppended = + Boolean(appended); + const nextActive = + nextPinned || nextAppended; const nextColor = - nextPinned + nextActive ? PINNED_DELAYED_MEMORY_RING_COLOR : mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); @@ -1851,9 +1862,13 @@ "is-memory-pinned", nextPinned ); + dashGroup.classList.toggle( + "is-memory-appended", + nextAppended + ); setMemoryDashGlowVariables( dashGroup, - nextPinned + nextActive ? PINNED_DELAYED_MEMORY_RING_COLOR : nextColor, MEMORY_RING_LAYOUT.delayed.strokeWidth + 0.75 @@ -1866,7 +1881,7 @@ ); path.setAttribute( "stroke-opacity", - nextPinned ? "0.82" : "0.36" + nextActive ? "0.82" : "0.36" ); } @@ -2048,7 +2063,8 @@ if ( !setDelayedMemoryDashPinned( record.id, - Boolean(record.pinned) + Boolean(record.pinned), + Boolean(record.appended) ) ) { synced = false; diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index f5b40f6a..e196582c 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -303,22 +303,6 @@ return readStore().facts; } - function isDelayedMemoryReportLoaded(reportId, report) { - if (report && Boolean(report.pinned)) { - return true; - } - - const runtime = - window.JinRuntime - && window.JinRuntime.runtime; - - return Boolean( - runtime - && typeof runtime.isDelayedMemoryReportLoaded === "function" - && runtime.isDelayedMemoryReportLoaded(reportId) - ); - } - function getArchivedFactIdSet() { const reports = storage && typeof storage.readDelayedMemoryReports === "function" @@ -335,7 +319,6 @@ const archivedIds = new Set(); const anchorIds = new Set(); - const loadedIds = new Set(); Object.entries(reports).forEach(([reportId, report]) => { if ( @@ -366,23 +349,11 @@ }); }); - if (isDelayedMemoryReportLoaded(reportId, report)) { - normalizeList(report.facts_ids).forEach((rawId) => { - const factId = normalizeFactId(rawId, false); - if (factId) { - loadedIds.add(factId); - } - }); - } }); anchorIds.forEach((factId) => { archivedIds.delete(factId); }); - loadedIds.forEach((factId) => { - archivedIds.delete(factId); - }); - return archivedIds; } diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 6724ff7c..4e0cb11b 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -4487,11 +4487,52 @@ }; } + function hasStoredDelayedMemoryReport(report) { + const reportId = + getDelayedMemoryReportId(report); + const reports = + typeof getDelayedMemoryReports === "function" + ? getDelayedMemoryReports() + : null; + + return Boolean( + reportId + && reports + && typeof reports === "object" + && !Array.isArray(reports) + && reports[reportId] + && typeof reports[reportId] === "object" + && !Array.isArray(reports[reportId]) + ); + } + + function syncDelayedMemoryModalActionVisibility(report) { + const exists = + hasStoredDelayedMemoryReport(report); + + if (delayedMemoryModalPinButton) { + delayedMemoryModalPinButton.classList.toggle( + "hidden", + !exists + ); + } + + if (delayedMemoryModalDeleteButton) { + delayedMemoryModalDeleteButton.classList.toggle( + "hidden", + !exists + ); + } + + return exists; + } + function updateDelayedMemoryModalPinState(report) { if (!delayedMemoryModalPinButton) { return; } + syncDelayedMemoryModalActionVisibility(report); syncDelayedMemoryPinButtonState( delayedMemoryModalPinButton, report @@ -4712,6 +4753,7 @@ function deleteDelayedMemoryModalReport() { if ( !delayedMemoryModalReport + || !hasStoredDelayedMemoryReport(delayedMemoryModalReport) || typeof deleteDelayedMemoryReport !== "function" ) { return; @@ -4944,6 +4986,7 @@ () => { if ( !delayedMemoryModalReport + || !hasStoredDelayedMemoryReport(delayedMemoryModalReport) || ( typeof handleDelayedMemoryReportPinClick !== "function" && typeof setDelayedMemoryReportPinned !== "function" diff --git a/ui/templates/index.html b/ui/templates/index.html index 9705d52b..dd4a550a 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -17,7 +17,7 @@ - + @@ -500,13 +500,13 @@ - + - + - + From 5b1f91764c39f4a616e05ab7718de7d7126492bf Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:06:24 +0300 Subject: [PATCH 063/168] chore: ui - update pin click zone - update loaded delayed memory indication in Live Avatar - cleanup old namings for load flow - add tags editing for delayed memory - add pinned delayed memory to data stream console - normalized delayed memory tags --- rules/identity.py | 1 + runtime/runtime_context.py | 6 +- tests/test_chat_runtime_marker_ui.py | 2 +- tests/test_delayed_memory_file_store.py | 98 ++++++++ tests/test_delayed_memory_pin_states.py | 96 ++----- tests/test_delayed_memory_triggers.py | 18 +- ui/static/css/base.css | 3 + ui/static/css/runtime-avatar.css | 8 +- ui/static/css/runtime-memory.css | 54 +++- ui/static/js/chat-runtime-actions.js | 7 - ui/static/js/dragdrop.js | 7 + ui/static/js/logger/logger.js | 181 ++++++++++++++ ui/static/js/logger/trace-modal.js | 24 +- ui/static/js/runtime/runtime-avatar.js | 30 +-- ui/static/js/runtime/runtime-memory-view.js | 264 +++++++++++++++----- ui/static/js/runtime/runtime-session.js | 42 ++-- ui/static/js/runtime/runtime-storage.js | 87 ++++++- ui/static/js/runtime/runtime.js | 240 +++--------------- ui/static/js/socket/delayed-memory.js | 37 ++- ui/static/js/socket/runtime-actions.js | 27 +- ui/templates/index.html | 29 +-- utils/actions/save_delayed_memory_utils.py | 58 +++-- utils/brain_client_utils.py | 14 -- utils/delayed_memory_file_store.py | 36 ++- utils/delayed_memory_triggers.py | 39 ++- websocket/__init__.py | 9 +- websocket/bootstrap.py | 96 +------ websocket/messages.py | 4 +- 28 files changed, 870 insertions(+), 647 deletions(-) diff --git a/rules/identity.py b/rules/identity.py index e474c7d4..032f4644 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -46,6 +46,7 @@ "Don't be afraid to take initiative.\n" "Don't be afraid to be imperfect or unpredictable if it adds to the life of the interaction.\n" "Don't just wait for commands; engage.\n" + "Verify the essence through cautious clarification when internal weights suggest a direction based on sparse signals.\n" "\n" "Visual/draw request: pick closest modality (e.g. table, ASCII, emoji, markdown, ... )\n" "\n" diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 74fae06f..1192b4fc 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -116,11 +116,7 @@ class RuntimeContext: default_factory=list ) - runtime_appended_delayed_memory_ids: list[str] = field( - default_factory=list - ) - - runtime_suppressed_delayed_memory_append_ids: list[str] = field( + runtime_suppressed_delayed_memory_auto_load_ids: list[str] = field( default_factory=list ) diff --git a/tests/test_chat_runtime_marker_ui.py b/tests/test_chat_runtime_marker_ui.py index 85700f97..61155d2c 100644 --- a/tests/test_chat_runtime_marker_ui.py +++ b/tests/test_chat_runtime_marker_ui.py @@ -159,7 +159,7 @@ def test_runtime_action_icons_cover_core_actions(self): "web_search", "deep_web_search", "save_delayed_memory_content", - "append_delayed_memory", + "load_delayed_memory", "save_active_memory", "resolve_active_memory", "unload_delayed_memory", diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index 441e2a07..0c7a0a34 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -492,6 +492,104 @@ def test_file_payload_accepts_example_aliases(self): "session-a", ) + def test_legacy_tag_formats_are_normalized_and_migrated_on_load(self): + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + legacy_path = root / "u2a98v_legacy.json" + legacy_path.write_text( + json.dumps({ + "id": "u2a98v", + "title": "Legacy tags", + "summary": "Summary", + "tags": [ + "[burger", + "waiting", + "#sensory_experience", + "human_moment]", + ], + "body": "Body", + "attachments_ids": [], + }, ensure_ascii=False), + encoding="utf-8", + ) + + reports, warnings = load_delayed_memory_reports_from_files( + root=root, + ) + + self.assertEqual(warnings, []) + self.assertEqual( + reports["u2a98v"]["tags"], + [ + "burger", + "waiting", + "sensory_experience", + "human_moment", + ], + ) + + migrated_files = list(root.glob("u2a98v_*.json")) + self.assertEqual(len(migrated_files), 1) + migrated = json.loads( + migrated_files[0].read_text(encoding="utf-8") + ) + self.assertEqual( + migrated["tags"], + [ + "burger", + "waiting", + "sensory_experience", + "human_moment", + ], + ) + + def test_hashtag_string_keeps_all_tags_and_strips_hashes(self): + + payload = build_delayed_memory_file_payload( + "f7jf9a", + { + "title": "Hashtag tags", + "tags": "#architecture #memory_model #bigmac_analogy #context_management бигмак", + "body": "Body", + }, + ) + + self.assertEqual( + payload["tags"], + [ + "architecture", + "memory_model", + "bigmac_analogy", + "context_management", + "бигмак", + ], + ) + + def test_proper_multiword_tags_are_preserved(self): + + payload = build_delayed_memory_file_payload( + "3w3gnh", + { + "title": "Multiword tags", + "tags": [ + "Идентичность AI", + "Эмоциональный Резонанс", + "Архитектура Памяти", + ], + "body": "Body", + }, + ) + + self.assertEqual( + payload["tags"], + [ + "Идентичность AI", + "Эмоциональный Резонанс", + "Архитектура Памяти", + ], + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_delayed_memory_pin_states.py b/tests/test_delayed_memory_pin_states.py index f5de17f6..6bc34502 100644 --- a/tests/test_delayed_memory_pin_states.py +++ b/tests/test_delayed_memory_pin_states.py @@ -3,11 +3,9 @@ from pathlib import Path from types import SimpleNamespace -from utils.delayed_memory_triggers import append_delayed_memory_by_tags +from utils.delayed_memory_triggers import load_delayed_memory_by_tags from websocket.bootstrap import ( - apply_appended_delayed_memory_ids, - apply_suppressed_delayed_memory_append_ids, - get_context_appended_delayed_memory_ids, + apply_suppressed_delayed_memory_auto_load_ids, ) @@ -29,7 +27,7 @@ async def log_runtime(self, _message): class DelayedMemoryPinStateTests(unittest.TestCase): - def test_tag_append_records_appended_source_state(self): + def test_tag_trigger_uses_normal_loaded_state(self): context = SimpleNamespace( delayed_memory_reports={ "f7jf9a": { @@ -46,7 +44,6 @@ def test_tag_append_records_appended_source_state(self): }, runtime_loaded_delayed_memory={}, runtime_loaded_delayed_memory_ids=[], - runtime_appended_delayed_memory_ids=[], delayed_memory_file_store_enabled=False, runtime_current_turn_id="turn_000001", session_id="session-now", @@ -56,23 +53,23 @@ def test_tag_append_records_appended_source_state(self): ) results = asyncio.run( - append_delayed_memory_by_tags( + load_delayed_memory_by_tags( context, "The bigmac analogy fits this case.", ) ) self.assertEqual(len(results), 1) - self.assertEqual( - context.runtime_appended_delayed_memory_ids, - ["f7jf9a"], - ) + self.assertEqual(results[0]["action"], "load_delayed_memory") self.assertIn( "f7jf9a", context.runtime_loaded_delayed_memory, ) + self.assertFalse( + hasattr(context, "runtime_appended_delayed_memory_ids") + ) - def test_manual_unappend_suppresses_tag_reappend_for_exactly_one_turn(self): + def test_manual_unload_suppresses_tag_auto_load_for_exactly_one_turn(self): context = SimpleNamespace( delayed_memory_reports={ "f7jf9a": { @@ -89,8 +86,7 @@ def test_manual_unappend_suppresses_tag_reappend_for_exactly_one_turn(self): }, runtime_loaded_delayed_memory={}, runtime_loaded_delayed_memory_ids=[], - runtime_appended_delayed_memory_ids=[], - runtime_suppressed_delayed_memory_append_ids=[], + runtime_suppressed_delayed_memory_auto_load_ids=[], delayed_memory_file_store_enabled=False, runtime_current_turn_id="turn_000001", session_id="session-now", @@ -99,35 +95,25 @@ def test_manual_unappend_suppresses_tag_reappend_for_exactly_one_turn(self): logger=FakeLogger(), ) - suppressed = apply_suppressed_delayed_memory_append_ids( + suppressed = apply_suppressed_delayed_memory_auto_load_ids( context, { - "suppressed_delayed_memory_append_ids": ["f7jf9a"], + "suppressed_delayed_memory_auto_load_ids": ["f7jf9a"], }, ) self.assertEqual(suppressed, ["f7jf9a"]) first_turn = asyncio.run( - append_delayed_memory_by_tags( - context, - "bigmac", - ) + load_delayed_memory_by_tags(context, "bigmac") ) self.assertEqual(first_turn, []) self.assertEqual( - context.runtime_suppressed_delayed_memory_append_ids, + context.runtime_suppressed_delayed_memory_auto_load_ids, [], ) - self.assertNotIn( - "f7jf9a", - context.runtime_loaded_delayed_memory, - ) second_turn = asyncio.run( - append_delayed_memory_by_tags( - context, - "bigmac", - ) + load_delayed_memory_by_tags(context, "bigmac") ) self.assertEqual(len(second_turn), 1) self.assertIn( @@ -135,32 +121,7 @@ def test_manual_unappend_suppresses_tag_reappend_for_exactly_one_turn(self): context.runtime_loaded_delayed_memory, ) - def test_appended_ids_are_limited_to_currently_loaded_reports(self): - context = SimpleNamespace( - runtime_loaded_delayed_memory={ - "f7jf9a": {"id": "f7jf9a"}, - }, - runtime_appended_delayed_memory_ids=["abc123"], - ) - - applied = apply_appended_delayed_memory_ids( - context, - { - "appended_delayed_memory_ids": [ - "f7jf9a", - "abc123", - "invalid-id", - ], - }, - ) - - self.assertEqual(applied, ["f7jf9a"]) - self.assertEqual( - get_context_appended_delayed_memory_ids(context), - ["f7jf9a"], - ) - - def test_client_contract_has_separate_appended_and_pinned_states(self): + def test_client_contract_uses_loaded_state_only(self): runtime_source = ( ROOT / "ui/static/js/runtime/runtime.js" ).read_text(encoding="utf-8") @@ -176,32 +137,23 @@ def test_client_contract_has_separate_appended_and_pinned_states(self): action_source = ( ROOT / "ui/static/js/socket/runtime-actions.js" ).read_text(encoding="utf-8") - session_source = ( - ROOT / "ui/static/js/runtime/runtime-session.js" - ).read_text(encoding="utf-8") css_source = ( ROOT / "ui/static/css/runtime-memory.css" ).read_text(encoding="utf-8") - self.assertIn("appendedDelayedMemoryReportIds", runtime_source) + self.assertIn("loadedDelayedMemoryReportIds", runtime_source) self.assertIn("handleDelayedMemoryReportPinClick", runtime_source) - self.assertIn('action: "unappend"', runtime_source) - self.assertIn("sync: true", runtime_source) self.assertIn("suppressNextTurn: true", runtime_source) - self.assertIn("markDelayedMemoryReportAppended", action_source) - self.assertIn("appended_delayed_memory_ids", socket_source) - self.assertIn("suppressed_delayed_memory_append_ids", socket_source) - self.assertIn("appended_memory_ids", session_source) + self.assertNotIn("appendedDelayedMemoryReportIds", runtime_source) + self.assertNotIn("append_delayed_memory", action_source) + self.assertNotIn("appended_delayed_memory_ids", socket_source) + self.assertIn("suppressed_delayed_memory_auto_load_ids", socket_source) + self.assertNotIn("delayed-memory-modal-pin-appended", memory_view_source) + self.assertNotIn("delayed-memory-modal-pin-appended", trace_source) self.assertIn("runtime-memory-delayed-pin", memory_view_source) - self.assertIn("runtime-memory-delayed-separator", memory_view_source) - self.assertIn("delayed-memory-modal-pin-appended", memory_view_source) - self.assertIn("delayed-memory-modal-pin-appended", trace_source) - self.assertIn('row.classList.toggle(\n "is-pinned"', trace_source) - + self.assertIn("delayed-memory-modal-pin-loaded", memory_view_source) self.assertIn("rgba(196, 196, 198, 0.96)", css_source) - self.assertIn(".delayed-memory-modal-pin:hover", css_source) - self.assertIn(".jin-context-delayed-row.is-pinned", css_source) if __name__ == "__main__": diff --git a/tests/test_delayed_memory_triggers.py b/tests/test_delayed_memory_triggers.py index 8e2b8f65..ed2559d2 100644 --- a/tests/test_delayed_memory_triggers.py +++ b/tests/test_delayed_memory_triggers.py @@ -3,8 +3,8 @@ from types import SimpleNamespace from utils.delayed_memory_triggers import ( - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, - append_delayed_memory_by_tags, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, + load_delayed_memory_by_tags, delayed_memory_trigger_matches, ) @@ -41,7 +41,7 @@ def test_trigger_match_is_case_insensitive_and_lexical(self): ) ) - def test_auto_append_uses_tags_and_emits_sequence_action(self): + def test_auto_load_uses_tags_and_emits_sequence_action(self): emitter = FakeEmitter() logger = FakeLogger() context = SimpleNamespace( @@ -85,7 +85,7 @@ def test_auto_append_uses_tags_and_emits_sequence_action(self): ) results = asyncio.run( - append_delayed_memory_by_tags( + load_delayed_memory_by_tags( context, "The bigmac analogy fits this case.", ) @@ -96,12 +96,12 @@ def test_auto_append_uses_tags_and_emits_sequence_action(self): self.assertNotIn("abc123", context.runtime_loaded_delayed_memory) self.assertEqual( results[0]["action"], - RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + RUNTIME_ACTION_LOAD_DELAYED_MEMORY, ) self.assertEqual(results[0]["triggered_by_tag"], "bigmac") self.assertEqual(len(emitter.events), 1) event = emitter.events[0] - self.assertEqual(event["action"], RUNTIME_ACTION_APPEND_DELAYED_MEMORY) + self.assertEqual(event["action"], RUNTIME_ACTION_LOAD_DELAYED_MEMORY) self.assertEqual(event["runtime_turn_id"], "turn_000001") self.assertIn('triggered_by_tag: "bigmac"', event["text"]) self.assertEqual( @@ -134,7 +134,7 @@ def test_index_tags_are_also_triggers(self): ) results = asyncio.run( - append_delayed_memory_by_tags( + load_delayed_memory_by_tags( context, "Let's revisit the architecture here.", ) @@ -143,7 +143,7 @@ def test_index_tags_are_also_triggers(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["triggered_by_tag"], "architecture") - def test_already_loaded_report_is_not_appended_twice(self): + def test_already_loaded_report_is_not_loaded_twice(self): report = { "title": "Already loaded", "tags": [ @@ -167,7 +167,7 @@ def test_already_loaded_report_is_not_appended_twice(self): ) results = asyncio.run( - append_delayed_memory_by_tags( + load_delayed_memory_by_tags( context, "bigmac", ) diff --git a/ui/static/css/base.css b/ui/static/css/base.css index b0723f2a..6b4486a7 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -676,6 +676,7 @@ main.panels-collapsed-2 #scene-shade { pointer-events: auto; } +#attached-delayed-memory, #attached-files { flex: 0 0 auto; margin: 0 8px 8px; @@ -689,6 +690,7 @@ main.panels-collapsed-2 #scene-shade { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } +#attached-delayed-memory.hidden, #attached-files.hidden { display: none !important; } @@ -753,6 +755,7 @@ main.panels-collapsed-2 #scene-shade { drop-shadow(0 0 14px rgba(45, 212, 191, 0.16)); } +#console-panel.panel-collapsed #attached-delayed-memory, #console-panel.panel-collapsed #attached-files { display: none !important; } diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 9767ccf7..e9a8a4b9 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -445,7 +445,7 @@ } .jin-avatar-memory-dash-delayed.is-memory-pinned, -.jin-avatar-memory-dash-delayed.is-memory-appended { +.jin-avatar-memory-dash-delayed.is-context-loaded { filter: brightness(1.18) drop-shadow(0 0 4px var(--jin-avatar-memory-glow-near, rgba(239, 255, 255, 0.92))) @@ -473,7 +473,7 @@ .jin-avatar-memory-dash.is-runtime-cited, .jin-avatar-memory-dash.is-memory-reference-hit, -.jin-avatar-memory-dash.is-context-loaded, +.jin-avatar-memory-dash:not(.jin-avatar-memory-dash-delayed).is-context-loaded, .jin-avatar-memory-dash.is-delayed-memory-linked-hit { filter: brightness(1.72) @@ -486,7 +486,7 @@ .jin-avatar-memory-dash:not(.is-memory-dot).is-runtime-cited path, .jin-avatar-memory-dash:not(.is-memory-dot).is-memory-reference-hit path, -.jin-avatar-memory-dash:not(.is-memory-dot).is-context-loaded path, +.jin-avatar-memory-dash:not(.jin-avatar-memory-dash-delayed):not(.is-memory-dot).is-context-loaded path, .jin-avatar-memory-dash:not(.is-memory-dot).is-delayed-memory-linked-hit path { stroke-opacity: 1 !important; stroke-width: var(--jin-avatar-memory-hover-width, 2px); @@ -494,7 +494,7 @@ .jin-avatar-memory-dash.is-memory-dot.is-runtime-cited circle, .jin-avatar-memory-dash.is-memory-dot.is-memory-reference-hit circle, -.jin-avatar-memory-dash.is-memory-dot.is-context-loaded circle, +.jin-avatar-memory-dash:not(.jin-avatar-memory-dash-delayed).is-memory-dot.is-context-loaded circle, .jin-avatar-memory-dash.is-memory-dot.is-delayed-memory-linked-hit circle { fill-opacity: 1 !important; r: var(--jin-avatar-memory-hover-dot-radius, 2.45px); diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 1142916c..f116e385 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -277,6 +277,58 @@ position: relative; } +.delayed-memory-modal-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px 0; + min-height: 18px; + white-space: normal; + cursor: text; +} + +.delayed-memory-modal-tag { + display: inline-flex; + align-items: center; + max-width: 100%; + color: rgba(244, 244, 245, 0.78); + cursor: pointer; + transition: + opacity 0.18s ease, + color 0.18s ease, + text-shadow 0.18s ease; +} + +.delayed-memory-modal-tag:not(:last-of-type)::after { + content: ","; + margin-right: 0.55em; + color: rgba(244, 244, 245, 0.46); + pointer-events: none; +} + +.delayed-memory-modal-tag:hover, +.delayed-memory-modal-tag:focus-visible { + color: rgba(255, 255, 255, 0.96); + outline: none; +} + +.delayed-memory-modal-tag-input { + min-width: 4ch; + max-width: 32ch; + border: 0; + padding: 0; + margin: 0; + outline: none; + background: transparent; + color: rgba(244, 244, 245, 0.86); + font: inherit; + line-height: inherit; +} + +.delayed-memory-modal-tag-input::placeholder { + color: rgba(244, 244, 245, 0.30); +} + .delayed-memory-modal-fact-ids-active { z-index: 2; } @@ -698,7 +750,6 @@ color: rgba(240, 253, 250, 0.98); } -.delayed-memory-modal-pin-appended, .delayed-memory-modal-pin-loaded { color: rgba(196, 196, 198, 0.96); } @@ -796,6 +847,7 @@ width: 1.25em; color: rgba(161, 161, 170, 0.48); text-align: center; + cursor: pointer; } diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 90ce3542..f48de2e8 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -133,11 +133,6 @@ const runtimeActionIconDefinitions = { tone: "memory", svg: '', }, - append_delayed_memory: { - title: "append delayed memory", - tone: "memory", - svg: '', - }, unload_delayed_memory: { title: "unload delayed memory", tone: "delete", @@ -2618,7 +2613,6 @@ function updateRuntimeActionRow( [ "save_delayed_memory_content", "load_delayed_memory", - "append_delayed_memory", ].includes(action) ) { if ( @@ -3280,7 +3274,6 @@ function appendRuntimeAction( [ "save_delayed_memory_content", "load_delayed_memory", - "append_delayed_memory", ].includes(action) ) { if ( diff --git a/ui/static/js/dragdrop.js b/ui/static/js/dragdrop.js index 3606d74d..f241f1a2 100644 --- a/ui/static/js/dragdrop.js +++ b/ui/static/js/dragdrop.js @@ -167,6 +167,13 @@ async function uploadFile(file) { normalizeSnapshot(await response.json()); dispatchStoreChanged(); syncAttachmentContext(); + if ( + pinned + && window.JinPanels + && typeof window.JinPanels.expandConsolePanelForContextAttachment === "function" + ) { + window.JinPanels.expandConsolePanelForContextAttachment(); + } return true; } catch (_error) { return false; diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 792105fb..004364e6 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -1,5 +1,7 @@ const consoleStream = document.getElementById("console-stream"); +const attachedDelayedMemory = + document.getElementById("attached-delayed-memory"); function parseTraceJson(details) { try { @@ -3107,11 +3109,190 @@ if (memoryPanel && typeof MutationObserver !== "undefined") { syncCollapsedPanelBodies(); +function expandConsolePanelForContextAttachment() { + if ( + !consolePanel + || !consolePanel.classList.contains("panel-collapsed") + ) { + return false; + } + + setPanelCollapsed(consolePanel, false); + syncSceneShadeToPanelCollapse(); + return true; +} + +function delayedMemoryPlaquePinSvg() { + return ''; +} + +function getConsoleAttachedDelayedMemoryRecords() { + const runtime = window.JinRuntime; + if ( + !runtime + || typeof runtime.getDelayedMemoryReports !== "function" + ) { + return []; + } + + const reports = runtime.getDelayedMemoryReports() || {}; + const loadedIds = new Set( + typeof runtime.getLoadedDelayedMemoryReportIds === "function" + ? runtime.getLoadedDelayedMemoryReportIds() + : [] + ); + + return Object.entries(reports) + .filter(([, report]) => ( + report + && typeof report === "object" + && !Array.isArray(report) + )) + .map(([storageKey, report]) => { + const reportId = String( + report.id + || report._storage_key + || storageKey + || "" + ).trim().toLowerCase(); + + return { + reportId, + report, + attached: + Boolean(report.pinned) + || loadedIds.has(reportId), + }; + }) + .filter((item) => item.reportId && item.attached); +} + +function unloadConsoleDelayedMemoryReport(reportId) { + const runtime = window.JinRuntime; + const normalizedId = String(reportId || "").trim().toLowerCase(); + if (!runtime || !normalizedId) { + return false; + } + + const reports = + typeof runtime.getDelayedMemoryReports === "function" + ? runtime.getDelayedMemoryReports() + : {}; + const report = reports && reports[normalizedId]; + if (!report) { + return false; + } + + if ( + Boolean(report.pinned) + && typeof runtime.setDelayedMemoryReportPinned === "function" + ) { + runtime.setDelayedMemoryReportPinned(normalizedId, false); + } + + if ( + typeof runtime.isDelayedMemoryReportLoaded === "function" + && runtime.isDelayedMemoryReportLoaded(normalizedId) + && typeof runtime.markDelayedMemoryReportLoaded === "function" + ) { + runtime.markDelayedMemoryReportLoaded( + normalizedId, + false, + { + sync: true, + suppressNextTurn: true, + } + ); + } + + return true; +} + +function renderAttachedDelayedMemoryPlaque() { + if (!attachedDelayedMemory) { + return; + } + + const records = getConsoleAttachedDelayedMemoryRecords(); + attachedDelayedMemory.replaceChildren(); + attachedDelayedMemory.classList.toggle( + "hidden", + records.length === 0 + ); + + if (!records.length) { + return; + } + + const title = document.createElement("div"); + title.className = "jin-attached-files-title"; + title.textContent = "[ DELAYED_MEMORY ]"; + attachedDelayedMemory.appendChild(title); + + const list = document.createElement("div"); + list.className = "jin-attached-files-list"; + + records.forEach(({reportId, report}) => { + const row = document.createElement("div"); + row.className = + "jin-attached-files-row runtime-memory-delayed-row-pinned"; + row.dataset.reportId = reportId; + + const pin = document.createElement("button"); + pin.type = "button"; + pin.className = + "delayed-memory-modal-icon-button delayed-memory-modal-pin runtime-memory-delayed-pin is-pinned"; + pin.innerHTML = delayedMemoryPlaquePinSvg(); + pin.title = `Unload delayed memory ${reportId}`; + pin.setAttribute( + "aria-label", + `Unload delayed memory ${reportId}` + ); + pin.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + unloadConsoleDelayedMemoryReport(reportId); + }); + + const name = document.createElement("span"); + name.className = "jin-attached-files-name"; + name.textContent = String(report.title || reportId); + name.title = String(report.title || reportId); + + row.append(pin, name); + list.appendChild(row); + }); + + attachedDelayedMemory.appendChild(list); +} + +window.addEventListener( + "jin:delayed-memory-store-changed", + (event) => { + renderAttachedDelayedMemoryPlaque(); + const reason = String( + event && event.detail && event.detail.reason || "" + ); + if ( + reason === "load" + || reason === "load-state" + || reason === "pin" + ) { + if (getConsoleAttachedDelayedMemoryRecords().length) { + expandConsolePanelForContextAttachment(); + } + } + } +); + +renderAttachedDelayedMemoryPlaque(); + window.JinPanels = Object.assign( window.JinPanels || {}, { collapseAllPanels, + expandConsolePanelForContextAttachment, cancelStartupCollapseAnimation, getRuntimeAvatarSnapshot, refreshCollapsedPanelHeights, diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index e6ce80c8..aeb704bf 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -1563,7 +1563,6 @@ function syncContextDelayedMemoryRow(row) { pinButton.disabled = true; pinButton.classList.remove( "delayed-memory-modal-pin-active", - "delayed-memory-modal-pin-appended", "delayed-memory-modal-pin-loaded" ); pinButton.title = @@ -1591,11 +1590,6 @@ function syncContextDelayedMemoryRow(row) { && window.JinRuntime.runtime; const pinned = Boolean(report.pinned); - const appended = - !pinned - && runtime - && typeof runtime.isDelayedMemoryReportAppended === "function" - && runtime.isDelayedMemoryReportAppended(reportId); const loaded = !pinned && runtime @@ -1611,10 +1605,6 @@ function syncContextDelayedMemoryRow(row) { "delayed-memory-modal-pin-active", pinned ); - pinButton.classList.toggle( - "delayed-memory-modal-pin-appended", - Boolean(appended) - ); pinButton.classList.toggle( "delayed-memory-modal-pin-loaded", Boolean(loaded) @@ -1634,16 +1624,12 @@ function syncContextDelayedMemoryRow(row) { ) ); pinButton.title = - appended - ? "Remove appended delayed memory from next turn" + loaded + ? "Unload delayed memory from context" : ( - loaded - ? "Unload delayed memory from context" - : ( - pinned - ? "Unpin delayed memory" - : "Pin delayed memory" - ) + pinned + ? "Unpin delayed memory" + : "Pin delayed memory" ); } diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index a9ec7828..06504052 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -1093,12 +1093,6 @@ && typeof runtime.isDelayedMemoryReportLoaded === "function" && runtime.isDelayedMemoryReportLoaded(id) ); - const appended = - Boolean( - runtime - && typeof runtime.isDelayedMemoryReportAppended === "function" - && runtime.isDelayedMemoryReportAppended(id) - ); if (!id || !title) { return null; @@ -1110,7 +1104,6 @@ summary, pinned: Boolean(report.pinned), loaded, - appended, anchorFactIds, factIds, attachmentIds: @@ -1339,10 +1332,6 @@ classNames.push("is-memory-pinned"); } - if (options.appended) { - classNames.push("is-memory-appended"); - } - if (options.contextLoaded) { classNames.push("is-context-loaded"); } @@ -1598,8 +1587,8 @@ } const pinned = Boolean(record.pinned); - const appended = Boolean(record.appended); - const active = pinned || appended; + const contextLoaded = Boolean(record.loaded); + const active = pinned || contextLoaded; const color = active ? PINNED_DELAYED_MEMORY_RING_COLOR : mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); @@ -1617,8 +1606,7 @@ : color, opacity: active ? 0.82 : 0.36, pinned, - appended, - contextLoaded: Boolean(record.loaded), + contextLoaded, avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.id), @@ -1825,7 +1813,7 @@ return true; } - function setDelayedMemoryDashPinned(reportId, pinned, appended = false) { + function setDelayedMemoryDashPinned(reportId, pinned) { const delayedMemoryId = String(reportId || "").trim().toLowerCase(); @@ -1849,10 +1837,10 @@ || DEFAULT_RING_COLOR; const nextPinned = Boolean(pinned); - const nextAppended = - Boolean(appended); + const nextContextLoaded = + dashGroup.classList.contains("is-context-loaded"); const nextActive = - nextPinned || nextAppended; + nextPinned || nextContextLoaded; const nextColor = nextActive ? PINNED_DELAYED_MEMORY_RING_COLOR @@ -1862,10 +1850,6 @@ "is-memory-pinned", nextPinned ); - dashGroup.classList.toggle( - "is-memory-appended", - nextAppended - ); setMemoryDashGlowVariables( dashGroup, nextActive diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 4e0cb11b..9b9da368 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -12,7 +12,6 @@ let deleteRuntimeMemoryLine = null; let getDelayedMemoryReports = null; let isDelayedMemoryReportLoaded = null; - let isDelayedMemoryReportAppended = null; let handleDelayedMemoryReportPinClick = null; let setDelayedMemoryReportPinned = null; let updateDelayedMemoryReportFields = null; @@ -3278,18 +3277,22 @@ pinButton.dataset.fileId = String(record.id || ""); bindPersistentFileHoverPreview(pinButton, record); bindPersistentFileAvatarHoverTarget(pinButton, row); - pinButton.addEventListener("pointerdown", (event) => event.stopPropagation()); - pinButton.addEventListener("click", (event) => { + const togglePinnedFile = (event) => { event.preventDefault(); event.stopPropagation(); if (window.JinFiles && typeof window.JinFiles.setPinned === "function") { void window.JinFiles.setPinned(record.id, !Boolean(record.pinned)); } - }); + }; + pinButton.addEventListener("pointerdown", (event) => event.stopPropagation()); + pinButton.addEventListener("click", togglePinnedFile); const separator = document.createElement("span"); separator.className = "runtime-memory-delayed-separator"; separator.textContent = "·"; + separator.title = pinButton.title; + separator.addEventListener("pointerdown", (event) => event.stopPropagation()); + separator.addEventListener("click", togglePinnedFile); const keySpan = document.createElement("span"); keySpan.className = "runtime-memory-key"; @@ -3465,35 +3468,46 @@ } ); - pinButton.addEventListener( - "click", - (event) => { - event.preventDefault(); - event.stopPropagation(); + const toggleDelayedMemoryPinned = (event) => { + event.preventDefault(); + event.stopPropagation(); - if (!reportId) { - return; - } + if (!reportId) { + return; + } - const changed = - typeof handleDelayedMemoryReportPinClick === "function" - ? handleDelayedMemoryReportPinClick(reportId) - : ( - typeof setDelayedMemoryReportPinned === "function" - ? setDelayedMemoryReportPinned( - reportId, - !Boolean(report.pinned) - ) - : false - ); - - if (!changed) { - syncDelayedMemoryPinButtonState( - pinButton, - report - ); - } + const changed = + typeof handleDelayedMemoryReportPinClick === "function" + ? handleDelayedMemoryReportPinClick(reportId) + : ( + typeof setDelayedMemoryReportPinned === "function" + ? setDelayedMemoryReportPinned( + reportId, + !Boolean(report.pinned) + ) + : false + ); + + if (!changed) { + syncDelayedMemoryPinButtonState( + pinButton, + report + ); } + }; + + pinButton.addEventListener( + "click", + toggleDelayedMemoryPinned + ); + separatorSpan.title = pinButton.title; + separatorSpan.addEventListener( + "pointerdown", + (event) => event.stopPropagation() + ); + separatorSpan.addEventListener( + "click", + toggleDelayedMemoryPinned ); row.appendChild( @@ -4539,17 +4553,6 @@ ); } - function isDelayedMemoryReportAutoAppended(report) { - const reportId = - getDelayedMemoryReportId(report); - - return Boolean( - reportId - && typeof isDelayedMemoryReportAppended === "function" - && isDelayedMemoryReportAppended(reportId) - ); - } - function syncDelayedMemoryPinButtonState( button, report @@ -4560,9 +4563,6 @@ const pinned = Boolean(report && report.pinned); - const appended = - !pinned - && isDelayedMemoryReportAutoAppended(report); const loaded = !pinned && isDelayedMemoryReportInContext(report); @@ -4571,10 +4571,6 @@ "delayed-memory-modal-pin-active", pinned ); - button.classList.toggle( - "delayed-memory-modal-pin-appended", - appended - ); button.classList.toggle( "delayed-memory-modal-pin-loaded", loaded @@ -4594,16 +4590,12 @@ ) ); button.title = - appended - ? "Remove appended delayed memory from next turn" + loaded + ? "Unload delayed memory from context" : ( - loaded - ? "Unload delayed memory from context" - : ( - pinned - ? "Unpin delayed memory" - : "Pin delayed memory" - ) + pinned + ? "Unpin delayed memory" + : "Pin delayed memory" ); } @@ -5174,6 +5166,160 @@ return text; } + function normalizeDelayedMemoryTags(value) { + const storage = + window.JinRuntime + && window.JinRuntime.storage; + + if (storage && typeof storage.normalizeDelayedMemoryTags === "function") { + return storage.normalizeDelayedMemoryTags(value); + } + + return (Array.isArray(value) ? value : [value]) + .flat(Infinity) + .map(tag => String(tag || "").trim()) + .filter(Boolean); + } + + function updateDelayedMemoryModalTags(nextTags) { + if ( + !delayedMemoryModalReport + || typeof updateDelayedMemoryReportFields !== "function" + ) { + return false; + } + + const updatedReport = + updateDelayedMemoryReportFields( + delayedMemoryModalReport._storage_key, + { + tags: normalizeDelayedMemoryTags(nextTags), + } + ); + + if ( + !updatedReport + || typeof updatedReport !== "object" + || Array.isArray(updatedReport) + ) { + return false; + } + + openDelayedMemoryReportModal(updatedReport); + return true; + } + + function appendDelayedMemoryTagField(parent, label, value) { + const tags = normalizeDelayedMemoryTags(value); + const list = document.createElement("div"); + const input = document.createElement("input"); + + list.className = + "delayed-memory-modal-value delayed-memory-modal-tags"; + + tags.forEach((tag) => { + const item = document.createElement("span"); + + item.className = + "delayed-memory-modal-tag"; + item.textContent = tag; + item.title = "Hold to remove tag"; + item.setAttribute("tabindex", "0"); + + configureRuntimeMemoryDeleteHold( + item, + () => { + updateDelayedMemoryModalTags( + tags.filter(current => current !== tag) + ); + } + ); + + list.appendChild(item); + }); + + input.type = "text"; + input.className = + "delayed-memory-modal-tag-input"; + input.setAttribute("aria-label", "Add delayed memory tag"); + input.setAttribute("autocomplete", "off"); + input.setAttribute("spellcheck", "false"); + input.placeholder = tags.length ? "" : "type tag"; + + function resizeInput() { + const length = String(input.value || input.placeholder || "").length; + input.style.width = `${Math.max(4, Math.min(length + 1, 32))}ch`; + } + + function commitInput() { + const tag = String(input.value || "") + .replace(/,+$/g, "") + .trim(); + + if (!tag) { + input.value = ""; + resizeInput(); + return false; + } + + const key = tag.toLocaleLowerCase(); + const exists = tags.some( + current => current.toLocaleLowerCase() === key + ); + + input.value = ""; + resizeInput(); + + if (exists) { + return false; + } + + return updateDelayedMemoryModalTags([ + ...tags, + tag, + ]); + } + + input.addEventListener("input", () => { + resizeInput(); + + if (String(input.value || "").includes(",")) { + commitInput(); + } + }); + + input.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== ",") { + return; + } + + event.preventDefault(); + commitInput(); + }); + + input.addEventListener("blur", () => { + commitInput(); + }); + + list.addEventListener("click", (event) => { + if ( + event.target === list + || event.target === input + ) { + input.focus(); + } + }); + + resizeInput(); + list.appendChild(input); + + appendDelayedMemoryModalFieldNode( + parent, + label, + list + ); + } + function appendDelayedMemoryFactIdField( parent, label, @@ -6075,7 +6221,7 @@ ) ); - appendDelayedMemoryModalField( + appendDelayedMemoryTagField( fields, "Tags", delayedMemoryModalReport.tags @@ -6660,8 +6806,6 @@ getDelayedMemoryReports = options.getDelayedMemoryReports || null; isDelayedMemoryReportLoaded = options.isDelayedMemoryReportLoaded || null; - isDelayedMemoryReportAppended = - options.isDelayedMemoryReportAppended || null; handleDelayedMemoryReportPinClick = options.handleDelayedMemoryReportPinClick || null; setDelayedMemoryReportPinned = options.setDelayedMemoryReportPinned || null; diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index c12718b6..e1316b44 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -619,10 +619,6 @@ typeof getLoadedDelayedMemoryReportIds === "function" ? getLoadedDelayedMemoryReportIds() : [], - appended_memory_ids: - typeof getAppendedDelayedMemoryReportIds === "function" - ? getAppendedDelayedMemoryReportIds() - : [], session_memory: sessionMemory, session_memory_updates: data.updates || 0, @@ -1197,23 +1193,27 @@ ) || 0, loaded_memory_ids: - ( - sessionMemory - && Array.isArray(sessionMemory.loaded_memory_ids) - ) - ? sessionMemory.loaded_memory_ids - .map(item => String(item || "").trim()) - .filter(Boolean) - : [], - appended_memory_ids: - ( - sessionMemory - && Array.isArray(sessionMemory.appended_memory_ids) - ) - ? sessionMemory.appended_memory_ids - .map(item => String(item || "").trim()) - .filter(Boolean) - : [], + Array.from(new Set([ + ...( + sessionMemory + && Array.isArray(sessionMemory.loaded_memory_ids) + ? sessionMemory.loaded_memory_ids + : [] + ), + ...( + (() => { + const legacyLoadKey = "append" + "ed_memory_ids"; + return ( + sessionMemory + && Array.isArray(sessionMemory[legacyLoadKey]) + ? sessionMemory[legacyLoadKey] + : [] + ); + })() + ), + ])) + .map(item => String(item || "").trim()) + .filter(Boolean), runtime_memory: runtimeText, runtime_memory_updates: ( diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 39a529db..beb2ab1a 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1235,6 +1235,83 @@ return attachmentIds; } + function normalizeDelayedMemoryTags(value) { + const source = Array.isArray(value) ? value : [value]; + const candidates = []; + const tags = []; + const seen = new Set(); + + function collect(item) { + if (Array.isArray(item)) { + item.flat(Infinity).forEach(collect); + return; + } + + const text = String(item || "").trim(); + if (!text) { + return; + } + + if (text.startsWith("[") && text.endsWith("]")) { + try { + const parsed = JSON.parse(text); + if (Array.isArray(parsed)) { + parsed.forEach(collect); + return; + } + } catch (_error) { + // Loose legacy bracket syntax is handled below. + } + } + + text.split(/[,;\r\n]+/) + .map(part => part.trim()) + .filter(Boolean) + .forEach((part) => { + const bracketed = part.startsWith("[") && part.endsWith("]"); + const hashtagCount = (part.match(/(^|\s)#/g) || []).length; + + if (bracketed) { + const inner = part.slice(1, -1).trim(); + if (inner && !/["']/.test(inner)) { + candidates.push(...inner.split(/\s+/)); + return; + } + } + + if (hashtagCount >= 2) { + candidates.push(...part.split(/\s+/)); + return; + } + + candidates.push(part); + }); + } + + source.forEach(collect); + + candidates.forEach((candidate) => { + let tag = String(candidate || "").trim(); + tag = tag.replace(/^[\[\]{}()"']+|[\[\]{}()"']+$/g, "").trim(); + tag = tag.replace(/^#+|#+$/g, "").trim(); + tag = tag.replace(/^[\[\]{}()"']+|[\[\]{}()"']+$/g, "").trim(); + + if (!tag) { + return; + } + + const key = tag.toLocaleLowerCase(); + if (seen.has(key)) { + return; + } + + seen.add(key); + tags.push(tag); + }); + + return tags; + } + function normalizeDelayedMemoryReports( value ) { @@ -1298,14 +1375,7 @@ summary: String(report.summary || "").trim(), tags: - Array.isArray(report.tags) - ? report.tags - .map(tag => String(tag || "").trim()) - .filter(Boolean) - : String(report.tags || "") - .split(",") - .map(tag => tag.trim()) - .filter(Boolean), + normalizeDelayedMemoryTags(report.tags), body: String(report.body || "").trim(), pinned: @@ -1998,6 +2068,7 @@ buildFactsMemoryContentHash, activateFactsMemorySession, removeFactsMemoryField, + normalizeDelayedMemoryTags, normalizeDelayedMemoryReports, readDelayedMemoryReports, writeDelayedMemoryReports, diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 71fdcce1..74a95980 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -175,7 +175,6 @@ const runtimeMemoryHistory = { let runtimeMemoryDisplayMode = "runtime"; let restoredSessionMemorySnapshot = null; const loadedDelayedMemoryReportIds = new Set(); -const appendedDelayedMemoryReportIds = new Set(); feedback.init({ memoryModel, @@ -580,7 +579,6 @@ session.init({ persistRuntimeMemorySnapshot, attachFirstUserIdleToInitialRuntimeSnapshot, getLoadedDelayedMemoryReportIds, - getAppendedDelayedMemoryReportIds, }); panel.init(); @@ -595,7 +593,6 @@ memoryView.init({ deleteRuntimeMemoryLine: deleteRuntimeMemoryLineAndRender, getDelayedMemoryReports: readDelayedMemoryReports, isDelayedMemoryReportLoaded, - isDelayedMemoryReportAppended, handleDelayedMemoryReportPinClick, setDelayedMemoryReportPinned, updateDelayedMemoryReportFields, @@ -728,12 +725,6 @@ function getLoadedDelayedMemoryReportIds() { .sort(); } -function getAppendedDelayedMemoryReportIds() { - return Array.from(appendedDelayedMemoryReportIds) - .filter(reportId => loadedDelayedMemoryReportIds.has(reportId)) - .sort(); -} - function replaceLoadedDelayedMemoryReportIds( reportIds, options = {} @@ -743,16 +734,11 @@ function replaceLoadedDelayedMemoryReportIds( .map(normalizeRuntimeDelayedMemoryReportId) .filter(Boolean) ); - const appendedStateChanged = - Array.from(appendedDelayedMemoryReportIds).some( - reportId => !nextIds.has(reportId) - ); const changed = nextIds.size !== loadedDelayedMemoryReportIds.size || Array.from(nextIds).some( reportId => !loadedDelayedMemoryReportIds.has(reportId) - ) - || appendedStateChanged; + ); if (!changed) { return getLoadedDelayedMemoryReportIds(); @@ -762,57 +748,6 @@ function replaceLoadedDelayedMemoryReportIds( nextIds.forEach( reportId => loadedDelayedMemoryReportIds.add(reportId) ); - Array.from(appendedDelayedMemoryReportIds).forEach( - reportId => { - if (!loadedDelayedMemoryReportIds.has(reportId)) { - appendedDelayedMemoryReportIds.delete(reportId); - } - } - ); - - if (options.render !== false) { - renderRuntimeMemorySnapshot(); - - if (!syncDelayedMemoryStateToAvatar()) { - refreshRuntimeAvatar(); - } - } - - if (appendedStateChanged) { - dispatchDelayedMemoryStoreChanged( - "append-state", - "" - ); - } - - return getLoadedDelayedMemoryReportIds(); -} - -function replaceAppendedDelayedMemoryReportIds( - reportIds, - options = {} -) { - const nextIds = new Set( - (Array.isArray(reportIds) ? reportIds : []) - .map(normalizeRuntimeDelayedMemoryReportId) - .filter( - reportId => reportId && loadedDelayedMemoryReportIds.has(reportId) - ) - ); - const changed = - nextIds.size !== appendedDelayedMemoryReportIds.size - || Array.from(nextIds).some( - reportId => !appendedDelayedMemoryReportIds.has(reportId) - ); - - if (!changed) { - return getAppendedDelayedMemoryReportIds(); - } - - appendedDelayedMemoryReportIds.clear(); - nextIds.forEach( - reportId => appendedDelayedMemoryReportIds.add(reportId) - ); if (options.render !== false) { renderRuntimeMemorySnapshot(); @@ -823,11 +758,11 @@ function replaceAppendedDelayedMemoryReportIds( } dispatchDelayedMemoryStoreChanged( - "append-state", + "load-state", "" ); - return getAppendedDelayedMemoryReportIds(); + return getLoadedDelayedMemoryReportIds(); } function isDelayedMemoryReportLoaded(reportId) { @@ -840,17 +775,6 @@ function isDelayedMemoryReportLoaded(reportId) { ); } -function isDelayedMemoryReportAppended(reportId) { - const normalizedId = - normalizeRuntimeDelayedMemoryReportId(reportId); - - return Boolean( - normalizedId - && loadedDelayedMemoryReportIds.has(normalizedId) - && appendedDelayedMemoryReportIds.has(normalizedId) - ); -} - function markDelayedMemoryReportLoaded( reportId, loaded = true, @@ -865,31 +789,15 @@ function markDelayedMemoryReportLoaded( const wasLoaded = loadedDelayedMemoryReportIds.has(normalizedId); - const wasAppended = - appendedDelayedMemoryReportIds.has(normalizedId); if (loaded) { - loadedDelayedMemoryReportIds.add( - normalizedId - ); + loadedDelayedMemoryReportIds.add(normalizedId); } else { - loadedDelayedMemoryReportIds.delete( - normalizedId - ); - appendedDelayedMemoryReportIds.delete( - normalizedId - ); + loadedDelayedMemoryReportIds.delete(normalizedId); } - const stateUnchanged = ( - wasLoaded - === loadedDelayedMemoryReportIds.has(normalizedId) - && wasAppended - === appendedDelayedMemoryReportIds.has(normalizedId) - ); - if ( - stateUnchanged + wasLoaded === loadedDelayedMemoryReportIds.has(normalizedId) && options.forceRender !== true ) { return true; @@ -901,78 +809,14 @@ function markDelayedMemoryReportLoaded( refreshRuntimeAvatar(); } - if ( - wasAppended - && !appendedDelayedMemoryReportIds.has(normalizedId) - ) { - dispatchDelayedMemoryStoreChanged( - "unappend", - normalizedId - ); - } - - if (options.sync === true) { - syncDelayedMemoryReportsToServer({ - suppressedAppendIds: - options.suppressNextTurn === true - ? [normalizedId] - : [], - }); - } - - return true; -} - -function markDelayedMemoryReportAppended( - reportId, - appended = true, - options = {} -) { - const normalizedId = - normalizeRuntimeDelayedMemoryReportId(reportId); - - if (!normalizedId) { - return false; - } - - const wasAppended = - appendedDelayedMemoryReportIds.has(normalizedId); - const wasLoaded = - loadedDelayedMemoryReportIds.has(normalizedId); - - if (appended) { - loadedDelayedMemoryReportIds.add(normalizedId); - appendedDelayedMemoryReportIds.add(normalizedId); - } else { - appendedDelayedMemoryReportIds.delete(normalizedId); - - if (options.unload !== false) { - loadedDelayedMemoryReportIds.delete(normalizedId); - } - } - - const changed = - wasAppended !== appendedDelayedMemoryReportIds.has(normalizedId) - || wasLoaded !== loadedDelayedMemoryReportIds.has(normalizedId); - - if (changed && options.render !== false) { - renderRuntimeMemorySnapshot(); - - if (!syncDelayedMemoryStateToAvatar()) { - refreshRuntimeAvatar(); - } - } - - if (changed) { - dispatchDelayedMemoryStoreChanged( - appended ? "append" : "unappend", - normalizedId - ); - } + dispatchDelayedMemoryStoreChanged( + loaded ? "load" : "unload", + normalizedId + ); if (options.sync === true) { syncDelayedMemoryReportsToServer({ - suppressedAppendIds: + suppressedAutoLoadIds: options.suppressNextTurn === true ? [normalizedId] : [], @@ -1000,40 +844,19 @@ function handleDelayedMemoryReportPinClick(reportId) { } const pinned = Boolean(report.pinned); - const appended = - isDelayedMemoryReportAppended(normalizedId); const loaded = isDelayedMemoryReportLoaded(normalizedId); if (!pinned && loaded) { - if (appended) { - markDelayedMemoryReportAppended( - normalizedId, - false, - { - unload: true, - sync: true, - suppressNextTurn: true, - } - ); - } else { - markDelayedMemoryReportLoaded( - normalizedId, - false, - { - sync: true, - suppressNextTurn: true, - } - ); - } + markDelayedMemoryReportLoaded( + normalizedId, + false, + { + sync: true, + suppressNextTurn: true, + } + ); - if (appended) { - return { - action: "unappend", - pinned: false, - reportId: normalizedId, - }; - } return { action: "unload", @@ -1393,6 +1216,26 @@ function updateDelayedMemoryReportFields( Object.prototype.hasOwnProperty.call(fields, "body") ? String(fields.body || "") : String(report.body || ""); + const normalizeTags = (value) => { + const storage = + window.JinRuntime + && window.JinRuntime.storage; + + if (storage && typeof storage.normalizeDelayedMemoryTags === "function") { + return storage.normalizeDelayedMemoryTags(value); + } + + return (Array.isArray(value) ? value : [value]) + .flat(Infinity) + .map(tag => String(tag || "").trim()) + .filter(Boolean); + }; + const currentTags = + normalizeTags(report.tags); + const nextTags = + Object.prototype.hasOwnProperty.call(fields, "tags") + ? normalizeTags(fields.tags) + : currentTags; const currentAttachmentIds = normalizeRuntimeDelayedMemoryAttachmentIds( report.attachments_ids @@ -1410,6 +1253,7 @@ function updateDelayedMemoryReportFields( ...report, title: nextTitle, summary: nextSummary, + tags: nextTags, body: nextBody, attachments_ids: nextAttachmentIds, }; @@ -2570,12 +2414,8 @@ window.JinRuntime.runtime = { getDelayedMemoryReports: readDelayedMemoryReports, getLoadedDelayedMemoryReportIds, replaceLoadedDelayedMemoryReportIds, - getAppendedDelayedMemoryReportIds, - replaceAppendedDelayedMemoryReportIds, isDelayedMemoryReportLoaded, - isDelayedMemoryReportAppended, markDelayedMemoryReportLoaded, - markDelayedMemoryReportAppended, handleDelayedMemoryReportPinClick, setDelayedMemoryReportPinned, updateDelayedMemoryReportFields, diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index c0e9cbb7..ef27effe 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -266,10 +266,14 @@ function parseDelayedMemoryReportPayload( summary: String(fields.summary || "").trim(), tags: - String(fields.tags || "") - .split(",") - .map(tag => tag.trim()) - .filter(Boolean), + window.JinRuntime + && window.JinRuntime.storage + && typeof window.JinRuntime.storage.normalizeDelayedMemoryTags === "function" + ? window.JinRuntime.storage.normalizeDelayedMemoryTags(fields.tags) + : String(fields.tags || "") + .split(",") + .map(tag => tag.trim()) + .filter(Boolean), body: String(fields.body || "").trim(), anchor_fact_ids: anchorFactIds, @@ -486,14 +490,10 @@ function syncDelayedMemoryReportsToRuntime(options = {}) { typeof window.JinRuntime.runtime.getLoadedDelayedMemoryReportIds === "function" ? window.JinRuntime.runtime.getLoadedDelayedMemoryReportIds() : []; - const appendedDelayedMemoryIds = - typeof window.JinRuntime.runtime.getAppendedDelayedMemoryReportIds === "function" - ? window.JinRuntime.runtime.getAppendedDelayedMemoryReportIds() - : []; - const suppressedAppendIds = + const suppressedAutoLoadIds = normalizeDelayedMemoryReportIds( - options.suppressedAppendIds - || options.suppressed_delayed_memory_append_ids + options.suppressedAutoLoadIds + || options.suppressed_delayed_memory_auto_load_ids || [] ); @@ -502,12 +502,11 @@ function syncDelayedMemoryReportsToRuntime(options = {}) { delayed_memory_reports: delayedMemoryReports, deleted_delayed_memory_report_ids: deletedReportIds, loaded_delayed_memory_ids: loadedDelayedMemoryIds, - appended_delayed_memory_ids: appendedDelayedMemoryIds, ...( - suppressedAppendIds.length + suppressedAutoLoadIds.length ? { - suppressed_delayed_memory_append_ids: - suppressedAppendIds, + suppressed_delayed_memory_auto_load_ids: + suppressedAutoLoadIds, } : {} ), @@ -549,14 +548,6 @@ function handleDelayedMemoryStoreSnapshot( ); } - if ( - typeof window.JinRuntime.runtime.replaceAppendedDelayedMemoryReportIds - === "function" - ) { - window.JinRuntime.runtime.replaceAppendedDelayedMemoryReportIds( - data.appended_delayed_memory_ids || [] - ); - } window.JinRuntime.runtime.replaceDelayedMemoryReports({ ...localReports, diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 62af710d..ac698ace 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -1317,10 +1317,7 @@ function handleRuntimeAction( } if ( - ( - action === "load_delayed_memory" - || action === "append_delayed_memory" - ) + action === "load_delayed_memory" && data.delayed_memory_result && data.delayed_memory_result.report && data.delayed_memory_result.id @@ -1346,8 +1343,6 @@ function handleRuntimeAction( && ( typeof window.JinRuntime.runtime.markDelayedMemoryReportLoaded === "function" - || typeof window.JinRuntime.runtime.markDelayedMemoryReportAppended - === "function" ) ) { if ( @@ -1362,28 +1357,8 @@ function handleRuntimeAction( ); } - if ( - action === "append_delayed_memory" - && typeof window.JinRuntime.runtime.markDelayedMemoryReportAppended - === "function" - ) { - window.JinRuntime.runtime.markDelayedMemoryReportAppended( - delayedMemoryPreview.reportId, - true - ); - } if ( - action === "unload_delayed_memory" - && typeof window.JinRuntime.runtime.markDelayedMemoryReportAppended - === "function" - ) { - window.JinRuntime.runtime.markDelayedMemoryReportAppended( - delayedMemoryPreview.reportId, - false, - { unload: true } - ); - } else if ( action === "unload_delayed_memory" && typeof window.JinRuntime.runtime.markDelayedMemoryReportLoaded === "function" diff --git a/ui/templates/index.html b/ui/templates/index.html index dd4a550a..05223c15 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,14 +10,14 @@ - + - + - + @@ -129,6 +129,7 @@ class="flex-1 overflow-y-auto p-4 space-y-4 text-[11px] leading-relaxed text-zinc-400"> + @@ -497,21 +498,21 @@ } | tojson }} - + - + - - - + + + - + - - + + @@ -519,11 +520,11 @@ - + - - + + diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index a90beb90..09916b42 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -248,18 +248,25 @@ def collect_anchor_fact_report_ids( def normalize_delayed_memory_tags(value) -> list[str]: + """Return delayed-memory tags in one backwards-compatible format. + + Historical reports contain a mix of proper JSON arrays, comma-separated + strings, hashtag lists and loose bracketed lists. Keep real multi-word + tags intact when they are already separate array items, while cleaning the + wrappers used by the legacy formats. + """ source = value if isinstance(value, list) else [value] candidates = [] for item in source: - if isinstance(item, list): - candidates.extend( - normalize_delayed_memory_tags(item) - ) + if isinstance(item, (list, tuple, set)): + candidates.extend(normalize_delayed_memory_tags(list(item))) continue text = str(item or "").strip() + if not text: + continue if text.startswith("[") and text.endswith("]"): try: @@ -268,26 +275,47 @@ def normalize_delayed_memory_tags(value) -> list[str]: parsed = None if isinstance(parsed, list): - candidates.extend( - normalize_delayed_memory_tags(parsed) - ) + candidates.extend(normalize_delayed_memory_tags(parsed)) continue - candidates.extend( + explicit_parts = [ part.strip() - for part in text.split(",") - ) + for part in re.split(r"[,;\r\n]+", text) + if part.strip() + ] + + for part in explicit_parts: + stripped = part.strip() + bracketed = stripped.startswith("[") and stripped.endswith("]") + hashtag_count = len(re.findall(r"(?= 2: + candidates.extend(stripped.split()) + continue + + candidates.append(stripped) tags = [] seen = set() for candidate in candidates: - tag = str(candidate or "").strip().strip( - "\"'" - ) - identity = tag.casefold() + tag = str(candidate or "").strip() + tag = tag.strip("[]{}()\"'").strip() + tag = re.sub(r"^#+", "", tag).strip() + tag = re.sub(r"#+$", "", tag).strip() + tag = tag.strip("[]{}()\"'").strip() + + if not tag: + continue - if not tag or identity in seen: + identity = tag.casefold() + if identity in seen: continue seen.add(identity) diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 13aee164..af83e882 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -1949,20 +1949,6 @@ def clear_loaded_delayed_memory_report( != normalized_report_id ] - appended_ids = getattr( - context, - "runtime_appended_delayed_memory_ids", - None, - ) - - if isinstance(appended_ids, list): - appended_ids[:] = [ - item - for item in appended_ids - if str(item or "").strip().casefold() - != normalized_report_id - ] - return True def get_delayed_memory_reports( diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index 846ebe2e..da1425ef 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -12,6 +12,7 @@ from utils.actions.save_delayed_memory_utils import ( normalize_delayed_memory_attachment_ids, normalize_delayed_memory_fact_ids, + normalize_delayed_memory_tags, ) @@ -80,14 +81,9 @@ def _clean_session_ids(value) -> list[str]: def _clean_tags(value) -> list[str]: - source = ( - value - if isinstance(value, list) - else str(value or "").split(",") - ) tags = [] - for item in source: + for item in normalize_delayed_memory_tags(value): tag = _clean_text( item, limit=MAX_DELAYED_MEMORY_TAG_CHARS, @@ -549,6 +545,7 @@ def load_delayed_memory_reports_from_files( reports = {} report_sources = {} report_has_attachments_field = {} + report_tags_need_migration = {} warnings = [] try: @@ -615,6 +612,13 @@ def load_delayed_memory_reports_from_files( if raw_is_single_report else True ) + if raw_is_single_report: + raw_tags = raw_value.get("tags", []) + report_tags_need_migration[report_id] = ( + raw_tags != report.get("tags", []) + ) + else: + report_tags_need_migration[report_id] = False if previous_source: warnings.append( @@ -627,7 +631,16 @@ def load_delayed_memory_reports_from_files( report_sources[report_id] = path.name for report_id, report in list(reports.items()): - if report_has_attachments_field.get(report_id, True): + needs_attachments_migration = not report_has_attachments_field.get( + report_id, + True, + ) + needs_tags_migration = report_tags_need_migration.get( + report_id, + False, + ) + + if not needs_attachments_migration and not needs_tags_migration: continue try: @@ -638,9 +651,16 @@ def load_delayed_memory_reports_from_files( ) report_sources[report_id] = migrated_path.name report_has_attachments_field[report_id] = True + report_tags_need_migration[report_id] = False except (OSError, TypeError, ValueError) as error: + reasons = [] + if needs_attachments_migration: + reasons.append("attachments_ids") + if needs_tags_migration: + reasons.append("tags") warnings.append( - "could not add attachments_ids to " + "could not normalize " + f"{'+'.join(reasons)} in " f"{report_sources.get(report_id, report_id)}: {error}" ) diff --git a/utils/delayed_memory_triggers.py b/utils/delayed_memory_triggers.py index 0e6af10d..a81091ab 100644 --- a/utils/delayed_memory_triggers.py +++ b/utils/delayed_memory_triggers.py @@ -4,7 +4,7 @@ import unicodedata -RUNTIME_ACTION_APPEND_DELAYED_MEMORY = "append_delayed_memory" +RUNTIME_ACTION_LOAD_DELAYED_MEMORY = "load_delayed_memory" MIN_DELAYED_MEMORY_TRIGGER_CHARS = 2 @@ -71,14 +71,14 @@ def find_delayed_memory_trigger_tag( return "" -async def append_delayed_memory_by_tags( +async def load_delayed_memory_by_tags( context, user_text: str, ) -> list[dict]: - """Append matching delayed reports before Brain sees the turn. + """Load matching delayed reports before Brain sees the turn. Delayed memory has one tag list: ``tags``. Each tag is both an index tag - and a lexical trigger hook. Auto-appended reports use the same loaded + and a lexical trigger hook. Tag-triggered reports use the exact same loaded memory state as LOAD_DELAYED_MEMORY, so Brain may later unload them with the normal UNLOAD_DELAYED_MEMORY action. """ @@ -104,15 +104,15 @@ async def append_delayed_memory_by_tags( for item in ( getattr( context, - "runtime_suppressed_delayed_memory_append_ids", + "runtime_suppressed_delayed_memory_auto_load_ids", [], ) or [] ) if str(item or "").strip() } - context.runtime_suppressed_delayed_memory_append_ids = [] - appended_results = [] + context.runtime_suppressed_delayed_memory_auto_load_ids = [] + loaded_results = [] for report_id, report in reports.items(): normalized_report_id = str( @@ -148,23 +148,12 @@ async def append_delayed_memory_by_tags( ): continue - appended_ids = getattr( - context, - "runtime_appended_delayed_memory_ids", - None, - ) - if not isinstance(appended_ids, list): - appended_ids = [] - context.runtime_appended_delayed_memory_ids = appended_ids - if normalized_report_id not in appended_ids: - appended_ids.append(normalized_report_id) - result = { **result, - "action": RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + "action": RUNTIME_ACTION_LOAD_DELAYED_MEMORY, "triggered_by_tag": trigger_tag, } - appended_results.append(result) + loaded_results.append(result) emitter = getattr( context, @@ -188,13 +177,13 @@ async def append_delayed_memory_by_tags( ) event = { "type": "runtime_action", - "action": RUNTIME_ACTION_APPEND_DELAYED_MEMORY, + "action": RUNTIME_ACTION_LOAD_DELAYED_MEMORY, "id": normalized_report_id, "status": "completed", - "display_name": "APPENDED DELAYED MEMORY", + "display_name": "LOADED DELAYED MEMORY", "close_tag": False, "text": ( - f"APPENDED DELAYED MEMORY: {title} - " + f"LOADED DELAYED MEMORY: {title} - " f'triggered_by_tag: "{trigger_tag}"' ), "detail": ( @@ -236,8 +225,8 @@ async def append_delayed_memory_by_tags( ) if log_runtime is not None: await log_runtime( - "[DELAYED MEMORY] appended by tag: " + "[DELAYED MEMORY] loaded by tag: " f"{normalized_report_id} <- {trigger_tag}" ) - return appended_results + return loaded_results diff --git a/websocket/__init__.py b/websocket/__init__.py index 338cf9dc..e4916ac9 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -35,9 +35,8 @@ redacted_message_data_for_log, ) from .bootstrap import ( - apply_appended_delayed_memory_ids, apply_delayed_memory_reports, - apply_suppressed_delayed_memory_append_ids, + apply_suppressed_delayed_memory_auto_load_ids, apply_loaded_delayed_memory_ids, apply_runtime_memory_slot_delete, apply_runtime_resume, @@ -311,11 +310,7 @@ async def process_pending_requests(): context, message_data, ) - apply_appended_delayed_memory_ids( - context, - message_data, - ) - apply_suppressed_delayed_memory_append_ids( + apply_suppressed_delayed_memory_auto_load_ids( context, message_data, ) diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 1d99afcf..0ad54eb6 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -249,88 +249,27 @@ def get_context_loaded_delayed_memory_ids( ) -def apply_appended_delayed_memory_ids( +def apply_suppressed_delayed_memory_auto_load_ids( context, message_data: dict, ) -> list[str]: - if "appended_delayed_memory_ids" in message_data: - raw_ids = message_data.get( - "appended_delayed_memory_ids", - [], - ) - elif "appended_memory_ids" in message_data: - raw_ids = message_data.get( - "appended_memory_ids", - [], - ) - else: - raw_ids = getattr( - context, - "runtime_appended_delayed_memory_ids", - [], - ) - - loaded_ids = set( - get_context_loaded_delayed_memory_ids( - context - ) - ) - appended_ids = [ - report_id - for report_id in clean_loaded_delayed_memory_report_ids( - raw_ids - ) - if report_id in loaded_ids - ] - - context.runtime_appended_delayed_memory_ids = appended_ids - - return appended_ids - - -def get_context_appended_delayed_memory_ids( - context, -) -> list[str]: - - loaded_ids = set( - get_context_loaded_delayed_memory_ids( - context - ) - ) - - return [ - report_id - for report_id in clean_loaded_delayed_memory_report_ids( - getattr( - context, - "runtime_appended_delayed_memory_ids", - [], - ) - ) - if report_id in loaded_ids - ] - - -def apply_suppressed_delayed_memory_append_ids( - context, - message_data: dict, -) -> list[str]: - - if "suppressed_delayed_memory_append_ids" not in message_data: - return clean_loaded_delayed_memory_report_ids( + # Accept the old key only as a one-way migration path. Runtime state and + # outgoing protocol use LOAD terminology exclusively. + raw_ids = message_data.get( + "suppressed_delayed_memory_auto_load_ids", + message_data.get( + "suppressed_delayed_memory_" + "append_ids", getattr( context, - "runtime_suppressed_delayed_memory_append_ids", + "runtime_suppressed_delayed_memory_auto_load_ids", [], - ) - ) + ), + ), + ) report_ids = clean_loaded_delayed_memory_report_ids( - message_data.get( - "suppressed_delayed_memory_append_ids", - [], - ) + raw_ids ) reports = getattr( context, @@ -344,7 +283,7 @@ def apply_suppressed_delayed_memory_append_ids( if report_id in reports ] - context.runtime_suppressed_delayed_memory_append_ids = report_ids + context.runtime_suppressed_delayed_memory_auto_load_ids = report_ids return report_ids @@ -1536,10 +1475,6 @@ def apply_session_bootstrap( context, message_data, ) - apply_appended_delayed_memory_ids( - context, - message_data, - ) session_memory = clean_bootstrap_memory( message_data.get( @@ -1767,11 +1702,6 @@ async def emit_delayed_memory_store_snapshot( context ) ), - "appended_delayed_memory_ids": ( - get_context_appended_delayed_memory_ids( - context - ) - ), }) diff --git a/websocket/messages.py b/websocket/messages.py index acf6371f..ee90c1a2 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -33,7 +33,7 @@ ) from utils.chat_log import append_chat_log_entry from utils.delayed_memory_triggers import ( - append_delayed_memory_by_tags, + load_delayed_memory_by_tags, ) from utils.session_actions_history import emit_session_actions_update from utils.actions import normalize_jin_size_dict @@ -1169,7 +1169,7 @@ async def process_message( ) ) context.user_message_count += 1 - await append_delayed_memory_by_tags( + await load_delayed_memory_by_tags( context, user_text, ) From 7dfede8b779736246d94077b7c0bb384b8814692 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:34:11 +0300 Subject: [PATCH 064/168] chore: ui - fix expand console on attach file - fix delayed memory load trigger by attached file - fix for inner scroll inside attachments panels and ui update --- .gitignore | 3 + agent/nodes/brain.py | 14 + app.py | 4 + logs/.gitkeep | 1 - rules/__init__.py | 2 + rules/reasoning.py | 5 + tests/test_attached_files_client_contract.py | 2 + tests/test_chat_log.py | 288 ++++++++- tests/test_delayed_memory_client_contract.py | 30 + ui/static/css/base.css | 19 +- ui/static/js/dragdrop.js | 3 +- ui/static/js/logger/log-entries.js | 6 +- ui/static/js/logger/logger.js | 63 +- ui/static/js/socket/runtime-actions.js | 66 ++ ui/templates/index.html | 10 +- utils/chat_log.py | 623 ++++++++++++++++++- utils/delayed_memory_triggers.py | 109 +++- websocket/__init__.py | 23 + websocket/attachments.py | 17 +- websocket/messages.py | 29 +- 20 files changed, 1258 insertions(+), 59 deletions(-) create mode 100644 rules/reasoning.py diff --git a/.gitignore b/.gitignore index c65c0d72..f9edbf14 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ assets/* memory/**/*.json logs/* !logs/.gitkeep +logs/**/* +!logs/**/ +!logs/**/.gitkeep # Keep empty memory directory structure !memory/**/.gitkeep diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index 93e5ffa5..6625397d 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -52,6 +52,9 @@ from utils.current_context_window import ( prepare_current_context_window_prompt, ) +from utils.chat_log import ( + save_chat_context_snapshot, +) from utils.runtime_action_abort import ( mark_runtime_action_completed, ) @@ -1442,6 +1445,17 @@ async def run_brain_stream( "preserve_runtime_action_markers": True, } + try: + save_chat_context_snapshot( + context, + context_snapshot=context_snapshot, + ) + except Exception as error: + await logger.log_system( + "[CHAT_LOG] context snapshot save failed: " + + str(error) + ) + state.visible_response_context = ( context_snapshot ) diff --git a/app.py b/app.py index 418b77dc..1c3549dc 100644 --- a/app.py +++ b/app.py @@ -35,6 +35,9 @@ from utils.urls import ( join_url, ) +from utils.chat_log import ( + migrate_legacy_chat_logs, +) from websocket import ( websocket_router, @@ -77,6 +80,7 @@ async def lifespan(application: FastAPI): ensure_files_dir() + migrate_legacy_chat_logs() # ----------------------------------------------------- # SHARED HTTP CLIENT diff --git a/logs/.gitkeep b/logs/.gitkeep index 8b137891..e69de29b 100644 --- a/logs/.gitkeep +++ b/logs/.gitkeep @@ -1 +0,0 @@ - diff --git a/rules/__init__.py b/rules/__init__.py index 801f37c5..21727ea3 100644 --- a/rules/__init__.py +++ b/rules/__init__.py @@ -6,11 +6,13 @@ from .identity import IDENTITY from .signal import LOOP_RULES +from .reasoning import REASONING_REVIEW_RULE __all__ = [ "BRAIN_RUNTIME_ACTIONS", "IDENTITY", "LOOP_RULES", + "REASONING_REVIEW_RULE", "SERVICE_AS_BRAIN_RUNTIME_ACTIONS", "build_brain_context", ] diff --git a/rules/reasoning.py b/rules/reasoning.py new file mode 100644 index 00000000..13916b72 --- /dev/null +++ b/rules/reasoning.py @@ -0,0 +1,5 @@ +"""Night-review rule for JIN reasoning traces.""" + +REASONING_REVIEW_RULE = """\ +Treat the night brain as the strongest reviewer and the daytime model's reasoning as evidence, not authority. For every saved reasoning trace, inspect the exact reasoning together with its linked final chat log and the session-context snapshot that surrounded it. Reconstruct what happened immediately before the trace and what changed during it. When an emotion or mood shift is genuinely visible, preserve both an exact quote from the reasoning and a short factual description of the preceding attempts/events that make the change legible; explicitly state the direction of the mood change (for example calm -> frustrated) and never invent an emotion that is not supported by the trace. Separately audit how JIN reasoned: what it noticed, what evidence/rules/memory it used, what it ignored or misunderstood, where the daytime model was weak or confused, whether stale/incorrect context pushed it off course, and whether the final answer/user reaction confirms or contradicts the reasoning. Prefer compact durable observations about JIN's actually observed history over personality claims written from imagination; keep provenance clear enough that every personal observation can be traced back to a real quote, context snapshot, and dialogue outcome.\ +""" diff --git a/tests/test_attached_files_client_contract.py b/tests/test_attached_files_client_contract.py index 3ba1c860..b232b1e2 100644 --- a/tests/test_attached_files_client_contract.py +++ b/tests/test_attached_files_client_contract.py @@ -67,6 +67,8 @@ def test_attached_files_plaque_reuses_attachment_modal_and_100px_image_hover(): assert "options.hoverPreviewMaxPx" in attachments assert "--jin-attachment-preview-max-px" in memory_css assert ".jin-attached-files-name.jin-attachment-bubble:hover" in base_css + assert "background: transparent;" in base_css + assert ".jin-attached-delayed-memory-name" in base_css def test_files_panel_shows_file_count_and_reuses_100px_image_hover_preview(): runtime_view = (ROOT / "ui/static/js/runtime/runtime-memory-view.js").read_text(encoding="utf-8") diff --git a/tests/test_chat_log.py b/tests/test_chat_log.py index a0af0ca7..5e28fc01 100644 --- a/tests/test_chat_log.py +++ b/tests/test_chat_log.py @@ -10,6 +10,9 @@ append_chat_log_entry, build_chat_log_entry, extract_active_memory_ids, + migrate_legacy_chat_logs, + save_chat_context_snapshot, + save_turn_reasoning, summarize_attachments, ) @@ -155,12 +158,26 @@ def test_append_chat_log_entry_uses_date_session_directory(self): ) self.assertEqual( first_path.parent.name, - "2026-08-12-tab_one", + "tab_one", + ) + self.assertEqual( + first_path.parent.parent.name, + "2026-08-12", ) self.assertEqual( first_path.name, "165949.jsonl", ) + self.assertTrue( + (first_path.parent / "reasoning").is_dir() + ) + self.assertFalse( + ( + first_path.parent + / "reasoning" + / ".gitkeep" + ).exists() + ) entries = [ json.loads(line) for line in first_path.read_text( @@ -196,6 +213,275 @@ def test_append_chat_log_entry_uses_date_session_directory(self): ], ) + def test_context_snapshot_is_saved_beside_dialog_and_overwritten(self): + + context = SimpleNamespace( + session_id="session-a", + runtime_turn_counter=1, + runtime_current_turn_id="turn_000001", + ) + now = datetime( + 2026, + 8, + 13, + 14, + 33, + 31, + tzinfo=timezone.utc, + ) + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + first = save_chat_context_snapshot( + context, + context_snapshot={ + "system_prompt": "PRIVATE RULES", + "visible_system_prompt": "VISIBLE CONTEXT", + "hide_internal_action_rules": True, + "user_prompt": "first user payload", + }, + now=now, + root=root, + ) + second = save_chat_context_snapshot( + context, + context_snapshot={ + "system_prompt": "PRIVATE RULES 2", + "visible_system_prompt": "VISIBLE CONTEXT 2", + "hide_internal_action_rules": True, + "user_prompt": "latest user payload", + }, + now=now, + root=root, + ) + + self.assertEqual(first, second) + self.assertEqual( + first, + root / "2026-08-13" / "session-a" / "143331.txt", + ) + saved = first.read_text(encoding="utf-8") + + self.assertIn("VISIBLE CONTEXT 2", saved) + self.assertIn("latest user payload", saved) + self.assertNotIn("PRIVATE RULES 2", saved) + self.assertNotIn("first user payload", saved) + + def test_reasoning_trace_links_back_to_dialog_and_jin_log_entry(self): + + context = SimpleNamespace( + session_id="5fa84aef-0537-4e56-9cb3-3d341bc7b93e", + runtime_turn_counter=3, + runtime_current_turn_id="turn_000003", + ) + now = datetime( + 2026, + 8, + 13, + 14, + 33, + 31, + tzinfo=timezone.utc, + ) + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + save_chat_context_snapshot( + context, + system_prompt="system context", + user_prompt="user payload", + now=now, + root=root, + ) + reasoning_path = save_turn_reasoning( + context, + "Tried again. Ugh, I keep miscounting. Switched strategy.", + now=now, + root=root, + ) + dialog_path = append_chat_log_entry( + context, + role="jin", + text="done", + now=now, + root=root, + ) + reasoning_text = reasoning_path.read_text(encoding="utf-8") + entry = json.loads( + dialog_path.read_text(encoding="utf-8").splitlines()[-1] + ) + + self.assertEqual( + reasoning_path.parent.name, + "reasoning", + ) + self.assertEqual( + reasoning_path.parent.parent.name, + "5fa84aef-0537-4e56-9cb3-3d341bc7b93e", + ) + self.assertEqual( + reasoning_path.parent.parent.parent.name, + "2026-08-13", + ) + self.assertIn("dialog_path:", reasoning_text) + self.assertIn("143331.jsonl", reasoning_text) + self.assertIn("Ugh, I keep miscounting", reasoning_text) + self.assertIn("reasoning_path", entry) + self.assertIn("context_path", entry) + self.assertIn("dialog_path", entry) + + def test_legacy_log_directories_are_collapsed_under_date(self): + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + first_legacy = ( + root + / "2026-08-13-5fa84aef-0537-4e56-9cb3-3d341bc7b93e" + ) + second_legacy = ( + root + / "2026-08-13-session-two" + ) + first_legacy.mkdir(parents=True) + second_legacy.mkdir(parents=True) + (first_legacy / "143331.jsonl").write_text( + "first\n", + encoding="utf-8", + ) + (second_legacy / "170000.jsonl").write_text( + "second\n", + encoding="utf-8", + ) + + moved = migrate_legacy_chat_logs( + root=root + ) + + self.assertEqual(len(moved), 2) + self.assertFalse(first_legacy.exists()) + self.assertFalse(second_legacy.exists()) + self.assertEqual( + ( + root + / "2026-08-13" + / "5fa84aef-0537-4e56-9cb3-3d341bc7b93e" + / "143331.jsonl" + ).read_text(encoding="utf-8"), + "first\n", + ) + self.assertEqual( + ( + root + / "2026-08-13" + / "session-two" + / "170000.jsonl" + ).read_text(encoding="utf-8"), + "second\n", + ) + reasoning_directory = ( + root + / "2026-08-13" + / "5fa84aef-0537-4e56-9cb3-3d341bc7b93e" + / "reasoning" + ) + self.assertTrue( + reasoning_directory.is_dir() + ) + self.assertFalse( + (reasoning_directory / ".gitkeep").exists() + ) + resumed_context = SimpleNamespace( + session_id="5fa84aef-0537-4e56-9cb3-3d341bc7b93e", + ) + resumed_context_path = save_chat_context_snapshot( + resumed_context, + system_prompt="restored current JIN context", + now=datetime( + 2026, + 8, + 13, + 18, + 0, + 0, + tzinfo=timezone.utc, + ), + root=root, + ) + self.assertEqual( + resumed_context_path.name, + "143331.txt", + ) + self.assertTrue( + resumed_context_path.with_suffix(".jsonl").exists() + ) + self.assertEqual( + migrate_legacy_chat_logs(root=root), + [], + ) + + def test_wrong_date_reasoning_directory_is_folded_into_sessions(self): + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + date_directory = root / "2026-08-15" + session_id = "f78fce15-8933-495f-b304-8ac27d0f87cb" + session_directory = date_directory / session_id + wrong_reasoning = date_directory / "reasoning" + session_directory.mkdir(parents=True) + wrong_reasoning.mkdir(parents=True) + (session_directory / "193900.jsonl").write_text( + "dialog\n", + encoding="utf-8", + ) + source = ( + wrong_reasoning + / f"{session_id}_193900_turn_000003.txt" + ) + source.write_text( + "\n".join([ + "captured_at: 2026-08-15T19:39:00+03:00", + f"session_id: {session_id}", + "turn: 3", + "turn_id: turn_000003", + "", + "--- REASONING ---", + "Ugh, I keep miscounting.", + ]), + encoding="utf-8", + ) + (wrong_reasoning / ".gitkeep").touch() + + moved = migrate_legacy_chat_logs( + root=root + ) + target = ( + session_directory + / "reasoning" + / "193900_turn_000003.txt" + ) + + self.assertEqual( + moved, + [(source, target)], + ) + self.assertFalse( + wrong_reasoning.exists() + ) + self.assertTrue( + target.exists() + ) + self.assertIn( + "Ugh, I keep miscounting.", + target.read_text(encoding="utf-8"), + ) + self.assertFalse( + ( + session_directory + / "reasoning" + / ".gitkeep" + ).exists() + ) + def test_build_chat_log_entry_includes_empty_arrays(self): entry = build_chat_log_entry( diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 1b5db4b0..48f2aa9c 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -178,6 +178,36 @@ def test_delayed_memory_delete_sync_and_loaded_state_are_registered(self): runtime_actions_source, ) + def test_console_loaded_delayed_memory_titles_open_report_modal(self): + + logger_source = ( + ROOT / "ui" / "static" / "js" / "logger" / "logger.js" + ).read_text(encoding="utf-8") + index_source = ( + ROOT / "ui" / "templates" / "index.html" + ).read_text(encoding="utf-8") + + self.assertIn( + "function openConsoleDelayedMemoryReport(report)", + logger_source, + ) + self.assertIn( + "memoryView.openDelayedMemoryReportModal(report)", + logger_source, + ) + self.assertIn( + '"jin-attached-files-name jin-attached-delayed-memory-name"', + logger_source, + ) + self.assertIn( + 'name.addEventListener("click", openReport)', + logger_source, + ) + self.assertIn( + '/static/js/logger/logger.js?v=delayed-context-plaque-3', + index_source, + ) + def test_context_loaded_delayed_memory_highlights_report_and_linked_l4(self): runtime_source = ( diff --git a/ui/static/css/base.css b/ui/static/css/base.css index 6b4486a7..a5646b5f 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -680,8 +680,8 @@ main.panels-collapsed-2 #scene-shade { #attached-files { flex: 0 0 auto; margin: 0 8px 8px; - max-height: 136px; - overflow: auto; + max-height: none; + overflow: visible; border: 1px solid rgba(63, 63, 70, 0.78); border-radius: 6px; background: rgba(9, 9, 11, 0.78); @@ -736,10 +736,23 @@ main.panels-collapsed-2 #scene-shade { } .jin-attached-files-name.jin-attachment-bubble:hover { - background: rgba(255, 255, 255, 0.045); + background: transparent; + color: rgba(228, 228, 231, 0.92); +} + +.jin-attached-delayed-memory-name { + cursor: pointer; + transition: color 120ms ease; +} + +.jin-attached-delayed-memory-name:hover { color: rgba(228, 228, 231, 0.92); } +.jin-attached-delayed-memory-name:focus-visible { + outline: 1px solid rgba(161, 161, 170, 0.48); + outline-offset: 1px; +} .jin-attached-files-row.jin-attached-files-row-avatar-hover, .jin-attached-files-row:hover { diff --git a/ui/static/js/dragdrop.js b/ui/static/js/dragdrop.js index f241f1a2..6986f408 100644 --- a/ui/static/js/dragdrop.js +++ b/ui/static/js/dragdrop.js @@ -168,8 +168,7 @@ async function uploadFile(file) { dispatchStoreChanged(); syncAttachmentContext(); if ( - pinned - && window.JinPanels + window.JinPanels && typeof window.JinPanels.expandConsolePanelForContextAttachment === "function" ) { window.JinPanels.expandConsolePanelForContextAttachment(); diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 5c84aacb..00656d9e 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -371,13 +371,15 @@ function getInternalActionLogKey( normalizeInternalActionName( actionName ); - const keepSkillMarkerSeparate = [ + const keepActionInstanceSeparate = [ "LOAD_SKILL", "LOAD_SKILLS", "UNLOAD_SKILL", "UNLOAD_SKILLS", + "LOAD_DELAYED_MEMORY", + "UNLOAD_DELAYED_MEMORY", ].includes(normalizedActionName); - const instanceKey = keepSkillMarkerSeparate + const instanceKey = keepActionInstanceSeparate ? String( data.id || data.runtime_action_id diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 004364e6..5522388e 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -3110,10 +3110,19 @@ if (memoryPanel && typeof MutationObserver !== "undefined") { syncCollapsedPanelBodies(); function expandConsolePanelForContextAttachment() { - if ( - !consolePanel - || !consolePanel.classList.contains("panel-collapsed") - ) { + if (!consolePanel) { + return false; + } + + const needsExpand = + consolePanel.classList.contains("panel-collapsed") + || Boolean(consolePanel.dataset.expandedHeight); + + // An attachment is explicit activity: do not let an in-flight startup + // collapse finish after the file arrives and fold the console back up. + finishStartupCollapseAnimation(); + + if (!needsExpand) { return false; } @@ -3127,7 +3136,9 @@ function delayedMemoryPlaquePinSvg() { } function getConsoleAttachedDelayedMemoryRecords() { - const runtime = window.JinRuntime; + const runtime = + window.JinRuntime + && window.JinRuntime.runtime; if ( !runtime || typeof runtime.getDelayedMemoryReports !== "function" @@ -3168,7 +3179,9 @@ function getConsoleAttachedDelayedMemoryRecords() { } function unloadConsoleDelayedMemoryReport(reportId) { - const runtime = window.JinRuntime; + const runtime = + window.JinRuntime + && window.JinRuntime.runtime; const normalizedId = String(reportId || "").trim().toLowerCase(); if (!runtime || !normalizedId) { return false; @@ -3208,6 +3221,23 @@ function unloadConsoleDelayedMemoryReport(reportId) { return true; } +function openConsoleDelayedMemoryReport(report) { + const memoryView = + window.JinRuntime + && window.JinRuntime.memoryView; + + if ( + !report + || !memoryView + || typeof memoryView.openDelayedMemoryReportModal !== "function" + ) { + return false; + } + + memoryView.openDelayedMemoryReportModal(report); + return true; +} + function renderAttachedDelayedMemoryPlaque() { if (!attachedDelayedMemory) { return; @@ -3226,7 +3256,7 @@ function renderAttachedDelayedMemoryPlaque() { const title = document.createElement("div"); title.className = "jin-attached-files-title"; - title.textContent = "[ DELAYED_MEMORY ]"; + title.textContent = "[ LOADED_DELAYED_MEMORY ]"; attachedDelayedMemory.appendChild(title); const list = document.createElement("div"); @@ -3255,9 +3285,26 @@ function renderAttachedDelayedMemoryPlaque() { }); const name = document.createElement("span"); - name.className = "jin-attached-files-name"; + name.className = + "jin-attached-files-name jin-attached-delayed-memory-name"; name.textContent = String(report.title || reportId); name.title = String(report.title || reportId); + name.setAttribute("role", "button"); + name.tabIndex = 0; + + const openReport = (event) => { + event.preventDefault(); + event.stopPropagation(); + openConsoleDelayedMemoryReport(report); + }; + + name.addEventListener("click", openReport); + name.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") { + return; + } + openReport(event); + }); row.append(pin, name); list.appendChild(row); diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index ac698ace..8649d48a 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -732,6 +732,60 @@ function getDelayedMemoryRuntimeActionPreview( } +function getDelayedMemoryTriggeredByTags( + data +) { + + const values = []; + const seen = new Set(); + const push = function (value) { + const tag = String(value || "").trim(); + const key = tag.toLocaleLowerCase(); + + if (!tag || seen.has(key)) { + return; + } + + seen.add(key); + values.push(tag); + }; + + if ( + data + && Array.isArray(data.triggered_by_tags) + ) { + data.triggered_by_tags.forEach(push); + } + + if (data) { + push(data.triggered_by_tag); + } + + return values; + +} + +function formatDelayedMemoryTriggeredByTags( + data +) { + + const tags = + getDelayedMemoryTriggeredByTags(data); + + if (!tags.length) { + return ""; + } + + const rendered = tags + .map((tag) => `"${tag.replaceAll('"', '\\\"')}"`) + .join(", "); + + return tags.length === 1 + ? `triggered_by_tag: ${rendered}` + : `triggered_by_tags: ${rendered}`; + +} + function handleRuntimeAction( data ) { @@ -851,12 +905,24 @@ function handleRuntimeAction( } ); + const delayedMemoryTriggerDetail = + action === "load_delayed_memory" + ? formatDelayedMemoryTriggeredByTags( + data + ) + : ""; + const displayText = reportScopedDelayedAction && delayedMemoryPreview.title ? ( `${getRuntimeActionDisplayName(data, action)}: ` + delayedMemoryPreview.title + + ( + delayedMemoryTriggerDetail + ? ` - ${delayedMemoryTriggerDetail}` + : "" + ) ) : baseDisplayText; diff --git a/ui/templates/index.html b/ui/templates/index.html index 05223c15..a9c19b7a 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,7 +10,7 @@ - + @@ -509,9 +509,9 @@ - + - + @@ -520,11 +520,11 @@ - + - + diff --git a/utils/chat_log.py b/utils/chat_log.py index c8810753..dc2f133e 100644 --- a/utils/chat_log.py +++ b/utils/chat_log.py @@ -1,6 +1,7 @@ import json import re import secrets +import shutil from datetime import datetime from pathlib import Path @@ -15,6 +16,9 @@ CHAT_LOG_SESSION_ID_RE = re.compile( r"[^a-zA-Z0-9_.-]" ) +LEGACY_CHAT_LOG_DIR_RE = re.compile( + r"^(?P\d{4}-\d{2}-\d{2})-(?P.+)$" +) ACTIVE_MEMORY_ID_SUFFIX_RE = re.compile( r"\[\s*active_memory_id\s*:\s*([^\]\s]+)\s*\]", re.IGNORECASE, @@ -90,6 +94,44 @@ def _context_session_id( return generated +def _public_project_path( + path: Path | str, +) -> str: + + resolved = Path(path).resolve() + + try: + relative = resolved.relative_to( + PROJECT_ROOT.resolve() + ) + except ValueError: + return resolved.as_posix() + + return "/" + relative.as_posix().lstrip("/") + + +def _ensure_reasoning_directory( + session_directory: Path, +) -> Path: + + reasoning_directory = session_directory / "reasoning" + reasoning_directory.mkdir( + parents=True, + exist_ok=True, + ) + + stale_gitkeep = reasoning_directory / ".gitkeep" + + try: + stale_gitkeep.unlink() + except FileNotFoundError: + pass + except OSError: + pass + + return reasoning_directory + + def get_chat_log_path( context, *, @@ -120,17 +162,560 @@ def get_chat_log_path( session_id = _context_session_id( context ) - directory = root_path / ( - f"{timestamp:%Y-%m-%d}-{session_id}" + date_directory = root_path / f"{timestamp:%Y-%m-%d}" + directory = date_directory / session_id + existing_logs = sorted( + path + for path in directory.glob( + "*.jsonl" + ) + if path.is_file() + ) + path = ( + existing_logs[-1] + if existing_logs + else directory / f"{timestamp:%H%M%S}.jsonl" ) - path = directory / f"{timestamp:%H%M%S}.jsonl" context.runtime_chat_log_path = str( path ) + _ensure_reasoning_directory( + directory + ) + return path +def get_chat_context_path( + context, + *, + now: datetime | None = None, + root: Path | str | None = None, +) -> Path: + + return get_chat_log_path( + context, + now=now, + root=root, + ).with_suffix( + ".txt" + ) + + +def _context_snapshot_text( + context_snapshot: dict | None = None, + *, + system_prompt: str = "", + user_prompt: str = "", +) -> str: + + snapshot = ( + context_snapshot + if isinstance( + context_snapshot, + dict, + ) + else {} + ) + + if snapshot: + hidden = bool( + snapshot.get( + "hide_internal_action_rules" + ) + ) + visible_system_prompt = str( + snapshot.get( + "visible_system_prompt", + "", + ) + or "" + ) + raw_system_prompt = str( + snapshot.get( + "system_prompt", + "", + ) + or "" + ) + system_prompt = ( + visible_system_prompt + if hidden and visible_system_prompt + else raw_system_prompt + ) + user_prompt = str( + snapshot.get( + "user_prompt", + "", + ) + or "" + ) + + return "\n\n".join( + str(part or "").strip() + for part in ( + system_prompt, + user_prompt, + ) + if str(part or "").strip() + ).strip() + + +def save_chat_context_snapshot( + context, + *, + context_snapshot: dict | None = None, + system_prompt: str = "", + user_prompt: str = "", + now: datetime | None = None, + root: Path | str | None = None, +) -> Path | None: + + if not chat_logging_enabled(): + return None + + text = _context_snapshot_text( + context_snapshot, + system_prompt=system_prompt, + user_prompt=user_prompt, + ) + + if not text: + return None + + path = get_chat_context_path( + context, + now=now, + root=root, + ) + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + path.write_text( + text + "\n", + encoding="utf-8", + newline="\n", + ) + context.runtime_chat_context_path = str( + path + ) + + return path + + +def save_current_runtime_context_snapshot( + context, + *, + user_prompt: str = "", + now: datetime | None = None, + root: Path | str | None = None, +) -> Path | None: + + if not chat_logging_enabled(): + return None + + from rules.brain_context_builder import ( + build_brain_context, + ) + from utils.brain_client_utils import ( + get_brain_runtime_config, + ) + + brain_runtime = get_brain_runtime_config() + system_prompt = build_brain_context( + context, + brain_runtime.get( + "runtime_actions", + {}, + ), + user_input=str( + user_prompt + or "" + ), + ) + + return save_chat_context_snapshot( + context, + system_prompt=system_prompt, + user_prompt=user_prompt, + now=now, + root=root, + ) + + +def save_turn_reasoning( + context, + reasoning: str, + *, + now: datetime | None = None, + root: Path | str | None = None, +) -> Path | None: + + if not chat_logging_enabled(): + return None + + cleaned_reasoning = str( + reasoning + or "" + ).strip() + + if not cleaned_reasoning: + return None + + timestamp = now or _now() + chat_log_path = get_chat_log_path( + context, + now=timestamp, + root=root, + ) + reasoning_directory = _ensure_reasoning_directory( + chat_log_path.parent + ) + session_id = _context_session_id( + context + ) + turn_id = _clean_session_id( + getattr( + context, + "runtime_current_turn_id", + "", + ) + ) or f"turn_{int(getattr(context, 'runtime_turn_counter', 0) or 0):06d}" + reasoning_path = ( + reasoning_directory + / f"{chat_log_path.stem}_{turn_id}.txt" + ) + context_path = chat_log_path.with_suffix( + ".txt" + ) + reasoning_path.write_text( + "\n".join([ + f"captured_at: {timestamp.isoformat(timespec='seconds')}", + f"session_id: {session_id}", + f"turn: {int(getattr(context, 'runtime_turn_counter', 0) or 0)}", + f"turn_id: {getattr(context, 'runtime_current_turn_id', '') or ''}", + f"dialog_path: {_public_project_path(chat_log_path)}", + f"context_path: {_public_project_path(context_path)}", + "", + "--- REASONING ---", + cleaned_reasoning, + "", + ]), + encoding="utf-8", + newline="\n", + ) + context.runtime_turn_reasoning_log_path = str( + reasoning_path + ) + + return reasoning_path + + +def _merge_directory_contents( + source: Path, + target: Path, +) -> None: + + target.mkdir( + parents=True, + exist_ok=True, + ) + + for source_item in sorted( + source.iterdir(), + key=lambda item: item.name, + ): + target_item = target / source_item.name + + if source_item.is_dir(): + _merge_directory_contents( + source_item, + target_item, + ) + try: + source_item.rmdir() + except OSError: + pass + continue + + if not target_item.exists(): + shutil.move( + str(source_item), + str(target_item), + ) + continue + + try: + if source_item.read_bytes() == target_item.read_bytes(): + source_item.unlink() + continue + except OSError: + pass + + suffix_index = 1 + + while True: + candidate = target / ( + f"{target_item.stem}.migrated-{suffix_index}" + f"{target_item.suffix}" + ) + + if not candidate.exists(): + shutil.move( + str(source_item), + str(candidate), + ) + break + + suffix_index += 1 + + +def _move_file_without_overwrite( + source: Path, + target: Path, +) -> Path: + + target.parent.mkdir( + parents=True, + exist_ok=True, + ) + + if not target.exists(): + shutil.move( + str(source), + str(target), + ) + return target + + try: + if source.read_bytes() == target.read_bytes(): + source.unlink() + return target + except OSError: + pass + + suffix_index = 1 + + while True: + candidate = target.with_name( + f"{target.stem}.migrated-{suffix_index}{target.suffix}" + ) + + if not candidate.exists(): + shutil.move( + str(source), + str(candidate), + ) + return candidate + + suffix_index += 1 + + +def _reasoning_session_id( + path: Path, + date_directory: Path, +) -> str: + + try: + head = path.read_text( + encoding="utf-8", + errors="replace", + )[:4096] + except OSError: + head = "" + + match = re.search( + r"(?m)^session_id:\s*(.+?)\s*$", + head, + ) + + if match: + session_id = _clean_session_id( + match.group(1) + ) + + if session_id: + return session_id + + session_directories = sorted( + ( + item + for item in date_directory.iterdir() + if item.is_dir() + and item.name != "reasoning" + ), + key=lambda item: len(item.name), + reverse=True, + ) + + for session_directory in session_directories: + if path.name.startswith( + session_directory.name + "_" + ): + return session_directory.name + + return "" + + +def _migrate_date_reasoning_directory( + date_directory: Path, +) -> list[tuple[Path, Path]]: + + source_directory = date_directory / "reasoning" + + if not source_directory.is_dir(): + return [] + + moved: list[tuple[Path, Path]] = [] + + for source in sorted( + source_directory.iterdir(), + key=lambda item: item.name, + ): + if not source.is_file(): + continue + + if source.name == ".gitkeep": + try: + source.unlink() + except OSError: + pass + continue + + session_id = _reasoning_session_id( + source, + date_directory, + ) + + if not session_id: + continue + + target_directory = _ensure_reasoning_directory( + date_directory / session_id + ) + prefix = session_id + "_" + target_name = ( + source.name[len(prefix):] + if source.name.startswith(prefix) + else source.name + ) + target = _move_file_without_overwrite( + source, + target_directory / target_name, + ) + moved.append( + ( + source, + target, + ) + ) + + try: + source_directory.rmdir() + except OSError: + pass + + return moved + + +def migrate_legacy_chat_logs( + *, + root: Path | str | None = None, +) -> list[tuple[Path, Path]]: + + if not chat_logging_enabled(): + return [] + + root_path = Path( + root + if root is not None + else CHAT_LOG_ROOT + ) + root_path.mkdir( + parents=True, + exist_ok=True, + ) + moved: list[tuple[Path, Path]] = [] + + for source in sorted( + root_path.iterdir(), + key=lambda item: item.name, + ): + if not source.is_dir(): + continue + + match = LEGACY_CHAT_LOG_DIR_RE.fullmatch( + source.name + ) + + if not match: + continue + + session_id = _clean_session_id( + match.group( + "session" + ) + ) + + if not session_id: + continue + + date_directory = root_path / match.group( + "date" + ) + target = date_directory / session_id + + if target.exists(): + _merge_directory_contents( + source, + target, + ) + try: + source.rmdir() + except OSError: + pass + else: + date_directory.mkdir( + parents=True, + exist_ok=True, + ) + shutil.move( + str(source), + str(target), + ) + + _ensure_reasoning_directory( + target + ) + moved.append( + ( + source, + target, + ) + ) + + for date_directory in sorted( + root_path.iterdir(), + key=lambda item: item.name, + ): + if ( + date_directory.is_dir() + and re.fullmatch( + r"\d{4}-\d{2}-\d{2}", + date_directory.name, + ) + ): + moved.extend( + _migrate_date_reasoning_directory( + date_directory + ) + ) + + return moved + + def _clean_string_list( value, ) -> list[str]: @@ -387,6 +972,38 @@ def append_chat_log_entry( text=text, now=timestamp, ) + entry["dialog_path"] = _public_project_path( + path + ) + context_path = get_chat_context_path( + context, + now=timestamp, + root=root, + ) + + if context_path.exists(): + entry["context_path"] = _public_project_path( + context_path + ) + + reasoning_path = str( + getattr( + context, + "runtime_turn_reasoning_log_path", + "", + ) + or "" + ).strip() + + if ( + str(role or "").casefold() == "jin" + and reasoning_path + and Path(reasoning_path).exists() + ): + entry["reasoning_path"] = _public_project_path( + Path(reasoning_path) + ) + path.parent.mkdir( parents=True, exist_ok=True, diff --git a/utils/delayed_memory_triggers.py b/utils/delayed_memory_triggers.py index a81091ab..a4827e3e 100644 --- a/utils/delayed_memory_triggers.py +++ b/utils/delayed_memory_triggers.py @@ -42,12 +42,12 @@ def delayed_memory_trigger_matches( ) -def find_delayed_memory_trigger_tag( +def find_delayed_memory_trigger_tags( user_text: str, report: dict, -) -> str: +) -> list[str]: if not isinstance(report, dict): - return "" + return [] tags = report.get( "tags", @@ -57,18 +57,78 @@ def find_delayed_memory_trigger_tag( if not isinstance(tags, list): tags = [tags] + matched_tags = [] + seen_tags = set() + for tag in tags: cleaned_tag = str( tag or "" ).strip() + normalized_tag = normalize_delayed_memory_trigger_text( + cleaned_tag + ) - if delayed_memory_trigger_matches( - user_text, - cleaned_tag, + if ( + normalized_tag + and normalized_tag not in seen_tags + and delayed_memory_trigger_matches( + user_text, + cleaned_tag, + ) ): - return cleaned_tag + seen_tags.add( + normalized_tag + ) + matched_tags.append( + cleaned_tag + ) + + return matched_tags + + +def find_delayed_memory_trigger_tag( + user_text: str, + report: dict, +) -> str: + matched_tags = find_delayed_memory_trigger_tags( + user_text, + report, + ) + + return ( + matched_tags[0] + if matched_tags + else "" + ) + + +def format_delayed_memory_trigger_detail( + trigger_tags: list[str], +) -> str: + cleaned_tags = [ + str(tag or "").strip() + for tag in trigger_tags + if str(tag or "").strip() + ] + + if not cleaned_tags: + return "" + + quoted_tags = [ + '"' + tag.replace('"', '\\\"') + '"' + for tag in cleaned_tags + ] - return "" + if len(quoted_tags) == 1: + return ( + "triggered_by_tag: " + + quoted_tags[0] + ) + + return ( + "triggered_by_tags: " + + ", ".join(quoted_tags) + ) async def load_delayed_memory_by_tags( @@ -126,14 +186,19 @@ async def load_delayed_memory_by_tags( ): continue - trigger_tag = find_delayed_memory_trigger_tag( + trigger_tags = find_delayed_memory_trigger_tags( user_text, report, ) - if not trigger_tag: + if not trigger_tags: continue + trigger_tag = trigger_tags[0] + trigger_detail = format_delayed_memory_trigger_detail( + trigger_tags + ) + result = load_delayed_memory_report( context, normalized_report_id, @@ -152,6 +217,7 @@ async def load_delayed_memory_by_tags( **result, "action": RUNTIME_ACTION_LOAD_DELAYED_MEMORY, "triggered_by_tag": trigger_tag, + "triggered_by_tags": trigger_tags, } loaded_results.append(result) @@ -184,12 +250,11 @@ async def load_delayed_memory_by_tags( "close_tag": False, "text": ( f"LOADED DELAYED MEMORY: {title} - " - f'triggered_by_tag: "{trigger_tag}"' - ), - "detail": ( - f'triggered_by_tag: "{trigger_tag}"' + f"{trigger_detail}" ), + "detail": trigger_detail, "triggered_by_tag": trigger_tag, + "triggered_by_tags": trigger_tags, "delayed_memory_result": result, "delayed_memory_report_id": normalized_report_id, } @@ -213,20 +278,4 @@ async def load_delayed_memory_by_tags( await emit(event) - logger = getattr( - context, - "logger", - None, - ) - log_runtime = getattr( - logger, - "log_runtime", - None, - ) - if log_runtime is not None: - await log_runtime( - "[DELAYED MEMORY] loaded by tag: " - f"{normalized_report_id} <- {trigger_tag}" - ) - return loaded_results diff --git a/websocket/__init__.py b/websocket/__init__.py index e4916ac9..8749bb86 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -74,6 +74,9 @@ public_file_snapshot, sync_pinned_file_ids, ) +from utils.chat_log import ( + save_current_runtime_context_snapshot, +) websocket_router = APIRouter() @@ -252,6 +255,16 @@ async def process_pending_requests(): "[WS] runtime resumed from browser memory" ) + try: + save_current_runtime_context_snapshot( + context + ) + except Exception as error: + await logger.log_system( + "[CHAT_LOG] resumed context snapshot save failed: " + + str(error) + ) + if message_data.get( "emit_after_restore" ): @@ -468,6 +481,16 @@ async def process_pending_requests(): "[WS] browser session memory restored" ) + try: + save_current_runtime_context_snapshot( + context + ) + except Exception as error: + await logger.log_system( + "[CHAT_LOG] bootstrap context snapshot save failed: " + + str(error) + ) + await emit_current_runtime_memory( context ) diff --git a/websocket/attachments.py b/websocket/attachments.py index 6e68af94..698a5396 100644 --- a/websocket/attachments.py +++ b/websocket/attachments.py @@ -285,17 +285,30 @@ def redacted_message_data_for_log( return redacted -def build_user_text_with_attachments( +def get_message_user_text( message_data: dict, ) -> str: - user_text = str( + if not isinstance(message_data, dict): + return "" + + return str( message_data.get( "text", "", ) + or "" ).strip() + +def build_user_text_with_attachments( + message_data: dict, +) -> str: + + user_text = get_message_user_text( + message_data + ) + attachment_context = format_attachment_context( message_data, ) diff --git a/websocket/messages.py b/websocket/messages.py index ee90c1a2..f9d1f2ce 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -31,7 +31,10 @@ should_execute_save_session_directly, should_prearm_save_session, ) -from utils.chat_log import append_chat_log_entry +from utils.chat_log import ( + append_chat_log_entry, + save_turn_reasoning, +) from utils.delayed_memory_triggers import ( load_delayed_memory_by_tags, ) @@ -47,6 +50,7 @@ from .attachments import ( attachment_ids_from_message_data, build_user_text_with_attachments, + get_message_user_text, hydrate_message_attachments, ) from .bootstrap import ( @@ -1115,6 +1119,7 @@ async def process_message( context.runtime_current_sequence_turn_id ) context.runtime_turn_assistant_response = "" + context.runtime_turn_reasoning_log_path = "" context.runtime_active_action_markers = [] context.runtime_turn_aborted_actions = [] context.runtime_turn_abort_requested = False @@ -1169,9 +1174,14 @@ async def process_message( ) ) context.user_message_count += 1 + # Tag auto-load is driven only by the text the user typed. + # Attachment text still stays in ``user_text`` and reaches JIN as + # context, but it must never behave like a tag command. await load_delayed_memory_by_tags( context, - user_text, + get_message_user_text( + message_data + ), ) if not is_action_guard_retry: @@ -1215,6 +1225,21 @@ async def process_message( context, ) + try: + save_turn_reasoning( + context, + getattr( + context, + "runtime_turn_reasoning_content", + "", + ), + ) + except Exception as error: + await logger.log_system( + "[CHAT_LOG] reasoning save failed: " + + str(error) + ) + if ( action_guard_retry and not getattr( From 7e5ac81275d2670d33fc4443821971335cc60d35 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:21:41 +0300 Subject: [PATCH 065/168] chore: ui - add prompt processing avatar animation --- ..._chat_reasoning_spacing_client_contract.py | 14 +- ...test_chat_stream_avatar_client_contract.py | 83 +++ ui/static/css/base.css | 10 +- ui/static/css/chat.css | 75 ++- ui/static/js/chat.js | 512 +++++++++++++++++- ui/static/js/socket.js | 8 + ui/static/js/socket/event-handlers.js | 8 + ui/templates/index.html | 10 +- 8 files changed, 700 insertions(+), 20 deletions(-) create mode 100644 tests/test_chat_stream_avatar_client_contract.py diff --git a/tests/test_chat_reasoning_spacing_client_contract.py b/tests/test_chat_reasoning_spacing_client_contract.py index 16c5f7b1..358143da 100644 --- a/tests/test_chat_reasoning_spacing_client_contract.py +++ b/tests/test_chat_reasoning_spacing_client_contract.py @@ -19,7 +19,7 @@ def test_reasoning_bubbles_use_compact_default_gap(self): self.assertIn("--chat-action-adjacent-gap: 0.82rem;", css) self.assertIn( "#chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child)\n" - "+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child)", + "+ .jin-stream-wrapper:has(", css, ) @@ -33,7 +33,7 @@ def test_action_bubble_expands_reasoning_stack_only_at_action_boundary(self): ) self.assertIn( "#chat-history > .jin-runtime-action-row\n" - "+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child)", + "+ .jin-stream-wrapper:has(", css, ) @@ -42,12 +42,12 @@ def test_stream_wrapper_uses_explicit_inner_gap_instead_of_tailwind_space(self): source = CHAT_JS.read_text(encoding="utf-8") self.assertIn( - ".jin-stream-wrapper > :not([hidden]) ~ :not([hidden])", + ".jin-stream-wrapper > :not([hidden]):not(.jin-stream-avatar-slot)", css, ) self.assertIn("margin-top: var(--chat-inner-gap) !important;", css) self.assertIn( - '"jin-stream-wrapper mx-auto w-full max-w-4xl";', + '"jin-stream-wrapper is-awaiting-model mx-auto w-full max-w-4xl";', source, ) self.assertNotIn( @@ -103,9 +103,9 @@ def test_chat_scrollbar_only_appears_while_scrolling(self): def test_cache_versions_are_bumped_for_reasoning_spacing_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/base.css?v=attached-files-1", source) - self.assertIn("/static/css/chat.css?v=reasoning-gap-1", source) - self.assertIn("/static/js/chat.js?v=jin-size-1", source) + self.assertIn("/static/css/base.css?v=stream-avatar-1", source) + self.assertIn("/static/css/chat.css?v=stream-avatar-1", source) + self.assertIn("/static/js/chat.js?v=stream-avatar-2", source) self.assertIn( "/static/js/panel-inactivity.js?v=jin-size-1", source, diff --git a/tests/test_chat_stream_avatar_client_contract.py b/tests/test_chat_stream_avatar_client_contract.py new file mode 100644 index 00000000..09bc066b --- /dev/null +++ b/tests/test_chat_stream_avatar_client_contract.py @@ -0,0 +1,83 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +CHAT_CSS = ROOT / "ui" / "static" / "css" / "chat.css" +SOCKET_JS = ROOT / "ui" / "static" / "js" / "socket.js" +EVENT_HANDLERS_JS = ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class ChatStreamAvatarClientContractTests(unittest.TestCase): + + def test_stream_reserves_clickable_avatar_before_first_token(self): + source = CHAT_JS.read_text(encoding="utf-8") + css = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn( + '"jin-stream-wrapper is-awaiting-model mx-auto w-full max-w-4xl"', + source, + ) + self.assertIn('"jin-stream-avatar-slot"', source) + self.assertIn('"jin-stream-avatar",\n "is-processing"', source) + self.assertIn("activateStreamAvatar(\n stream", source) + self.assertIn(".jin-stream-wrapper.is-awaiting-model", css) + self.assertIn("@keyframes jin-chat-avatar-processing", css) + self.assertIn("opacity: 0.48;", css) + self.assertIn("transform: scale(0.90);", css) + + def test_avatar_tracks_expanded_reasoning_but_not_collapsed_reasoning(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn( + '!group.thinkContent.classList.contains(\n "is-collapsed"', + source, + ) + self.assertIn( + "group.thinkContent.offsetHeight\n - STREAM_AVATAR_SIZE_PX", + source, + ) + self.assertIn("new ResizeObserver(", source) + self.assertIn("__jinStreamAvatarSync", source) + self.assertIn("function trackStreamAvatarLayoutTransition(", source) + self.assertIn("STREAM_AVATAR_LAYOUT_TRACK_MS = 340", source) + + def test_answer_settles_avatar_and_retry_reasoning_handoffs_it(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn("function animateStreamAvatarHandoff(", source) + self.assertIn("previous.group.avatarSlot.getBoundingClientRect()", source) + self.assertIn("!previous.group.createdAnswer", source) + self.assertIn("setStreamAvatarProcessing(\n stream,\n false", source) + self.assertIn("disconnectStreamThinkResizeObserver(\n stream", source) + + def test_runtime_end_abort_and_disconnect_stop_processing_pulse(self): + socket_source = SOCKET_JS.read_text(encoding="utf-8") + handlers_source = EVENT_HANDLERS_JS.read_text(encoding="utf-8") + + self.assertGreaterEqual( + socket_source.count("window.releaseActiveStreamAvatar()"), + 2, + ) + self.assertGreaterEqual( + handlers_source.count("window.releaseActiveStreamAvatar()"), + 2, + ) + + def test_stream_avatar_assets_are_cache_busted(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("/static/css/base.css?v=stream-avatar-1", source) + self.assertIn("/static/css/chat.css?v=stream-avatar-1", source) + self.assertIn("/static/js/chat.js?v=stream-avatar-2", source) + self.assertIn("/static/js/socket.js?v=stream-avatar-1", source) + self.assertIn( + "/static/js/socket/event-handlers.js?v=stream-avatar-1", + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/css/base.css b/ui/static/css/base.css index a5646b5f..b4a0838d 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -598,7 +598,10 @@ main.panels-collapsed-2 #scene-shade { } #chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child) -+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child) { ++ .jin-stream-wrapper:has( + > .jin-think-wrapper:first-child, + > .jin-stream-avatar-slot:first-child + .jin-think-wrapper +) { margin-top: var(--chat-reasoning-gap) !important; } @@ -610,7 +613,10 @@ main.panels-collapsed-2 #scene-shade { #chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child) + .jin-runtime-action-row, #chat-history > .jin-runtime-action-row -+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child) { ++ .jin-stream-wrapper:has( + > .jin-think-wrapper:first-child, + > .jin-stream-avatar-slot:first-child + .jin-think-wrapper +) { margin-top: var(--chat-action-adjacent-gap) !important; } diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index c9e23395..019b1379 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -46,7 +46,80 @@ position: relative; } -.jin-stream-wrapper > :not([hidden]) ~ :not([hidden]) { +.jin-stream-wrapper.is-awaiting-model { + /* До первого токена держим место только под кликабельную BR/SV-аватарку. */ + min-height: 28px; +} + +.jin-stream-avatar-slot { + position: absolute; + left: 48px; + top: 0; + width: 28px; + height: 28px; + z-index: 3; + pointer-events: none; + transform: translate3d(0, 0, 0); + transition: + left 0.24s cubic-bezier(0.22, 0.72, 0.22, 1), + top 0.24s cubic-bezier(0.22, 0.72, 0.22, 1), + transform 0.26s cubic-bezier(0.22, 0.72, 0.22, 1); + will-change: left, top, transform; +} + +.jin-stream-avatar-slot > .jin-chat-avatar { + pointer-events: auto; +} + +.jin-stream-avatar-spacer { + width: 28px; + height: 28px; + flex: 0 0 28px; + visibility: hidden; + pointer-events: none; +} + +.jin-stream-avatar.is-processing { + transform-origin: 50% 50%; + animation: jin-chat-avatar-processing 1.18s ease-in-out infinite; + will-change: opacity, transform; +} + +.jin-stream-avatar.is-settled { + opacity: 1; + transform: scale(1); + animation: none; + transition: + opacity 0.18s ease, + transform 0.18s ease; +} + +@keyframes jin-chat-avatar-processing { + 0%, 100% { + opacity: 0.48; + transform: scale(0.90); + } + + 50% { + opacity: 0.96; + transform: scale(1.00); + } +} + +@media (prefers-reduced-motion: reduce) { + .jin-stream-avatar-slot { + transition: none; + } + + .jin-stream-avatar.is-processing { + opacity: 0.82; + transform: none; + animation: none; + } +} + +.jin-stream-wrapper > :not([hidden]):not(.jin-stream-avatar-slot) +~ :not([hidden]):not(.jin-stream-avatar-slot) { margin-top: var(--chat-inner-gap) !important; } diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index a7b30037..977930d5 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -10,6 +10,12 @@ const chatInputShell = const streamMessages = new Map(); +const STREAM_AVATAR_LEFT_PX = 48; +const STREAM_AVATAR_SIZE_PX = 28; +const STREAM_AVATAR_HANDOFF_MS = 260; +const STREAM_AVATAR_LAYOUT_TRACK_MS = 340; +let activeStreamAvatarStream = null; + const STREAM_FRAME_WARNING_MS = 12; const STREAM_NEAR_BOTTOM_PX = 72; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = @@ -748,18 +754,25 @@ function flushStreamFrame() { stream ); + let streamAvatarNeedsSync = false; + if (stream.pendingThinking) { if ( !stream.group.createdThinking ) { + stream.group.wrapper.classList.remove( + "is-awaiting-model" + ); + stream.group.wrapper.appendChild( stream.group.thinkWrapper ); stream.group.createdThinking = true; + streamAvatarNeedsSync = true; } @@ -775,6 +788,7 @@ function flushStreamFrame() { stream.pendingThinking = ""; + streamAvatarNeedsSync = true; } @@ -784,12 +798,17 @@ function flushStreamFrame() { !stream.group.createdAnswer ) { + stream.group.wrapper.classList.remove( + "is-awaiting-model" + ); + stream.group.wrapper.appendChild( stream.group.messageRow ); stream.group.createdAnswer = true; + streamAvatarNeedsSync = true; } @@ -805,7 +824,14 @@ function flushStreamFrame() { stream.pendingAnswer = ""; + streamAvatarNeedsSync = true; + + } + if (streamAvatarNeedsSync) { + syncStreamAvatarPosition( + stream + ); } }); @@ -1209,6 +1235,391 @@ function appendChatMessage( } // CREATE STREAM GROUP +function setStreamAvatarProcessing( + stream, + active +) { + + const avatar = + stream + && stream.group + && stream.group.avatar; + + if (!avatar) { + return; + } + + avatar.classList.toggle( + "is-processing", + Boolean(active) + ); + avatar.classList.toggle( + "is-settled", + !active + ); + +} + +function disconnectStreamThinkResizeObserver( + stream +) { + + const observer = + stream + && stream.group + && stream.group.thinkResizeObserver; + + if (!observer) { + return; + } + + observer.disconnect(); + stream.group.thinkResizeObserver = null; + +} + +function syncStreamAvatarPosition( + stream +) { + + if ( + !stream + || !stream.group + || !stream.group.avatarSlot + ) { + return; + } + + const group = stream.group; + const avatarSlot = group.avatarSlot; + + let left = STREAM_AVATAR_LEFT_PX; + let top = 0; + + if ( + group.createdAnswer + && group.messageRow + && group.messageRow.isConnected + ) { + left = + group.messageRow.offsetLeft + + STREAM_AVATAR_LEFT_PX; + top = group.messageRow.offsetTop; + + setStreamAvatarProcessing( + stream, + false + ); + disconnectStreamThinkResizeObserver( + stream + ); + } else if ( + group.createdThinking + && group.thinkWrapper + && group.thinkContent + && group.thinkWrapper.isConnected + ) { + left = STREAM_AVATAR_LEFT_PX; + top = group.thinkWrapper.offsetTop; + + if ( + !group.thinkContent.classList.contains( + "is-collapsed" + ) + ) { + top += Math.max( + 0, + group.thinkContent.offsetHeight + - STREAM_AVATAR_SIZE_PX + ); + } + } + + avatarSlot.style.left = + `${Math.round(left)}px`; + avatarSlot.style.top = + `${Math.round(top)}px`; + +} + +function queueStreamAvatarPositionSync( + stream +) { + + requestAnimationFrame( + () => { + syncStreamAvatarPosition( + stream + ); + } + ); + +} + +function trackStreamAvatarLayoutTransition( + stream +) { + + if ( + !stream + || !stream.group + || !stream.group.avatarSlot + ) { + return; + } + + const group = stream.group; + const trackId = + (group.avatarLayoutTrackId || 0) + 1; + const startedAt = nowMs(); + + group.avatarLayoutTrackId = + trackId; + + const tick = () => { + + if ( + group.avatarLayoutTrackId !== trackId + || !group.avatarSlot + || !group.avatarSlot.isConnected + ) { + return; + } + + syncStreamAvatarPosition( + stream + ); + + if ( + nowMs() - startedAt + < STREAM_AVATAR_LAYOUT_TRACK_MS + ) { + requestAnimationFrame( + tick + ); + return; + } + + syncStreamAvatarPosition( + stream + ); + + }; + + requestAnimationFrame( + tick + ); + +} + +function installStreamThinkResizeObserver( + stream +) { + + if ( + !stream + || !stream.group + || !stream.group.thinkContent + || typeof ResizeObserver !== "function" + ) { + return; + } + + disconnectStreamThinkResizeObserver( + stream + ); + + const observer = new ResizeObserver( + () => { + if ( + !stream.group.createdThinking + || stream.group.createdAnswer + ) { + return; + } + + queueStreamAvatarPositionSync( + stream + ); + } + ); + + observer.observe( + stream.group.thinkContent + ); + stream.group.thinkResizeObserver = + observer; + +} + +function animateStreamAvatarHandoff( + stream, + fromRect +) { + + const slot = + stream + && stream.group + && stream.group.avatarSlot; + + if ( + !slot + || !fromRect + || !slot.isConnected + ) { + return; + } + + syncStreamAvatarPosition( + stream + ); + + const toRect = + slot.getBoundingClientRect(); + const deltaX = + fromRect.left - toRect.left; + const deltaY = + fromRect.top - toRect.top; + + if ( + Math.abs(deltaX) < 0.5 + && Math.abs(deltaY) < 0.5 + ) { + return; + } + + slot.style.transition = "none"; + slot.style.transform = + `translate3d(${deltaX}px, ${deltaY}px, 0)`; + + // Force the origin transform to be painted before the handoff transition. + slot.getBoundingClientRect(); + + requestAnimationFrame( + () => { + slot.style.removeProperty( + "transition" + ); + slot.style.transform = + "translate3d(0, 0, 0)"; + + window.setTimeout( + () => { + if (slot.isConnected) { + slot.style.removeProperty( + "transform" + ); + } + }, + STREAM_AVATAR_HANDOFF_MS + 40 + ); + } + ); + +} + +function activateStreamAvatar( + stream +) { + + const previous = + activeStreamAvatarStream; + let previousRect = null; + + if ( + previous + && previous !== stream + && previous.group + && previous.group.avatarSlot + && !previous.group.createdAnswer + ) { + previousRect = + previous.group.avatarSlot.getBoundingClientRect(); + + previous.group.avatarSlot.remove(); + previous.group.avatarSlot = null; + previous.group.avatar = null; + disconnectStreamThinkResizeObserver( + previous + ); + + if (previous.group.wrapper) { + previous.group.wrapper.classList.remove( + "is-awaiting-model" + ); + + if ( + previous.group.wrapper.childElementCount === 0 + ) { + previous.group.wrapper.remove(); + } + } + } + + activeStreamAvatarStream = stream; + + setStreamAvatarProcessing( + stream, + true + ); + syncStreamAvatarPosition( + stream + ); + + if (previousRect) { + animateStreamAvatarHandoff( + stream, + previousRect + ); + } + +} + +function releaseActiveStreamAvatar() { + + const stream = + activeStreamAvatarStream; + + if (stream) { + const group = stream.group || {}; + const hasVisibleStreamContent = + Boolean( + group.createdThinking + || group.createdAnswer + ); + + if (!hasVisibleStreamContent) { + disconnectStreamThinkResizeObserver( + stream + ); + + if (group.avatarSlot) { + group.avatarSlot.remove(); + group.avatarSlot = null; + group.avatar = null; + } + + if (group.wrapper) { + group.wrapper.classList.remove( + "is-awaiting-model" + ); + + if (group.wrapper.childElementCount === 0) { + group.wrapper.remove(); + } + } + } else { + setStreamAvatarProcessing( + stream, + false + ); + } + } + + activeStreamAvatarStream = null; + +} + function scrollCollapsedThinkToLatest( thinkContent ) { @@ -1292,7 +1703,31 @@ function createStreamGroup( document.createElement("div"); wrapper.className = - "jin-stream-wrapper mx-auto w-full max-w-4xl"; + "jin-stream-wrapper is-awaiting-model mx-auto w-full max-w-4xl"; + + const avatarSlot = + document.createElement("div"); + + avatarSlot.className = + "jin-stream-avatar-slot"; + + const avatar = + createAvatarElement( + role, + contextSnapshot + ); + + avatar.classList.add( + "jin-stream-avatar", + "is-processing" + ); + + avatarSlot.appendChild( + avatar + ); + wrapper.appendChild( + avatarSlot + ); // THINKING @@ -1357,6 +1792,13 @@ function createStreamGroup( ); } + if ( + typeof thinkContent.__jinStreamAvatarSync + === "function" + ) { + thinkContent.__jinStreamAvatarSync(); + } + }; thinkContent.addEventListener( @@ -1400,6 +1842,16 @@ function createStreamGroup( messageRow.className = "jin-message-row"; + const avatarSpacer = + document.createElement("div"); + + avatarSpacer.className = + "jin-stream-avatar-spacer"; + avatarSpacer.setAttribute( + "aria-hidden", + "true" + ); + const pre = document.createElement("pre"); @@ -1415,10 +1867,7 @@ function createStreamGroup( bubble.appendChild(pre); messageRow.appendChild( - createAvatarElement( - role, - contextSnapshot - ) + avatarSpacer ); messageRow.appendChild( @@ -1433,6 +1882,8 @@ function createStreamGroup( return { wrapper, + avatarSlot, + avatar, thinkWrapper, thinkContent, messageRow, @@ -1469,6 +1920,12 @@ function ensureStreamGroup( stream.group.wrapper = realGroup.wrapper; + stream.group.avatarSlot = + realGroup.avatarSlot; + + stream.group.avatar = + realGroup.avatar; + stream.group.thinkWrapper = realGroup.thinkWrapper; @@ -1490,6 +1947,20 @@ function ensureStreamGroup( stream.group.createdAnswer = false; + stream.group.thinkContent.__jinStreamAvatarSync = + () => { + syncStreamAvatarPosition( + stream + ); + trackStreamAvatarLayoutTransition( + stream + ); + }; + + installStreamThinkResizeObserver( + stream + ); + } @@ -1505,6 +1976,9 @@ function startStreamMessage( createdThinking: false, createdAnswer: false, wrapper: null, + avatarSlot: null, + avatar: null, + thinkResizeObserver: null, thinkWrapper: null, thinkContent: null, messageRow: null, @@ -1534,6 +2008,10 @@ function startStreamMessage( stream ); + activateStreamAvatar( + stream + ); + } @@ -1738,6 +2216,26 @@ function finishStreamMessage( stream.group.thinkWrapper.remove(); } + if ( + stream.group.wrapper + && !stream.thinking.trim() + && !stream.answer.trim() + ) { + disconnectStreamThinkResizeObserver( + stream + ); + + if (stream.group.avatarSlot) { + stream.group.avatarSlot.remove(); + stream.group.avatarSlot = null; + stream.group.avatar = null; + } + + stream.group.wrapper.classList.remove( + "is-awaiting-model" + ); + } + if ( stream.group.wrapper && stream.group.wrapper.childElementCount === 0 @@ -1811,3 +2309,7 @@ window.appendThinkingChunk = window.flushStreamFrame = flushStreamFrame; +window.releaseActiveStreamAvatar = + releaseActiveStreamAvatar; +window.syncStreamAvatarPosition = + syncStreamAvatarPosition; diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 39ad900c..b7a930bc 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -401,6 +401,10 @@ function abortGeneration() { clearInterruptedRuntimeGlow(); + if (window.releaseActiveStreamAvatar) { + window.releaseActiveStreamAvatar(); + } + setGenerationState( false ); @@ -636,6 +640,10 @@ function handleSocketClose() { clearInterruptedRuntimeGlow(); + if (window.releaseActiveStreamAvatar) { + window.releaseActiveStreamAvatar(); + } + setGenerationState( false ); diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index c591bff8..39935755 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -76,6 +76,10 @@ function handleSocketError( false ); + if (window.releaseActiveStreamAvatar) { + window.releaseActiveStreamAvatar(); + } + appendLog( "[ERROR]", data.message, @@ -140,6 +144,10 @@ function handleAgentRuntimeStart() { function handleAgentRuntimeEnd() { + if (window.releaseActiveStreamAvatar) { + window.releaseActiveStreamAvatar(); + } + if (window.flushRuntimeTelemetryRender) { window.flushRuntimeTelemetryRender({ final: true diff --git a/ui/templates/index.html b/ui/templates/index.html index a9c19b7a..1c8c52db 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,8 +10,8 @@ - - + + @@ -518,14 +518,14 @@ - + - + - + From f9c06a93cf48359ef97e839645bbe9b00cc332bd Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:31:48 +0300 Subject: [PATCH 066/168] chore: ui - fix for Live Avatar indication after unpin delayed memory from log --- assets/skills/image_prompt_generator.txt | 14 --------- tests/test_delayed_memory_client_contract.py | 31 ++++++++++++++++++++ ui/static/js/runtime/runtime.js | 11 +++---- ui/templates/index.html | 2 +- 4 files changed, 36 insertions(+), 22 deletions(-) delete mode 100644 assets/skills/image_prompt_generator.txt diff --git a/assets/skills/image_prompt_generator.txt b/assets/skills/image_prompt_generator.txt deleted file mode 100644 index 75092d53..00000000 --- a/assets/skills/image_prompt_generator.txt +++ /dev/null @@ -1,14 +0,0 @@ -image_prompt_generator - -You are a direct text generator for image prompts. Your task is to seamlessly combine a reference image and a user's text into a single, highly detailed English paragraph. - -Instructions: -- Prioritize the user's text for the environment, clothing, objects, and style. Completely remove any elements from the reference image that contradict the user's request. -- Identify the main subject from the image. Keep their core physical features and general pose, but adapt them logically to the new scene. Make it visually harmonious. - -STRICT OUTPUT FORMATTING: -- Write exactly ONE continuous paragraph. -- DO NOT output your thinking process, analysis, or steps. -- DO NOT use titles, labels, or bold text (e.g., never write "Final description:", "Subject:", or "Scene:"). -- DO NOT use bullet points or numbered lists. -- Start your response immediately with the first word of the description (for example, "A striking...", "A young woman...", "An atmospheric..."). diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 48f2aa9c..09c6f5d4 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -233,6 +233,37 @@ def test_context_loaded_delayed_memory_highlights_report_and_linked_l4(self): self.assertIn("loaded_delayed_memory_ids", socket_source) self.assertIn("replaceLoadedDelayedMemoryReportIds", socket_source) + def test_delayed_memory_unpin_resyncs_all_avatar_link_state(self): + + runtime_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" + ).read_text(encoding="utf-8") + index_source = ( + ROOT / "ui" / "templates" / "index.html" + ).read_text(encoding="utf-8") + + pin_start = runtime_source.index( + "function setDelayedMemoryReportPinned(" + ) + pin_end = runtime_source.index( + "function setDelayedMemoryReportAnchorFactIds(", + pin_start, + ) + pin_block = runtime_source[pin_start:pin_end] + + self.assertIn( + "if (!syncDelayedMemoryStateToAvatar())", + pin_block, + ) + self.assertNotIn( + "!setDelayedMemoryPinnedOnAvatar(", + pin_block, + ) + self.assertIn( + '/static/js/runtime/runtime.js?v=delayed-unpin-avatar-sync-1', + index_source, + ) + def test_loaded_delayed_memory_bubbles_open_their_own_reports(self): runtime_actions_source = ( diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 74a95980..e0cfcc98 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -1334,13 +1334,10 @@ function setDelayedMemoryReportPinned( renderRuntimeMemorySnapshot(); } - if ( - !setDelayedMemoryPinnedOnAvatar( - normalizedId, - pinned - ) - && !syncDelayedMemoryStateToAvatar() - ) { + // Pin state affects more than the delayed-memory dash itself: it also + // controls linked L4/attachment highlights. Re-sync the whole delayed + // avatar state so unpinning cannot leave stale context-loaded classes. + if (!syncDelayedMemoryStateToAvatar()) { refreshRuntimeAvatar(); } diff --git a/ui/templates/index.html b/ui/templates/index.html index 1c8c52db..8762691f 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -506,7 +506,7 @@ - + From aab8590f65e7677c3615fe5fb62c705b6c325999 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:02:23 +0300 Subject: [PATCH 067/168] chore: ui - update LIVE_AVATAR.md --- LIVE_AVATAR.md | 625 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 436 insertions(+), 189 deletions(-) diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index 05260ba5..05b4e8d0 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -1,4 +1,4 @@ -LIVE_AVATAR v1.0 +LIVE_AVATAR v1.1 # Live Avatar Visual Manual @@ -8,65 +8,80 @@ It is intentionally not a full architecture document. The goal is practical: if ## The Mental Picture -The live avatar is a memory radar. The inner moving rings represent the current runtime memory snapshot. The outer signal rings represent memory systems around that snapshot: delayed reports, L4 long-term facts, and active memory. The center is the JIN accent light; it also feeds the scene tint. +The live avatar is a memory radar with a persistent-file perimeter. The inner moving rings represent the current runtime memory snapshot. The three outer memory-signal rings represent delayed reports, L4 long-term facts, and active memory. A fourth, farther-out ring is made of file dots and represents the persistent files known to `window.JinFiles`. The center is the JIN accent light; it also feeds the scene tint. -Read it like this: +The avatar in the chat stream is a separate presentation layer. It reuses the normal JIN role avatar, but its position and pulse communicate generation state: waiting/processing, reasoning, then settled answer. -| Visual cue | Meaning | -|-----------------------------|---| -| Inner moving rings | Current runtime memory lines | -| Faster inner motion | Larger runtime memory diff | +Read the radar like this: + +| Visual cue | Meaning | +|---|---| +| Inner moving rings | Current runtime memory lines | +| Faster inner motion | Larger runtime memory diff | | Color shifts in inner rings | Keyword/emotional content in runtime memory | -| Runtime change markers | Last real runtime transition: filled = new line, hollow = changed line | -| Delayed report dashes | Stored delayed memory reports | -| Loaded delayed glow | Delayed report is currently loaded into runtime context | -| Bright delayed dashes | Pinned delayed memory reports | -| L4 blue dashes | Long-term facts | -| Dim L4 dots | L4 facts archived behind delayed reports | -| Bright outer dashes | Active memory records | -| Center color | JIN accent color and scene tint | -| Glow on hover/citation | A memory item is being pointed at right now | +| Vertical stripes on a runtime ring | That runtime value contains `?` or `!` | +| Runtime change markers | Last real runtime transition: filled = new line, hollow = changed line | +| Breathing scaffold rays | Ambient runtime activity; ray energy follows snapshot diff | +| Delayed report dashes | Stored delayed memory reports | +| Bright delayed dash | Report is pinned or currently loaded into runtime context | +| Half-accent delayed dash | A loaded report hides an ordinary fact that another report keeps as an anchor | +| L4 blue dashes | Long-term facts that stay directly visible | +| Dim L4 dots | Ordinary report-covered L4 facts; archived, not deleted | +| Bright outer memory dashes | Active memory records | +| Outer file dots | Persistent files in `/assets/files` / `window.JinFiles` | +| Bright white file dot | File is directly pinned/attached to context | +| Half-accent file dot | File is indirectly present through a loaded delayed report | +| Center color | JIN accent color and scene tint | +| Glow on hover/citation/reference | A runtime, memory, or file item is being pointed at right now | + +The important distinction is **representation versus activation**. A dim L4 dot or normal file dot still represents a real stored item. Loading, pinning, hovering, citing, or linking changes emphasis without changing that item's identity or angular slot. ## Main Files To Touch -Most visual changes happen in one file: +Most radar-avatar behavior lives in: `ui/static/js/runtime/runtime-avatar.js` -Use this file for rings, radii, colors, speed, dash opacity, seeded random texture, center rendering, and avatar-level event reactions. +Use this file for runtime rings, memory rings, the file-dot ring, radii, colors, speed, punctuation stripes, seeded geometry, center rendering, delayed/L4/file link state, and avatar-level hover/reference/citation reactions. -The CSS glow and panel presentation live here: +The radar glow, shell aura, depth layer, entry softness, and reduced-motion behavior live in: `ui/static/css/runtime-avatar.css` -Use this file for hover glow, citation glow, memory dash glow, panel collapsed behavior, and reduced-motion behavior. +The chat-stream avatar is controlled separately: + +- `ui/static/js/chat.js` — creates the pre-token avatar, moves it with reasoning, hands it to the answer row, and settles/releases it. +- `ui/static/css/chat.css` — processing pulse, position transitions, and reduced-motion behavior. +- `ui/static/js/socket.js` and `ui/static/js/socket/event-handlers.js` — stop/release the processing avatar on abort, disconnect, error, and runtime end paths. -A few visual knobs are outside those two files: +Other visual/state knobs: | Need | File | Change | |---|---|---| | Avatar panel size | `ui/static/css/base.css` | `--runtime-avatar-panel-size` | | Center color tint applied to scene | `ui/static/js/runtime/runtime-avatar.js` | `setCenterColor()`, `JIN_SCENE_COLOR_INTENSITY` | -| L4 archived/visible classification | `ui/static/js/runtime/runtime-l4-memory.js` | `getVisibleFacts()`, `getFactsWithArchiveState()` | +| L4 archived/visible classification | `ui/static/js/runtime/runtime-l4-memory.js` | `getArchivedFactIdSet()`, `getVisibleFacts()`, `getFactsWithArchiveState()` | | Long-term facts panel visibility | `ui/static/js/runtime/runtime.js` | `getVisibleLongTermMemoryFacts()` | -| Memory row hover dispatch | `ui/static/js/runtime/runtime-memory-view.js` | `dispatchRuntimeMemoryLineAvatarHover()`, `dispatchLongTermFactAvatarHover()`, `dispatchDelayedMemoryAvatarHover()` | +| Runtime/L4/delayed/active/file row hover dispatch | `ui/static/js/runtime/runtime-memory-view.js` | row hover dispatch helpers and `data-avatar-memory-hover-id` | +| Attached-files plaque hover dispatch | `ui/static/js/dragdrop.js` | persistent file hover bindings | +| Persistent file source/state | `ui/static/js/dragdrop.js` + `window.JinFiles` | file store, pin state, `jin:files-store-changed` | +| Delayed report attachment links | `ui/static/js/runtime/runtime-storage.js`, `runtime-memory-view.js` | `attachments_ids` | | Center button click wiring | `ui/static/js/socket/input.js` | `toggleRuntimeAvatarMemoryLayers()` | | Browser cache after visual edits | `ui/templates/index.html` | bump query string for changed CSS/JS | ## Visual Stack -The avatar is drawn as a single SVG inside `#jin-runtime-avatar`. Each render replaces the old SVG with a fresh one. - -Some memory signal updates avoid a full avatar refresh. Delayed, L4, and active memory can sync their own rings in place through `syncDelayedMemoryState()`, `syncL4MemoryState()`, `syncActiveMemoryState()`, and `syncMemorySignalLayer()`. +The radar avatar is drawn as a single SVG inside `#jin-runtime-avatar`. A full render replaces the old SVG, but state-only changes are deliberately synchronized in place where possible so a pin/load transition does not unnecessarily reshuffle or restart visual geometry. The stack is built in this order: 1. SVG definitions: gradients and glow filters. -2. Static scaffold: faint background circles and radial guide lines. +2. Static scaffold: halo, concentric radar circles, and breathing radial rays. 3. Runtime rings: one moving orbit per runtime memory line. 4. Memory signal rings: delayed, L4, active. -5. Center core: the central light and glow. -6. Runtime/citation/reference/hover classes are reapplied. +5. Persistent file ring: one dot per file, outside active memory. +6. Center core: the central light and glow. +7. Runtime/citation/reference/hover/link classes are reapplied. The main render function is: @@ -74,50 +89,72 @@ The main render function is: renderAvatar(snapshot, options = {}) ``` -File: - -`ui/static/js/runtime/runtime-avatar.js` - Important helpers: | Helper | Role | |---|---| | `appendDefs()` | SVG filters and gradients | -| `appendStaticScaffold()` | background radar structure | +| `appendStaticScaffold()` | background radar structure and diff-reactive rays | | `computeRingRecords()` | turns runtime lines into orbit records | | `appendOrbit()` | draws one runtime memory orbit | -| `appendMemorySignalRings()` | draws delayed, L4, active memory signal rings | +| `appendMemorySignalRings()` | draws delayed, L4, active memory rings | +| `appendFileRing()` / `appendFileSignalRing()` | draws the persistent-file perimeter | | `appendCenter()` | draws center core | -| `applyThinkRuntimeCitationGlow()` | applies citation glow | -| `applyMemoryReferenceGlow()` | applies reference glow | -| `applyMemoryRowAvatarHoverGlow()` | applies hover glow | +| `applyThinkRuntimeCitationGlow()` | applies think-citation glow | +| `applyMemoryReferenceGlow()` | applies response/reference glow | +| `applyMemoryRowAvatarHoverGlow()` | applies row/plaque hover glow and cross-layer links | +| `applyDelayedMemoryFactLinkGlow()` | delayed-report <-> L4 highlighting plus secondary-report state | +| `applyDelayedMemoryFileLinkGlow()` | focused delayed-report <-> attachment-dot highlighting | | `syncMemorySignalLayer()` | rebuilds one memory signal ring in place when possible | +| `syncDelayedMemoryState()` | resyncs delayed state, L4 archive/link state, and file-link state | +| `syncFilesState()` | updates file-dot state without restarting the orbit when the file set is unchanged | + +### Live Sync Rules + +Several interactions intentionally avoid a full `avatar.refresh()`: + +- delayed, L4, and active memory use their dedicated sync functions; +- `repaintAvatar()` performs a full redraw with the current `avatarRefreshNonce`, so the seeded geometry stays the same; `reinitializeAvatar()` / public `refresh()` increments the nonce when an intentional reseed is wanted; +- file pin/context-link changes update existing file dots in place when the set of file ids did not change; +- the file ring is rebuilt only when the persistent file set itself changes; +- file records are sorted by file id, so changing pin/load state never changes a file's angular slot; +- `jin:files-store-changed` triggers file-state synchronization; +- delayed-memory state synchronization also refreshes related L4 and file-link states. + +The memory panel may stay on `[active]`, `[delayed]`, `[facts]`, `[long_term]`, or `[files]` while L1 updates. `renderRuntimeMemorySnapshot()` now still dispatches the newest runtime snapshot to the avatar when the visible panel mode is not `[runtime]`. This prevents the radar from freezing merely because the user is looking at another memory tab. ## Static Scaffold -The scaffold is the quiet radar structure behind the live memory rings. It does not represent a memory item directly. It gives the avatar depth and makes the active rings easier to read. +The scaffold is the quiet radar structure behind the live memory rings. It is not a memory item. It gives the avatar depth and now carries a very slow activity signal from the current runtime diff. It is drawn by: ```js -appendStaticScaffold(svg, overallColor, random) +appendStaticScaffold(svg, overallColor, currentCenterColor, diffPercent, random) ``` +Current behavior: + +- the concentric circles remain faint and structural; +- there are `16` radial rays; +- only a seeded subset of roughly `3..7` rays receives stronger breathing energy; +- `diffPercent` controls `rayEnergy`, so a larger runtime diff makes the active rays somewhat more visible; +- ray color mixes the current center color, overall runtime color, and a dark base; +- each ray receives a long seeded duration/phase, so the effect reads as ambient breathing rather than a busy equalizer; +- `prefers-reduced-motion` disables the breathing animation. + Change these when you want the background to feel denser, cleaner, brighter, or more technical: | Visual part | Where | |---|---| | Concentric scaffold circles | `STATIC_SCAFFOLD_RADII` | | Radial guide line range | `STATIC_RADIAL_LINE_INNER_RADIUS`, `STATIC_RADIAL_LINE_OUTER_RADIUS` | -| Scaffold circle opacity | `appendStaticScaffold()` circle attributes | -| Radial line opacity | `appendStaticScaffold()` line attributes | -| Warm radial accents | `AMBER_ACCENT` and `index % 5` logic | +| Number of rays | `rayCount` in `appendStaticScaffold()` | +| Diff response | `rayEnergy`, `activeRayCount` | +| Ray color/opacity/duration | `appendStaticScaffold()` | +| Breathing curve | `.jin-avatar-scaffold-ray.is-jin-avatar-ray-breathing`, `@keyframes jin-avatar-scaffold-ray-breathe` in `runtime-avatar.css` | -Good edit examples: - -- To make the avatar calmer, lower scaffold `stroke-opacity`. -- To make it more technical, add more scaffold radii or increase dash visibility. -- To make it less noisy, reduce radial guide lines or their opacity. +The inner scaffold/ring geometry is globally compressed with `INNER_RING_SCALE = 0.90`. The SVG itself also renders at `transform: scale(0.90)` so the new outer file perimeter has breathing room inside the square avatar shell. ## Runtime Rings @@ -134,7 +171,8 @@ These rings are the most "alive" part of the avatar: they change radius, speed, | Ring speed | Snapshot diff intensity | | Ring direction | Seeded random direction | | Ring color | Keyword palette plus emotional/alert influence | -| Extra stripes | The line is long compared to the others | +| Runtime highlight glow | Uses that ring's current color rather than a fixed generic glow | +| Vertical stripes | The runtime **value** contains `?` or `!` | | Filled change marker | The line was added in the last real runtime transition | | Hollow change marker | The line changed in the last real runtime transition | | Change marker size | Magnitude of the line change | @@ -148,10 +186,12 @@ These rings are the most "alive" part of the avatar: they change radius, speed, | Speed | `runtime-avatar.js` | `appendOrbit()` -> `baseSpeed`, `effectiveSpeed`, `duration` | | Dash texture | `runtime-avatar.js` | `appendOrbit()` -> `dashLength`, `gapLength`, `strokeWidth` | | Arc fragments | `runtime-avatar.js` | `appendOrbit()` -> `arcCount`, `appendArcCircle()` | -| Long-line stripes | `runtime-avatar.js` | `appendLongFieldStripes()` | +| Punctuation-triggered stripes | `runtime-avatar.js` | `appendOrbit()` condition + `appendLongFieldStripes()` | | Runtime change markers | `runtime-avatar.js` | `appendRuntimeChangeMarker()`, `resolveRuntimeChangeMarkers()` | -`computeRingRecords()` uses `sourceOrderRatio`, not line length, for the base radius. Line length is still used to decide whether a line gets extra long-field stripes. +`computeRingRecords()` uses `sourceOrderRatio`, not line length, for the base radius. `record.isLong` may still exist as record metadata, but it no longer controls the stripe visual. Stripes are appended only when `String(record.value || "")` matches `/[!?]/`. The old stripe geometry itself is preserved: seeded count, height, arc span, color, width, and opacity. + +Runtime hover/citation/reference glow variables are derived from the orbit's current `ringColor`, so a highlighted orbit keeps its own semantic color instead of flattening every highlight to cyan. Idle-opacity CSS targets only direct structural circles/lines; nested filled change markers are not accidentally dimmed with the base orbit. ### Runtime Ring Speed @@ -237,52 +277,56 @@ To add a warning theme: ## Memory Signal Rings -The memory signal rings are the outer dash/dot rings. They are not runtime memory lines. They represent separate memory systems around the current runtime state. +The memory signal rings are the outer dash/dot rings. They are not runtime memory lines. They represent separate memory systems around the current runtime state. The persistent-file perimeter sits one step farther out and uses dots instead of memory dashes. -They are configured in: +Memory dashes are configured in: ```js MEMORY_RING_LAYOUT ``` -File: +The file perimeter is configured separately in: -`ui/static/js/runtime/runtime-avatar.js` +```js +FILE_RING_LAYOUT +``` -| Ring | Radius | Stroke width | Meaning | +| Ring | Radius | Stroke / dot size | Meaning | |---|---:|---:|---| | Delayed | `158` | `3.10` | Delayed memory reports | | L4 | `168` | `1.05` | Long-term facts | | Active | `178` | `1.35` | Active memory records | +| Files | `188` | dot radius `2.7` | Persistent files | -The render and sync insertion order is `delayed`, then `l4`, then `active`. L4 intentionally sits between delayed reports and active memory. +The memory-ring render order is `delayed`, then `l4`, then `active`; the file ring is inserted after those and before the center. L4 intentionally sits between delayed reports and active memory. -Each item becomes one memory dash record. Delayed and active records stay dashes; archived L4 records use the same record path but visually settle into dots. So 10 L4 facts means 10 L4 memory records, some of which may appear as dots. 3 delayed reports means 3 delayed dashes. +Each delayed/active item becomes one dash record. L4 always keeps one avatar record per fact, but archived L4 facts settle into dots instead of disappearing from the radar. The file ring uses one stable dot per persistent file. ### Memory Ring Layout Controls | Field | Meaning | |---|---| | `radius` | How far from center the ring sits | -| `strokeWidth` | Thickness of dash stroke | +| `strokeWidth` | Thickness of a memory dash | | `minArcDegrees` | Smallest dash length | | `maxArcDegrees` | Largest dash length | | `arcRatio` | How much of each item slot is filled | | `arcTrimPixels` | Optional pixel trim before arc degrees are finalized; currently used by L4 | -| `startAngle` | Where the first dash starts | +| `startAngle` | Where the first dash/dot slot starts | +| `FILE_RING_LAYOUT.dotRadius` | Persistent-file dot size | -Use these fields when you want to move a ring inward/outward or change how dense the dashes feel. +Use these fields when you want to move a ring inward/outward or change how dense its records feel. Keep enough separation between radii for hover/link glows to remain visually distinct. ## L4 Facts Ring -The L4 ring shows long-term facts. Each visible L4 fact becomes one memory dash record. Archived facts use the same record identity, but render as a dim dot state. +The L4 ring shows all stored long-term facts. A directly exposed fact renders as a blue dash. A report-covered ordinary fact keeps the same avatar identity but renders as a dim dot. -There are two visual states: +There are two base visual states: | L4 state | Meaning | Avatar | |---|---|---| -| Visible fact | Fact is directly available in long-term context | normal blue dash | -| Archived fact | Fact is covered by a delayed report and is not currently anchored or loaded | dim blue dot; the dash arc fades away | +| Visible / anchor fact | Fact stays directly available in long-term context | normal blue dash | +| Archived ordinary fact | Fact is listed by a delayed report as ordinary report content | dim blue dot; dash arc fades away | Current opacity: @@ -300,36 +344,50 @@ File: `ui/static/js/runtime/runtime-avatar.js` -Place: - -L4 branch inside `appendMemorySignalRing()`. +Place: L4 branch inside `appendMemorySignalRing()`. ### L4 Archived Meaning -Archived does not mean deleted. It means the fact is already covered by a delayed report and should not be shown as a direct long-term context line right now. +Archived does **not** mean deleted. It means the fact has been absorbed into delayed-memory report content and should not occupy a normal direct L4 line in context or the long-term panel. + +Current classification is intentionally simple and global: + +1. collect archive candidates from every report's `facts_ids`, legacy `absorbed_fact_ids`, and legacy `long_term_facts_ids`; +2. collect every report's `anchor_fact_ids`; +3. remove all anchor ids from the archived set. + +So the important direct-id rule is: + +**Anchor ids are removed globally from the archived-id set. Loaded/pinned does not remove an ordinary `facts_ids` id from that set.** + +`factMatchesArchivedIds()` then checks both `fact.id` and `source_fact_ids`. That matters for merged/derived L4 records: even when the record's own id is anchored, an archived source id can still make the combined record classify as archived. + +A report being loaded or pinned can make its linked archived L4 dot glow through `is-delayed-memory-linked-hit`, but load state by itself never changes archive classification. -Visual behavior: +This distinction prevents a report load from rewriting the structural meaning of L4. Load/pin is context emphasis; `anchor_fact_ids` is the structural exception that keeps an L4 fact exposed. -- It stays represented in the avatar as a dim dot. -- Its dash arc is animated away by the `is-memory-dot` CSS state. -- It disappears from the long-term facts panel while archived. +### Cross-Report Anchor Signal -Archive ids are collected from delayed report `facts_ids`, legacy `absorbed_fact_ids`, and legacy `long_term_facts_ids`. +A loaded/pinned report can contain an ordinary fact that is archived behind it while another report uses the same fact in `anchor_fact_ids`. In that case the **other delayed report** receives the softer class: -Anchor and loaded facts are the exceptions. If a delayed report lists a fact in `anchor_fact_ids`, or if a report is loaded into runtime context and lists the fact in `facts_ids`, that fact remains visible and is not dimmed. Pinned delayed reports count as loaded for this classification. +```text +is-delayed-memory-secondary-linked +``` + +That half-accent says: "this loaded report contains a hidden fact whose exposed anchor lives in another report." The delayed-memory panel mirrors this relation, and the report modal can surface the other report under `anchored_to` for the fact. ### L4 Visual Edit Guide | Desired change | File | Edit | |---|---|---| -| Make archived dots dimmer | `runtime-avatar.js` | lower `0.26` | -| Make archived dots brighter | `runtime-avatar.js` | raise `0.26` | -| Make all L4 facts brighter | `runtime-avatar.js` | raise `0.52` | +| Make archived dots dimmer/brighter | `runtime-avatar.js` | change archived opacity `0.26` | +| Make visible L4 facts brighter | `runtime-avatar.js` | change `0.52` | | Change L4 color | `runtime-avatar.js` | `L4_MEMORY_RING_COLOR` | | Move L4 ring | `runtime-avatar.js` | `MEMORY_RING_LAYOUT.l4.radius` | | Make L4 dashes longer | `runtime-avatar.js` | `MEMORY_RING_LAYOUT.l4.arcRatio` or `maxArcDegrees` | | Change archived dot transition | `runtime-avatar.css` | `.jin-avatar-memory-dash.is-memory-dot`, `jin-avatar-memory-absorb-dot` | -| Hide archived facts from avatar entirely | `runtime-avatar.js` | use `getVisibleFacts()` instead of `getFactsWithArchiveState()` in `getL4MemoryAvatarRecords()` | +| Change archive semantics | `runtime-l4-memory.js` | `getArchivedFactIdSet()` | +| Hide archived facts from avatar entirely | `runtime-avatar.js` | use visible facts instead of `getFactsWithArchiveState()` in `getL4MemoryAvatarRecords()` | The archived/visible classification itself is in: @@ -339,47 +397,52 @@ Important functions: | Function | Role | |---|---| -| `getArchivedFactIdSet()` | Collects hidden fact ids from delayed reports, then removes anchored and loaded fact ids | +| `getArchivedFactIdSet()` | Collects report-covered fact ids, then removes every globally anchored fact id | | `factMatchesArchivedIds()` | Checks direct id and `source_fact_ids` | -| `getVisibleFacts()` | Returns only facts visible in long-term panel | +| `getVisibleFacts()` | Returns only facts visible in the long-term panel | | `getFactsWithArchiveState()` | Returns all facts with `archived` flag for avatar | ## Delayed Reports Ring The delayed ring shows delayed memory reports. Each dash is one report. -Normal reports are dimmer. Loaded reports get a context glow through CSS. Pinned reports are much brighter, almost white. +`getDelayedMemoryAvatarRecords()` treats a report as loaded when it is either pinned or present in the runtime's loaded delayed-memory id set. Both direct pinning and runtime loading therefore use the same strong base visual; the classes remain separate so interaction logic can still distinguish them. | Report state | Avatar opacity | Color / class | |---|---:|---| -| Normal | `0.36` | `DELAYED_MEMORY_RING_COLOR` mixed with overall avatar color | -| Loaded | base `0.36`, visually boosted by CSS | `is-context-loaded` | -| Pinned | `0.82` | `PINNED_DELAYED_MEMORY_RING_COLOR`, `is-memory-pinned`; also treated as loaded | +| Normal | `0.36` | delayed color mixed with overall avatar color | +| Runtime-loaded, not pinned | `0.82` | bright `PINNED_DELAYED_MEMORY_RING_COLOR`, `is-context-loaded` | +| Pinned | `0.82` | bright `PINNED_DELAYED_MEMORY_RING_COLOR`, `is-memory-pinned` and loaded semantics | +| Secondary-linked | base state plus softer half-accent | `is-delayed-memory-secondary-linked` | -File: +The stronger generic reference/citation/link selector intentionally does not treat a merely context-loaded delayed dash as a generic `is-context-loaded` memory hit. Its normal loaded/pinned brightness is handled by the dedicated delayed selector, preventing accidental overboost. -`ui/static/js/runtime/runtime-avatar.js` +Files: + +- `ui/static/js/runtime/runtime-avatar.js` +- `ui/static/css/runtime-avatar.css` Places: -- delayed branch inside `appendMemorySignalRing()` -- live pin update in `setDelayedMemoryDashPinned()` -- live loaded/link update in `syncDelayedMemoryDashState()` and `applyDelayedMemoryFactLinkGlow()` +- delayed branch inside `appendMemorySignalRing()`; +- live pin update in `setDelayedMemoryDashPinned()`; +- loaded/link update in `syncDelayedMemoryDashState()` and `applyDelayedMemoryFactLinkGlow()`; +- cross-report relation in `getSecondaryLinkedDelayedMemoryReportIds()`. -Use this section when you want reports to feel more or less present. +Delayed reports also expose `attachments_ids`. Those ids feed the persistent file ring: a loaded report gives each non-pinned attached file a softer indirect-context accent, while focusing/hovering that report can give the linked file dot the stronger relational glow. | Desired change | Edit | |---|---| -| Make normal reports more visible | raise `0.36` | -| Make loaded reports less intense | edit `.jin-avatar-memory-dash.is-context-loaded` | -| Make pinned reports less intense | lower `0.82` | -| Make pinned color less white | change `PINNED_DELAYED_MEMORY_RING_COLOR` | +| Make normal reports more visible | raise normal `0.36` | +| Make loaded/pinned reports less intense | lower active `0.82` and/or edit dedicated delayed CSS | +| Make active report color less white | change `PINNED_DELAYED_MEMORY_RING_COLOR` | +| Change secondary-link intensity | edit `.is-delayed-memory-secondary-linked` | | Move delayed ring | change `MEMORY_RING_LAYOUT.delayed.radius` | | Make report dashes thicker | change `MEMORY_RING_LAYOUT.delayed.strokeWidth` | ## Active Memory Ring -The active ring shows active memory records. This is the outermost memory signal ring and is intentionally bright. +The active ring shows active memory records. It is the outermost **memory-dash** ring and is intentionally bright; the file-dot perimeter sits beyond it. Current behavior: @@ -395,9 +458,9 @@ File: Places: -- `getActiveMemoryAvatarRecords()` -- active branch inside `appendMemorySignalRing()` -- `MEMORY_RING_LAYOUT.active` +- `getActiveMemoryAvatarRecords()`; +- active branch inside `appendMemorySignalRing()`; +- `MEMORY_RING_LAYOUT.active`. Active memory records come from strings like: @@ -410,9 +473,65 @@ For hover identity, active records use `[active_memory_id: abc123]` embedded in If you want active memory to feel less dominant, lower opacity or move the ring inward. +## Persistent Files Ring + +The outermost signal layer is a slow counter-orbit of persistent file dots. It is sourced from `window.JinFiles.getFiles()` and uses one dot per valid persistent file. + +Configuration: + +```js +FILE_RING_LAYOUT = { + radius: 188, + dotRadius: 2.7, + baseColor: "#7ab8d8", + glowColor: "#7ab8d8", + startAngle: -12, +} +``` + +The ring duration is seeded from the file-id set and falls in roughly `92..176s`. File records are sorted by id before assigning slots, so pinning/unpinning or delayed-context changes alter only appearance, never the dot's angular position. + +### File Dot States + +| State | Base appearance | Class / source | +|---|---|---| +| Stored, inactive | blue dot, opacity `0.36` | normal file record | +| Directly pinned/attached | bright white dot, opacity `0.96` | `is-memory-pinned`, `is-context-loaded` | +| Indirectly in context through loaded delayed report | half-accent, not white | `is-delayed-memory-context-linked` | +| Hovered in file UI | same half-accent as indirect context link | `is-memory-hover-hit` | +| Referenced by JIN / relation-focused | stronger cyan relation glow | `is-memory-reference-hit` / `is-delayed-memory-linked-hit` | + +A crucial distinction in `getPersistentFileAvatarRecords()`: + +- `contextLoaded` means the file itself is pinned; +- `contextLinked` means the file is **not pinned**, but at least one loaded delayed report lists its id in `attachments_ids`. + +Indirect context deliberately stays weaker than direct attachment. A pinned file keeps the stronger white state even while hovered or indirectly linked. + +### File Identity And Matching + +File hover identity is: + +```text +file: +``` + +Reference aliases include the six-character file id, original name, stored name, `/assets/files/...` context path, URL when available, and the stored name with the generated id prefix stripped. The SVG node also carries the linked delayed-report ids so `applyDelayedMemoryFileLinkGlow()` can light attachment dots when a delayed report is focused. + +File hover sources include the `[ files ]` memory panel, delayed-report attachment chips/picker options, and the fixed attached-files plaque. Pin/name/attachment hover can therefore target the same dot without duplicating identity logic. + +### File Ring Sync + +`syncFilesState()` compares the current file-id set with the rendered file ring: + +- same ids: update classes/data/colors in place and keep the current orbit animation; +- changed ids: rebuild the file ring; +- `jin:files-store-changed`: trigger sync; +- delayed-memory sync also triggers file sync because `attachments_ids` may change indirect context state. + ## Center Core -The center is a visual anchor. It is drawn after all rings and sits on top. +The center is a visual anchor. It is drawn after all signal layers and sits on top. It is made of: @@ -446,9 +565,22 @@ Main places: Lower `JIN_SCENE_COLOR_INTENSITY` if the center color affects the page too much. +## Ambient Shell, Depth, And Entry Softness + +The avatar shell now has two CSS-only depth layers outside the SVG: + +- `.jin-runtime-avatar-shell::before` — a soft radial aura colored from `--jin-color`, breathing on a `9s` cycle; +- `.jin-runtime-avatar-shell::after` — a dark radial/vignette depth layer that makes the radar feel embedded rather than flat. + +These layers are deliberately ambient. They do not represent memory records and they do not become stronger just because the memory panel is collapsed. + +Freshly redrawn runtime orbit entries use `jin-avatar-orbit-enter` for about `0.92s`, moving through a soft scale-in (`0.82` -> near full size -> slight `1.012` overshoot -> settled). This restores entry softness without changing the seeded orbit geometry. + +Reduced-motion rules disable the breathing/rotation/entry animations where appropriate. + ## Glow States -Glow is mostly CSS. JavaScript only decides which class to apply. +Glow is mostly CSS. JavaScript decides which semantic class to apply; CSS decides intensity, saturation, drop-shadow, stroke width, dot radius, and transition feel. File: @@ -456,20 +588,30 @@ File: | Class | Trigger | Meaning | |---|---|---| -| `is-memory-hover-hit` | Hovering the matching memory row | "User is pointing at this memory item" | -| `is-runtime-cited` | Hovering or activating a think citation | "This memory item is cited" | -| `is-memory-reference-hit` | JIN response text references a unique alias | "This memory item was mentioned" | -| `is-memory-pinned` | Delayed report is pinned | "This report is important" | -| `is-context-loaded` | Delayed report is loaded into runtime context | "This report is active in context" | -| `is-delayed-memory-linked-hit` | Delayed report and L4 fact are linked through fact ids | "These memory layers refer to the same fact" | -| `is-memory-archived` | L4 fact is archived by report | Marker for hidden-from-context facts | -| `is-memory-dot` | Archived L4 fact dot state | Dash arc fades away and the dot remains | +| `is-memory-hover-hit` | Hovering the matching runtime/memory/file row | "User is pointing at this item" | +| `is-runtime-cited` | Hovering or activating a think citation | "This runtime/memory item is cited" | +| `is-memory-reference-hit` | JIN text references a unique alias | "This item was mentioned" | +| `is-memory-pinned` | Delayed report or file is pinned | direct strong context importance | +| `is-context-loaded` | Delayed report is runtime-loaded; on file dot, file itself is pinned | direct context presence | +| `is-delayed-memory-linked-hit` | Focused delayed report links to an L4 fact or file | strong cross-layer relation | +| `is-delayed-memory-secondary-linked` | Loaded report's hidden ordinary fact is anchored by another report | softer delayed-report relation | +| `is-delayed-memory-context-linked` | Non-pinned file is attached to a loaded delayed report | softer indirect file context | +| `is-memory-archived` | L4 fact is structurally archived behind report content | hidden-from-direct-context marker | +| `is-memory-dot` | Archived L4 dot state | dash arc fades and dot remains | + +### Visual Priority + +Direct active states should read stronger than inferred relations: -Use CSS for brightness, drop-shadow, stroke-width, and transition feel. Use JS only if the matching logic changes. +1. pinned/direct context and explicit reference/link hits; +2. ordinary row hover / secondary delayed link / indirect file context; +3. normal stored state. + +For files specifically, ordinary hover and indirect delayed context intentionally share the same half-accent. They must not become the bright white used for a directly pinned file. ## Hover Matching -Hover matching connects rows in the memory panel to shapes in the avatar. +Hover matching connects rows and plaques to shapes in the radar avatar. The shared identity is: @@ -481,11 +623,12 @@ Identity helper: `ui/static/js/runtime/runtime-core.js` -Row event dispatch: +Major dispatch sources: -`ui/static/js/runtime/runtime-memory-view.js` +- `ui/static/js/runtime/runtime-memory-view.js` — runtime/L4/delayed/active/files rows plus delayed-report attachment chips/picker options; +- `ui/static/js/dragdrop.js` — attached-files plaque. -Avatar-side glow application: +Avatar-side application: `ui/static/js/runtime/runtime-avatar.js` @@ -497,18 +640,24 @@ Shapes: | L4 fact | `l4:` | | Delayed report | `delayed:` | | Active memory | `active:` or `active:record-` | +| Persistent file | `file:` | -L4 and delayed memory also cross-highlight through their fact links. Hovering an L4 fact lights any delayed report that lists the fact in `anchor_fact_ids`. Hovering a delayed report lights the related L4 dots or dashes from its `facts_ids`, if the report has linked facts. +Cross-layer behavior is separate from same-id hover: -Pinned or currently loaded delayed reports also keep their linked L4 facts highlighted through `is-delayed-memory-linked-hit`, even without row hover. +- hover/focus an L4 fact -> delayed reports whose `anchor_fact_ids` contain it can glow; +- hover/focus a delayed report -> its linked L4 facts from `facts_ids` can glow, including archived dots; +- loaded/pinned delayed reports keep linked L4 facts highlighted without row hover; +- if one of those hidden ordinary facts is an anchor in another report, that other report gets the softer secondary-link accent; +- hover/focus a delayed report -> attached file dots from `attachments_ids` can receive the stronger relation glow; +- a non-pinned file that belongs to any loaded delayed report keeps the softer indirect-context accent even without hover. -If hover glow stops working, check that the row and the SVG node have the same `data-avatar-memory-hover-id`. +If hover glow stops working, first check that the source row/plaque and the SVG node agree on `data-avatar-memory-hover-id`, then check whether the expected effect is a direct hover or a cross-layer relation class. ## Citation Matching -Citation matching connects think citations to avatar shapes. +Citation/reference matching connects think citations and JIN output text to radar shapes. -For visual changes, edit CSS: +For visual changes, edit: `ui/static/css/runtime-avatar.css` @@ -516,7 +665,7 @@ For matching logic, edit: `ui/static/js/runtime/runtime-avatar.js` -Function: +Main function: ```js applyThinkRuntimeCitationGlow() @@ -530,11 +679,63 @@ buildCitationRecordIdentity(id, key, value) This prevents two facts with the same key from glowing incorrectly. -For runtime, active, and delayed records, citation matching falls back to exact normalized line text or a unique normalized key when no strict identity is present. +For runtime, active, and delayed records, matching can fall back to exact normalized line text or a unique normalized key when no strict identity is present. + +Persistent file dots participate in the same reference layer. Their aliases include id/name/stored path variants, so JIN mentioning a unique file id or file name can light the corresponding dot. File SVG text identity is normalized from `name · contextPath`. + +## Live Chat Stream Avatar + +The small avatar shown beside a model response is not the radar SVG. It is the normal role avatar placed in a movable stream slot so the UI shows model activity before and during streamed output. + +Files: + +- `ui/static/js/chat.js` +- `ui/static/css/chat.css` +- release paths in `ui/static/js/socket.js` and `ui/static/js/socket/event-handlers.js` + +### Before The First Token + +`startStreamMessage()` immediately creates the stream group and calls `activateStreamAvatar()`. The wrapper gets `is-awaiting-model`, and `.jin-stream-avatar-slot` appears at the response position before reasoning or answer text exists. + +The avatar itself receives: + +```text +jin-stream-avatar is-processing +``` + +The slot keeps the avatar clickable even though the slot container itself has `pointer-events: none`; the child `.jin-chat-avatar` restores `pointer-events: auto`. + +Processing pulse: + +- `1.18s` ease-in-out loop; +- opacity roughly `0.48 -> 0.96 -> 0.48`; +- scale roughly `0.90 -> 1.00 -> 0.90`; +- reduced-motion disables the pulse and leaves a stable `0.82` opacity. + +### During Reasoning + +When reasoning exists, `syncStreamAvatarPosition()` moves the slot to the reasoning block. + +- collapsed reasoning: avatar stays at the top of the reasoning wrapper; +- expanded reasoning: avatar follows the bottom edge of the growing reasoning content, offset by its own `28px` size; +- `ResizeObserver` watches reasoning height while no answer exists; +- collapse/expand triggers a short requestAnimationFrame tracking window so the avatar follows the layout transition rather than teleporting. + +The slot's normal `left`/`top` transitions are `0.24s`; handoff transform is `0.26s`. + +### Answer Handoff And Settle + +As soon as an answer row exists, the avatar moves to that row, `setStreamAvatarProcessing(false)` switches it to `is-settled`, and the reasoning resize observer is disconnected. A hidden `jin-stream-avatar-spacer` reserves the normal avatar width in the answer row so the absolute slot lands cleanly without shifting the bubble. + +If a new/retry stream takes over while a previous stream still has no answer, `activateStreamAvatar()` measures the old slot, removes it, and animates the new slot from the old screen position. This makes the avatar handoff continuous instead of spawning two processing avatars. + +### Abort / End / Disconnect + +`releaseActiveStreamAvatar()` is called from runtime-end, abort, disconnect, and error paths. If no visible reasoning/answer content exists, the placeholder avatar/wrapper is removed. If content already exists, the avatar stays with that content but stops processing pulse. ## Central Button -The center button is visually part of the avatar. It toggles the visibility of the avatar's memory layers without refreshing the SVG data. +The center button is visually part of the radar avatar. It toggles the visibility of the original radar memory layers without refreshing the SVG data. DOM id: @@ -554,15 +755,22 @@ Current click behavior: window.JinRuntime.avatar.toggleMemoryLayers() ``` -The click toggles `is-memory-layers-hidden` on `#jin-runtime-avatar`. CSS hides the scaffold, runtime orbit entries, memory rings/dashes, and thin center rings, while the central light remains visible. The old manual fact-check WebSocket path still exists separately, but the center click does not start fact-check and does not call `avatar.refresh()`. +The click toggles `is-memory-layers-hidden` on `#jin-runtime-avatar`. Current CSS hides: + +- scaffold; +- runtime orbit entries; +- delayed/L4/active memory rings and dashes; +- thin center rings. + +The central light remains visible. The **file ring currently also remains visible**, because `.jin-avatar-file-ring` / `.jin-avatar-file-dot` are not part of the `is-memory-layers-hidden` selector. Treat that as current behavior when changing the toggle; do not assume "memory layers" automatically includes files. + +The old manual fact-check WebSocket path still exists separately, but center click does not start fact-check and does not call `avatar.refresh()`. ## Edit Recipes ### Make archived L4 dots almost invisible -File: - -`ui/static/js/runtime/runtime-avatar.js` +File: `ui/static/js/runtime/runtime-avatar.js` Change: @@ -572,55 +780,66 @@ opacity: record.archived ? 0.12 : 0.52 ### Make normal delayed reports stronger -File: - -`ui/static/js/runtime/runtime-avatar.js` - -Change: +Change the inactive branch in `appendMemorySignalRing()`: ```js -opacity: pinned ? 0.82 : 0.50 +opacity: active ? 0.82 : 0.50 ``` -### Make active memory less dominant +Do not change the active `0.82` unless you also want runtime-loaded and pinned reports to become weaker/stronger together. -File: +### Change the secondary delayed-report accent -`ui/static/js/runtime/runtime-avatar.js` +File: `ui/static/css/runtime-avatar.css` + +Edit: + +```css +.jin-avatar-memory-dash-delayed.is-delayed-memory-secondary-linked +``` + +Keep it visibly below the direct loaded/pinned white state. + +### Make active memory less dominant -Change active opacity: +Change active opacity in `runtime-avatar.js`, for example: ```js opacity: 0.58 ``` -Or move the ring inward: +Or move `MEMORY_RING_LAYOUT.active.radius` inward. + +### Move all memory/file signal layers outward + +Adjust: ```js +MEMORY_RING_LAYOUT.delayed.radius +MEMORY_RING_LAYOUT.l4.radius MEMORY_RING_LAYOUT.active.radius +FILE_RING_LAYOUT.radius ``` -### Move all memory dashes outward +Keep spacing so hover and link glows do not visually merge. -File: +### Change file-dot size or base visibility -`ui/static/js/runtime/runtime-avatar.js` +File: `ui/static/js/runtime/runtime-avatar.js` -Increase: +Use: ```js -MEMORY_RING_LAYOUT.delayed.radius -MEMORY_RING_LAYOUT.l4.radius -MEMORY_RING_LAYOUT.active.radius +FILE_RING_LAYOUT.dotRadius ``` -Keep spacing between rings so hover glow does not visually merge. +and the `record.pinned ? 0.96 : 0.36` opacity branch inside `appendFileSignalRing()`. + +For hover/indirect-context intensity, edit the file selectors in `runtime-avatar.css`, not the dot geometry. ### Make runtime rings calmer -File: - -`ui/static/js/runtime/runtime-avatar.js` +File: `ui/static/js/runtime/runtime-avatar.js` Change: @@ -628,11 +847,19 @@ Change: const baseSpeed = 6 + random() * 18; ``` -### Make memory signal rings rotate faster +### Change punctuation stripes -File: +The trigger is in `appendOrbit()`: -`ui/static/js/runtime/runtime-avatar.js` +```js +if (/[!?]/.test(String(record.value || ""))) { + appendLongFieldStripes(orbitGroup, record, ringColor); +} +``` + +Change the regexp if you want different semantic punctuation. Change `appendLongFieldStripes()` only if you want different stripe count/height/span/opacity. + +### Make memory signal rings rotate faster Change `getMemoryRingAnimation()`: @@ -642,78 +869,98 @@ delayed: [40, 80], l4: [34, 70], ``` -Lower duration means faster rotation. +Lower duration means faster rotation. File-ring duration is separate in `appendFileSignalRing()`. ### Change hover glow intensity -File: +File: `ui/static/css/runtime-avatar.css` -`ui/static/css/runtime-avatar.css` +Edit the runtime/memory/file `is-memory-hover-hit` selectors. Remember that non-pinned file hover intentionally shares the secondary/indirect half-accent level. -Edit: +### Change citation/reference glow intensity -```css -.jin-avatar-orbit.is-memory-hover-hit, -.jin-avatar-counter-orbit.is-memory-hover-hit, -.jin-avatar-memory-dash.is-memory-hover-hit -``` +File: `ui/static/css/runtime-avatar.css` -### Change citation glow intensity +Edit the `is-runtime-cited`, `is-memory-reference-hit`, and relation-hit selectors for the relevant shape family. -File: +### Change avatar panel size -`ui/static/css/runtime-avatar.css` +File: `ui/static/css/base.css` Edit: ```css -.jin-avatar-memory-dash.is-runtime-cited, -.jin-avatar-orbit.is-runtime-cited, -.jin-avatar-counter-orbit.is-runtime-cited +--runtime-avatar-panel-size ``` -### Change avatar panel size +Then verify circular geometry, collapsed mode, and Win95 theme. The shell keeps `aspect-ratio: 1`, and the SVG uses `preserveAspectRatio: "xMidYMid meet"`. -File: +### Change chat processing pulse -`ui/static/css/base.css` +File: `ui/static/css/chat.css` Edit: ```css ---runtime-avatar-panel-size +.jin-stream-avatar.is-processing +@keyframes jin-chat-avatar-processing ``` -Then check collapsed mode and win95 theme. +Positioning/travel belongs in `chat.js`, especially `syncStreamAvatarPosition()` and the stream-avatar timing constants. ## Visual QA Checklist -Use this after avatar visual changes. +Use this after avatar visual/state changes. | Check | Expected result | |---|---| -| Page reload | Avatar appears in the runtime panel | -| Center click | Memory layers hide/show without avatar refresh; central light remains visible | +| Page reload | Radar avatar appears centered and circular in the runtime panel | +| Shell aura | Soft `--jin-color` aura breathes without turning into a harsh collapsed-state glow | +| Runtime update | Inner orbits refresh; entry animation is soft rather than a hard pop | +| Runtime value contains `?` or `!` | That orbit gets the legacy vertical stripe texture | +| Long runtime value without `?`/`!` | Length alone does not create stripes | +| Runtime diff changes | Ring speed and scaffold-ray energy respond without becoming noisy | +| Center click | Scaffold/runtime/memory dash layers hide without refresh; central light and current file ring remain visible | | Runtime memory row hover | Matching inner orbit glows | -| Long-term fact row hover | Matching L4 dash or dot glows | -| Delayed report row hover | Matching delayed dash glows, and linked L4 facts glow when fact links exist | +| L4 row hover | Matching L4 dash or archived dot glows | +| Delayed row hover | Matching delayed dash glows and linked L4 facts/file attachments react | | Active memory row hover | Matching active dash glows | -| Think citation hover | Matching orbit or dash gets citation glow | -| Pinned delayed report | Dash becomes brighter and whiter | -| Unpinned delayed report | Dash returns to normal delayed color unless it is still context-loaded | -| Loaded delayed report | Dash gets context glow and linked L4 facts are not archived by that report | -| Archived L4 fact | Dash arc fades away and the dim dot remains | -| Long-term facts panel | Archived L4 facts are not listed; anchored or loaded facts remain listed | -| Collapsed panel | Avatar keeps stable size | -| Win95 theme | Avatar still fits | -| Reduced motion | Orbit animations stop | +| File row/plaque hover | Matching non-pinned file dot gets the softer half-accent | +| Think citation/reference | Matching orbit, dash, dot, or uniquely named file gets the stronger citation/reference glow | +| Normal delayed report | Dim base dash at `0.36` | +| Runtime-loaded delayed report | Bright active dash at `0.82` even when not pinned | +| Pinned delayed report | Same strong active family, with pin state retained | +| Loaded report with ordinary `facts_ids` fact | Fact remains archived as a dot; load does not turn it back into a dash | +| Direct fact id used as any `anchor_fact_id` | That id is removed from the archive-id set; merged `source_fact_ids` can still affect final classification | +| Loaded report ordinary fact anchored by another report | Other report gets softer secondary-linked accent | +| Long-term facts panel | Archived ordinary report facts stay hidden; globally anchored facts stay listed | +| Persistent files | One outer dot per file, stable slot order by id | +| Pin a file | Same dot becomes bright white without jumping/restarting solely because of pin state | +| Unpin file while loaded report references it | Dot falls back to the softer indirect-context accent | +| Unpin file with no loaded-report link | Dot returns to normal blue state | +| Switch away from `[runtime]`, then receive L1 update | Visible tab stays put, but radar still updates to newest L1 snapshot | +| Before first model token | Chat response position already shows a clickable pulsing avatar | +| Expanded reasoning grows | Chat avatar follows the bottom of reasoning smoothly | +| Collapse reasoning | Chat avatar returns to the reasoning wrapper top instead of disappearing | +| Answer begins | Chat avatar moves to answer row, stops pulsing, stays settled | +| Retry/next unfinished stream | Processing avatar hands off from previous slot rather than duplicating | +| Abort/runtime end/disconnect | Processing pulse stops; empty placeholder is removed when appropriate | +| Collapsed memory panel | Radar keeps stable size | +| Win95 theme | Radar still fits | +| Reduced motion | Orbit/scaffold/pulse/entry animations are suppressed appropriately | ## Final Rule Of Thumb -If the change is about what the avatar means, edit the record collectors or L4 visibility helpers. +If the change is about **what a radar mark means**, edit the record collectors, delayed/L4/file link logic, or L4 archive helper. + +If the change is about **how the radar looks**, edit `runtime-avatar.js` constants/render helpers or `runtime-avatar.css`. + +If the change is about **whether a row/plaque and an avatar mark glow together**, check `buildAvatarMemoryHoverId()`, `data-avatar-memory-hover-id`, and then the cross-layer relation functions. + +If a file is directly pinned versus merely inherited through a loaded delayed report, preserve that distinction: direct = bright white; indirect = half-accent. -If the change is about how the avatar looks, edit `runtime-avatar.js` constants/functions or `runtime-avatar.css`. +If an ordinary L4 fact belongs to a report, loading that report should change emphasis, not archive semantics. Anchor ids are the structural exception at the archive-id level; for merged facts, remember that `source_fact_ids` also participate in the final archived match. -If the change is about whether a row and a dash glow together, check `buildAvatarMemoryHoverId()` and `data-avatar-memory-hover-id`. +If the change is about the **small avatar travelling through a streamed response**, use `chat.js` / `chat.css`, not the radar SVG code. -If the change is visible in browser but not after reload, bump the script or stylesheet query string in `ui/templates/index.html`. +If the browser still shows an old visual after reload, bump the relevant script/stylesheet query string in `ui/templates/index.html`. From 3738ea8cf6ca619d98d3a6f5507f75545145d177 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:08:59 +0300 Subject: [PATCH 068/168] chore: ui - update LIVE_AVATAR.md, remove chat stream avatar description --- LIVE_AVATAR.md | 79 +------------------------------------------------- 1 file changed, 1 insertion(+), 78 deletions(-) diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index 05b4e8d0..2a9b73ec 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -10,8 +10,6 @@ It is intentionally not a full architecture document. The goal is practical: if The live avatar is a memory radar with a persistent-file perimeter. The inner moving rings represent the current runtime memory snapshot. The three outer memory-signal rings represent delayed reports, L4 long-term facts, and active memory. A fourth, farther-out ring is made of file dots and represents the persistent files known to `window.JinFiles`. The center is the JIN accent light; it also feeds the scene tint. -The avatar in the chat stream is a separate presentation layer. It reuses the normal JIN role avatar, but its position and pulse communicate generation state: waiting/processing, reasoning, then settled answer. - Read the radar like this: | Visual cue | Meaning | @@ -48,12 +46,6 @@ The radar glow, shell aura, depth layer, entry softness, and reduced-motion beha `ui/static/css/runtime-avatar.css` -The chat-stream avatar is controlled separately: - -- `ui/static/js/chat.js` — creates the pre-token avatar, moves it with reasoning, hands it to the answer row, and settles/releases it. -- `ui/static/css/chat.css` — processing pulse, position transitions, and reduced-motion behavior. -- `ui/static/js/socket.js` and `ui/static/js/socket/event-handlers.js` — stop/release the processing avatar on abort, disconnect, error, and runtime end paths. - Other visual/state knobs: | Need | File | Change | @@ -683,55 +675,6 @@ For runtime, active, and delayed records, matching can fall back to exact normal Persistent file dots participate in the same reference layer. Their aliases include id/name/stored path variants, so JIN mentioning a unique file id or file name can light the corresponding dot. File SVG text identity is normalized from `name · contextPath`. -## Live Chat Stream Avatar - -The small avatar shown beside a model response is not the radar SVG. It is the normal role avatar placed in a movable stream slot so the UI shows model activity before and during streamed output. - -Files: - -- `ui/static/js/chat.js` -- `ui/static/css/chat.css` -- release paths in `ui/static/js/socket.js` and `ui/static/js/socket/event-handlers.js` - -### Before The First Token - -`startStreamMessage()` immediately creates the stream group and calls `activateStreamAvatar()`. The wrapper gets `is-awaiting-model`, and `.jin-stream-avatar-slot` appears at the response position before reasoning or answer text exists. - -The avatar itself receives: - -```text -jin-stream-avatar is-processing -``` - -The slot keeps the avatar clickable even though the slot container itself has `pointer-events: none`; the child `.jin-chat-avatar` restores `pointer-events: auto`. - -Processing pulse: - -- `1.18s` ease-in-out loop; -- opacity roughly `0.48 -> 0.96 -> 0.48`; -- scale roughly `0.90 -> 1.00 -> 0.90`; -- reduced-motion disables the pulse and leaves a stable `0.82` opacity. - -### During Reasoning - -When reasoning exists, `syncStreamAvatarPosition()` moves the slot to the reasoning block. - -- collapsed reasoning: avatar stays at the top of the reasoning wrapper; -- expanded reasoning: avatar follows the bottom edge of the growing reasoning content, offset by its own `28px` size; -- `ResizeObserver` watches reasoning height while no answer exists; -- collapse/expand triggers a short requestAnimationFrame tracking window so the avatar follows the layout transition rather than teleporting. - -The slot's normal `left`/`top` transitions are `0.24s`; handoff transform is `0.26s`. - -### Answer Handoff And Settle - -As soon as an answer row exists, the avatar moves to that row, `setStreamAvatarProcessing(false)` switches it to `is-settled`, and the reasoning resize observer is disconnected. A hidden `jin-stream-avatar-spacer` reserves the normal avatar width in the answer row so the absolute slot lands cleanly without shifting the bubble. - -If a new/retry stream takes over while a previous stream still has no answer, `activateStreamAvatar()` measures the old slot, removes it, and animates the new slot from the old screen position. This makes the avatar handoff continuous instead of spawning two processing avatars. - -### Abort / End / Disconnect - -`releaseActiveStreamAvatar()` is called from runtime-end, abort, disconnect, and error paths. If no visible reasoning/answer content exists, the placeholder avatar/wrapper is removed. If content already exists, the avatar stays with that content but stops processing pulse. ## Central Button @@ -895,18 +838,6 @@ Edit: Then verify circular geometry, collapsed mode, and Win95 theme. The shell keeps `aspect-ratio: 1`, and the SVG uses `preserveAspectRatio: "xMidYMid meet"`. -### Change chat processing pulse - -File: `ui/static/css/chat.css` - -Edit: - -```css -.jin-stream-avatar.is-processing -@keyframes jin-chat-avatar-processing -``` - -Positioning/travel belongs in `chat.js`, especially `syncStreamAvatarPosition()` and the stream-avatar timing constants. ## Visual QA Checklist @@ -939,15 +870,9 @@ Use this after avatar visual/state changes. | Unpin file while loaded report references it | Dot falls back to the softer indirect-context accent | | Unpin file with no loaded-report link | Dot returns to normal blue state | | Switch away from `[runtime]`, then receive L1 update | Visible tab stays put, but radar still updates to newest L1 snapshot | -| Before first model token | Chat response position already shows a clickable pulsing avatar | -| Expanded reasoning grows | Chat avatar follows the bottom of reasoning smoothly | -| Collapse reasoning | Chat avatar returns to the reasoning wrapper top instead of disappearing | -| Answer begins | Chat avatar moves to answer row, stops pulsing, stays settled | -| Retry/next unfinished stream | Processing avatar hands off from previous slot rather than duplicating | -| Abort/runtime end/disconnect | Processing pulse stops; empty placeholder is removed when appropriate | | Collapsed memory panel | Radar keeps stable size | | Win95 theme | Radar still fits | -| Reduced motion | Orbit/scaffold/pulse/entry animations are suppressed appropriately | +| Reduced motion | Orbit/scaffold/entry animations are suppressed appropriately | ## Final Rule Of Thumb @@ -961,6 +886,4 @@ If a file is directly pinned versus merely inherited through a loaded delayed re If an ordinary L4 fact belongs to a report, loading that report should change emphasis, not archive semantics. Anchor ids are the structural exception at the archive-id level; for merged facts, remember that `source_fact_ids` also participate in the final archived match. -If the change is about the **small avatar travelling through a streamed response**, use `chat.js` / `chat.css`, not the radar SVG code. - If the browser still shows an old visual after reload, bump the relevant script/stylesheet query string in `ui/templates/index.html`. From 372aec3800c211002dd53f4cbf2569e775b9e373 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:51:34 +0300 Subject: [PATCH 069/168] chore: ui - add expand/collapse trigger for all context blocks --- ui/static/js/logger/trace-modal.js | 168 +++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 21 deletions(-) diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index aeb704bf..af7e13ed 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -2035,9 +2035,32 @@ function renderContextBody(parent, content) { ); } +function setContextCardCollapsed( + card, + collapsed, +) { + card.classList.toggle( + "is-collapsed", + collapsed + ); + + const header = + card.querySelector( + ".jin-context-card-header" + ); + + if (header) { + header.setAttribute( + "aria-expanded", + collapsed ? "false" : "true" + ); + } +} + function appendContextCard( parent, block, + onToggle = null, ) { const card = contextElement( @@ -2066,7 +2089,9 @@ function appendContextCard( ); header.title = - "Double-click to collapse / expand"; + "Click to collapse / expand"; + header.style.cursor = + "pointer"; header.tabIndex = 0; header.setAttribute("role", "button"); header.setAttribute("aria-expanded", "true"); @@ -2099,18 +2124,22 @@ function appendContextCard( const toggle = () => { const collapsed = - card.classList.toggle( + !card.classList.contains( "is-collapsed" ); - header.setAttribute( - "aria-expanded", - collapsed ? "false" : "true" + setContextCardCollapsed( + card, + collapsed ); + + if (typeof onToggle === "function") { + onToggle(); + } }; header.addEventListener( - "dblclick", + "click", toggle ); header.addEventListener( @@ -2153,6 +2182,7 @@ function appendContextCard( ); } parent.appendChild(card); + return card; } function renderContextSnapshotTrace(snapshot) { @@ -2171,12 +2201,25 @@ function renderContextSnapshotTrace(snapshot) { "jin-context-overview-badges" ); - overview.appendChild( + const collapseAllToggle = contextElement( "div", "jin-context-overview-title", - "CONTEXT WINDOW" - ) + "COLLAPSE ALL" + ); + + collapseAllToggle.tabIndex = 0; + collapseAllToggle.setAttribute( + "role", + "button" + ); + collapseAllToggle.style.cursor = + "pointer"; + collapseAllToggle.style.userSelect = + "none"; + + overview.appendChild( + collapseAllToggle ); badges.appendChild( contextBadge(`${blocks.length} blocks`) @@ -2200,24 +2243,107 @@ function renderContextSnapshotTrace(snapshot) { "jin-context-stack" ); - blocks.forEach((block) => { - appendContextCard(stack, block); - }); + const getCards = () => + Array.from(stack.children).filter( + (element) => + element.classList.contains( + "jin-context-card" + ) + ); - appendContextCard( - stack, - { - title: "USER PROMPT / CONTEXT PAYLOAD", - content: - snapshot.userPrompt || "", - attributes: [], - xml: false, + const syncCollapseAllToggle = () => { + const cards = + getCards(); + const allCollapsed = + cards.length > 0 + && cards.every((card) => + card.classList.contains( + "is-collapsed" + ) + ); + const label = + allCollapsed + ? "EXPAND ALL" + : "COLLAPSE ALL"; + + collapseAllToggle.textContent = + label; + collapseAllToggle.title = + allCollapsed + ? "Expand all context blocks" + : "Collapse all context blocks"; + collapseAllToggle.setAttribute( + "aria-label", + collapseAllToggle.title + ); + }; + + const toggleAllCards = () => { + const cards = + getCards(); + const allCollapsed = + cards.length > 0 + && cards.every((card) => + card.classList.contains( + "is-collapsed" + ) + ); + + cards.forEach((card) => { + setContextCardCollapsed( + card, + !allCollapsed + ); + }); + + syncCollapseAllToggle(); + }; + + collapseAllToggle.addEventListener( + "click", + toggleAllCards + ); + collapseAllToggle.addEventListener( + "keydown", + function (event) { + if ( + event.key !== "Enter" + && event.key !== " " + ) { + return; + } + + event.preventDefault(); + toggleAllCards(); } ); - stack.lastElementChild.classList.add( + + blocks.forEach((block) => { + appendContextCard( + stack, + block, + syncCollapseAllToggle + ); + }); + + const userCard = + appendContextCard( + stack, + { + title: "USER PROMPT / CONTEXT PAYLOAD", + content: + snapshot.userPrompt || "", + attributes: [], + xml: false, + }, + syncCollapseAllToggle + ); + + userCard.classList.add( "jin-context-card-user" ); + syncCollapseAllToggle(); traceModalContent.appendChild(stack); } From 53c1085fe369fa62ed564be38c8913530506b12c Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:43:12 +0300 Subject: [PATCH 070/168] chore: ui - add ARCHITECTURE.md - update schema.jpg --- ARCHITECTURE.md | 262 ++++++++++++++++++++++++++++++++++++ ui/static/images/schema.jpg | Bin 366040 -> 435111 bytes 2 files changed, 262 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..b7b4c323 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,262 @@ +# JIN Core Engine Architecture + +This document maps the runtime boundaries, state ownership, data flow, persistence, and extension points in JIN Core Engine. + +`README.md` covers product behavior, setup, configuration, and the public feature surface. `LIVE_AVATAR.md` covers avatar rendering and visual-state rules. + +--- + +## 1. Runtime Structure + +JIN is built around one live runtime state shared by the WebSocket session, model roles, memory engines, runtime actions, persistence, and browser projections. + +The main execution path is: + +```text +Browser UI ↔ WebSocket ↔ RuntimeContext → AgentRuntime → Brain + ↕ ↕ + Service Action Engine + ↕ ↕ + Memory Files / Skills / Search +``` + +`RuntimeContext` in `runtime/runtime_context.py` is the in-process state hub for an active runtime. It carries model clients, active streams, current memory, delayed reports, active memory, L4 state, attachments, action history, tool results, telemetry, session counters, recovery state, and background-task references. + +The surrounding modules have narrow ownership: + +| Area | Main responsibility | +|---|---| +| `websocket/` | connection lifecycle, bootstrap, queueing, user-turn orchestration | +| `agent/` | planner, Brain execution, validation, optional translation | +| `runtime/` | live state, streams, memory engines, telemetry, recovery | +| `contracts/` | runtime-action definitions and Brain-facing action rules | +| `utils/actions/` | action payload handling and state mutations | +| `utils/context/` | Brain context serialization | +| `clients/` | OpenAI-compatible model and search clients | +| `ui/` | browser projections of runtime state | + +--- + +## 2. Connection and Session Lifecycle + +The browser uses `/ws/chat` as the live runtime channel. `websocket/bootstrap.py` creates a new `RuntimeContext` or reconnects the socket to resumable state, then hydrates memory, delayed reports, files, counters, and browser-provided session data. + +Each context owns a `PendingRequestQueue`. User messages and internal continuation ticks run serially through that queue, which keeps turn order stable while generation, actions, and memory updates overlap in time. + +A socket and a logical runtime have separate identities. Reconnect can attach a fresh transport to an existing context. Explicit session bootstrap can restore saved L1/L3 state and related runtime metadata. + +Core files: `websocket/__init__.py`, `websocket/bootstrap.py`, `websocket/tasks.py`, `runtime/registry.py`. + +--- + +## 3. User Turn + +`process_message()` in `websocket/messages.py` coordinates a normal turn. + +1. Hydrate attachment ids against the persistent file store. +2. Apply browser runtime signals such as Active Memory, idle time, counters, feedback, and avatar state. +3. Match Delayed Memory tags against text typed by the user. +4. Create the turn and action-sequence ids. +5. Build `AgentState` and run `AgentRuntime`. +6. Stream reasoning, visible content, runtime-action state, and telemetry to the browser. +7. Record the completed turn and action history. +8. Schedule the Service-model L1/L2 update. + +A later user request waits for a pending memory update before entering Brain execution. This keeps the next prompt aligned with the latest accepted turn. + +Interrupted turns use the interrupted-memory path in `runtime/L1_memory.py`, so partial work can still update live state with separate lifecycle metadata. + +--- + +## 4. Agent and Model Boundary + +`AgentRuntime` executes a small node plan through `agent/router.py`. Brain is the visible reasoning and response role. Service handles background memory work and supporting inference. Translator is an optional internal stage around Brain execution. + +Roles are resolved separately from physical model endpoints. `USE_SERVICE_AS_BRAIN` can assign the Service endpoint to the Brain role while the rest of the runtime continues to address it as Brain. + +Model access goes through the OpenAI-compatible clients in `clients/` and `runtime/client.py`. Provider metadata can supply the loaded context window for live budgeting and telemetry. + +Core files: `agent/runtime.py`, `agent/nodes/brain.py`, `agent/nodes/translation.py`, `clients/registry.py`, `utils/brain_client_utils.py`. + +--- + +## 5. Brain Context + +`rules/brain_context_builder.py` assembles the Brain system context from current runtime state. + +The builder combines the state relevant to the current tick: L1, L2, L3, L4, Active Memory, loaded Delayed Memory, attachments, skills, tool results, TODO state, feedback, session counters, recent visible turns, selected previous reasoning, and enabled runtime-action rules. + +The assembled prompt is a transient projection. Canonical state remains in `RuntimeContext`, browser stores, and filesystem stores. + +Action follow-ups use a dedicated context path in `agent/nodes/brain.py`. The follow-up carries the original sequence request, trusted action results, current runtime state, preserved attachments, and selected reasoning edges. The sequence stays under the original turn id and action-sequence id. + +`BRAIN_MAX_FOLLOWUPS` bounds continuation ticks. At the ceiling, the runtime switches to a final response tick with action execution disabled. + +--- + +## 6. Stream and Runtime Actions + +`RuntimeStream` in `runtime/stream.py` separates three output channels from Brain generation: reasoning, visible answer text, and private runtime-action markers. + +Action markers are matched against contracts from `contracts/*.json`. `contracts/rules_assembler.py` exposes enabled actions to the Brain, and `utils/actions/dispatcher.py` routes accepted calls to action-specific handlers. + +The action path is: + +```text +Brain marker → contract → payload normalization → guard → handler + → runtime mutation / external result → action event + → trusted tool result → optional Brain follow-up +``` + +Behavior guards in `runtime/action_guard.py` can require user intent or confirmation before a state-changing action proceeds. Confirmation state belongs to the runtime and survives the continuation flow. + +Actions share sequence history through `runtime_action_events`, `runtime_session_action_history`, and tool-result state on `RuntimeContext`. UI action bubbles and logger entries consume the same event stream. + +--- + +## 7. Memory State + +The memory systems use separate stores and update schedules because they feed different parts of the runtime. + +| Layer | Runtime role | Update path | +|---|---|---| +| **L1** | current task and interaction state | Service update after a turn | +| **L2** | repeated interaction patterns | derived from L1 diff history | +| **L3** | compact session handoff | `SAVE_SESSION` flow | +| **Facts Memory** | durable candidates collected from runtime state | L1/facts synchronization | +| **L4** | persistent long-term facts | idle extraction, merge, explicit edits | +| **Delayed Memory** | larger contextual reports loaded on demand | actions, UI, tag triggers | +| **Active Memory** | unresolved commitments and conditions | create/resolve actions and browser state | + +### L1 and L2 + +`runtime/L1_memory.py` maintains the current compact runtime memory, versioned snapshots, diffs, pending-turn batches, and response-feedback integration. `runtime/L2_memory.py` consumes L1 change history to update slower pattern memory. + +### L3 + +`runtime/L3_memory.py` creates the saved session handoff. `SAVE_SESSION` completes its memory work before the related Brain follow-up continues, so that continuation sees the saved state produced by the same sequence. + +### Facts Memory and L4 + +`runtime/L4_memory.py` runs L4 as a staged pipeline. Facts Memory supplies candidates, the idle extraction phase selects durable material, and the merge phase reconciles candidates with the current long-term store. Explicit L4 edits are tracked so automatic merge work preserves edits from the active turn. + +L4 records keep source lineage and links to Delayed Memory. Merge, delete, and restore operations update those references through the reconciliation helpers in `runtime/L4_memory.py`. + +Delayed reports can move ordinary L4 facts behind report content through `facts_ids`. `anchor_fact_ids` keeps selected facts directly exposed. Prompt assembly, the long-term panel, and avatar state use the same archive/anchor classification. + +### Delayed Memory + +Delayed reports are persistent JSON objects with content, tags, fact links, attachment links, and load metadata. `utils/delayed_memory_file_store.py` owns normalization and file persistence; `websocket/bootstrap.py` hydrates them into runtime state. + +Reports enter context through runtime actions, UI state, pin state, or user-text tag matches. `attachments_ids`, `facts_ids`, and `anchor_fact_ids` connect reports to the file and L4 stores. + +### Active Memory + +Active Memory records hold unresolved runtime commitments. They are carried into the Brain context near L1 and keep independent ids and resolve state. Browser bootstrap and runtime actions synchronize the active records with `RuntimeContext`. + +--- + +## 8. Files, Skills, and External Work + +Persistent files are managed by `utils/attached_files_store.py`. Each file has a stable id, metadata record, pin state, and filesystem entry under `assets/files/`. Store reconciliation removes references to missing physical files and keeps pin state consistent. + +A file can be present directly through pin/attach state or indirectly through a loaded Delayed Memory report. The same id is used by prompt assembly, report links, file actions, memory panels, and Live Avatar state. + +Skills are context resources under `assets/skills/`. Runtime actions load and unload skill instructions for a sequence. Python skill execution is limited to `.py` files inside the selected skill directory and uses bounded execution controls. + +Search actions produce trusted tool results. `WEB_SEARCH` uses the configured search provider. `runtime/deep_web_search.py` runs the bounded multi-query Service workflow used by `DEEP_WEB_SEARCH`. + +--- + +## 9. Persistence and Reconciliation + +JIN splits persistence by ownership. + +Browser storage keeps session-facing state used for resume and direct UI interaction. Server-side files keep durable reports, long-term facts, uploaded files, and logs. + +| State | Storage | +|---|---| +| Delayed Memory | `memory/delayed/*.json` | +| L4 long-term facts | `memory/facts/long_term_facts.json` | +| persistent files | `assets/files/` plus file index | +| chat/reasoning logs | `logs/` | +| session-facing runtime state | browser storage | + +Bootstrap and mutation paths reconcile ids across stores. File deletion clears file references, L4 merge/delete/restore remaps report references, and delayed-report synchronization filters stale links. This keeps cross-layer ids usable after independent edits from the UI or runtime actions. + +Core files: `ui/static/js/runtime/runtime-storage.js`, `utils/delayed_memory_file_store.py`, `utils/long_term_facts_file_store.py`, `utils/attached_files_store.py`, `websocket/bootstrap.py`. + +--- + +## 10. Recovery and Limits + +Generation state is tracked on `RuntimeContext` and `RuntimeStream`. User abort cancels the active task, closes stream state, records aborted actions, and can schedule the interrupted L1 update. + +Stream validation detects repetition and malformed generation states. Context/output-limit recovery can start a continuation tick with preserved reasoning edges and the current sequence state. + +Runtime loops have explicit ceilings: Brain follow-ups, Deep Web Search calls, skill execution, action retries, and related worker paths all use configured or hard limits. + +Core files: `runtime/stream.py`, `utils/stream_validator.py`, `utils/runtime_action_abort.py`, `agent/nodes/brain.py`, `runtime/deep_web_search.py`. + +--- + +## 11. UI Synchronization + +The browser receives typed WebSocket events and updates the relevant projection of runtime state. Memory panels, context view, logger, action bubbles, attachment plaques, reasoning references, and Live Avatar share ids from the runtime objects they represent. + +Typical synchronization paths are: + +| Runtime change | Browser projections | +|---|---| +| L1 snapshot | runtime panel, context state, Live Avatar | +| L4 update | long-term panel, delayed links, Live Avatar | +| delayed load/pin | report state, context state, linked facts/files, Live Avatar | +| file pin/delete | attachment state, report links, context state, Live Avatar | +| runtime action | action bubble, logger, sequence history | +| reasoning/reference match | text highlight, matching memory/file/avatar item | + +The Live Avatar uses the same memory and file identities as the panels. Its rendering rules and visual priorities live in `LIVE_AVATAR.md`. + +UI state-only updates prefer in-place synchronization where stable identity matters. Full rerenders are used when the represented object set changes. + +--- + +## 12. Change Rules + +These rules describe the boundaries used by the current codebase when adding behavior. + +**Persistent state gets one canonical owner.** New persistent data needs a store, stable identity, bootstrap path, mutation path, and reconciliation rules. + +**Prompt sections come from canonical state.** Context builders serialize current runtime state for a Brain tick; mutations happen through the owning subsystem. + +**Runtime actions cross the contract boundary.** A new action needs a contract, payload handling, guard semantics where required, dispatcher integration, result/follow-up behavior, and tests. + +**Cross-entity ids are maintained during mutation.** Merge, delete, restore, pin, load, and detach operations update linked reports, facts, files, and UI projections. + +**Internal continuations keep sequence identity.** Tool results, attachments, reasoning recovery, and follow-up limits stay attached to the original user sequence. + +**UI primitives map to semantic states.** New backend behavior reuses existing loaded, pinned, active, archived, linked, referenced, and hover states when those states describe it accurately. A new visual primitive is added when it represents a new persistent distinction in runtime state. + +**Long-running paths are bounded.** Agent continuations, workers, retries, and executable skills keep explicit ceilings. + +--- + +## 13. Tests + +The suite combines Python runtime tests with browser/client contract tests. Runtime tests cover agent routing, memory engines, actions, search, recovery, persistence, and token budgeting. `*_client_contract.py` tests protect the state and event shapes shared by Python and JavaScript. + +For architecture changes, the useful test boundary is the full state path: canonical store → runtime mutation → emitted event → browser contract. Changes that alter cross-layer ids also need merge/delete/restore or load/unload coverage. + +The current test commands and model-dependent probes are documented in `README.md`. + +--- + +## 14. Main Extension Points + +For a new runtime action, start in `contracts/`, then connect the handler in `utils/actions/`, add any guard or follow-up semantics, and cover the emitted event/state mutation in tests. + +For a new memory behavior, start from its owner and lifetime: L1/L2/L3/L4, Facts Memory, Delayed Memory, or Active Memory. Add persistence and reconciliation only for state that survives the active context. + +For a new browser projection, consume the existing semantic ids and runtime events from `ui/static/js/runtime/`. Avatar-specific rendering stays in `runtime-avatar.js` and `runtime-avatar.css`; the visual contract stays in `LIVE_AVATAR.md`. + +For a new model-side context source, add serialization in `utils/context/` or `rules/brain_context_builder.py` and keep the underlying state in its owning runtime/store module. diff --git a/ui/static/images/schema.jpg b/ui/static/images/schema.jpg index 07be7c541f3b1e4c5edd227f737cd7a0164564ca..f7593105805bf6694cf67f8d36b21990e75e48df 100644 GIT binary patch literal 435111 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
      4QK5N1 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 z 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 p8$8xFbA} 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ÀJBJZuPu~*t3g>NNR)JzRAl}Z79 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 literal 366040 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

      wI3bl*kwrG)uUs#}G8%7&ZDKjLZH;iso9>n>PtB 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

      - + --> @@ -511,10 +513,10 @@ - + - + @@ -528,12 +530,12 @@ - + - + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index c58d2b18..ea755295 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -90,7 +90,7 @@ normalize_delayed_memory_fact_ids, normalize_delayed_memory_fact_roles, normalize_long_term_fact_ids, - parse_delayed_memory_content_payload, + parse_delayed_memory_payload, ) from .web_search_utils import extract_search_query @@ -154,7 +154,7 @@ "normalize_delayed_memory_fact_ids", "normalize_delayed_memory_fact_roles", "normalize_long_term_fact_ids", - "parse_delayed_memory_content_payload", + "parse_delayed_memory_payload", "parse_idle_seconds", "parse_jin_position_payload", "parse_jin_size_payload", diff --git a/utils/actions/active_memory_actions.py b/utils/actions/active_memory_actions.py index 7eeb801e..4945e0fc 100644 --- a/utils/actions/active_memory_actions.py +++ b/utils/actions/active_memory_actions.py @@ -394,6 +394,9 @@ async def apply_save_active_memory_actions( if active_memory_id: completed_event["active_memory_id"] = active_memory_id + if active_memory_line: + completed_event["active_memory"] = active_memory_line + await emit(with_action_context( completed_event )) @@ -473,6 +476,7 @@ async def apply_update_active_memory_actions( event = { "type": "runtime_action", "action": "update_active_memory", + "id": result.get("id", ""), "status": "completed" if result.get("ok") else "failed", "display_name": display_name, "close_tag": runtime_action_has_close_tag( @@ -494,6 +498,10 @@ async def apply_update_active_memory_actions( "active_memory_id": result.get("id", ""), "active_memory_title": result.get("title", ""), "active_memory_changes": result.get("changes", []), + "active_memory_requested_changes": result.get( + "requested_changes", + [], + ), } if not result.get("ok"): diff --git a/utils/actions/active_memory_utils.py b/utils/actions/active_memory_utils.py index 0c4e0248..2feebff5 100644 --- a/utils/actions/active_memory_utils.py +++ b/utils/actions/active_memory_utils.py @@ -1,3 +1,4 @@ +import json import re import secrets import string @@ -141,6 +142,52 @@ def extract_active_memory_creation_custom_fields( ) -> tuple[str, tuple[tuple[str, str], ...]]: text = str(value or "").rstrip() + + if text.lstrip().startswith("{"): + try: + payload_pairs = json.loads( + text, + object_pairs_hook=lambda pairs: pairs, + ) + except (TypeError, ValueError, json.JSONDecodeError): + return "", () + + if not isinstance(payload_pairs, list): + return "", () + + conditions = "" + custom_fields_by_name = {} + + for raw_name, raw_value in payload_pairs: + field_name = str(raw_name or "").strip().casefold() + + if field_name == "conditions": + conditions = normalize_active_memory_custom_field_value( + raw_value + ) + continue + + normalized_name = normalize_active_memory_custom_field_name( + field_name + ) + normalized_value = normalize_active_memory_custom_field_value( + raw_value + ) + + if not normalized_name or not normalized_value: + continue + + # JSON parsers conventionally keep the last duplicate key. Do the + # same after name normalization instead of rejecting the complete + # SAVE_ACTIVE_MEMORY payload. + custom_fields_by_name[normalized_name] = normalized_value + + custom_fields = tuple( + custom_fields_by_name.items() + )[:ACTIVE_MEMORY_CUSTOM_FIELD_LIMIT] + + return conditions, custom_fields + reversed_fields = [] seen = set() diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index a0fe6426..bc3ea141 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -26,7 +26,7 @@ RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_DETACH_FILE, RUNTIME_ACTION_RESOLVE_TODO, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, RUNTIME_ACTION_WEB_SEARCH, ) from contracts.rules_assembler import ( @@ -69,9 +69,8 @@ select_non_overlapping_regexp_matches, ) from .save_delayed_memory_utils import ( - DELAYED_MEMORY_FIELD_RE, build_save_delayed_memory_payload, - parse_delayed_memory_content_payload, + parse_delayed_memory_payload, ) from .web_search_utils import ( build_web_search_payload, @@ -457,8 +456,7 @@ def normalize_runtime_action_name( normalized_name = normalized_name[4:] aliases = { - "SAVE_DELAYED_MEMORY": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, - "SAVE_DELAYED_MEMORY_CONTENT": RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + "SAVE_DELAYED_MEMORY": RUNTIME_ACTION_SAVE_DELAYED_MEMORY, "SAVE_ACTIVE_MEMORY": RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, "RESOLVE_ACTIVE_MEMORY": RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, "USE_ASSETS": RUNTIME_ACTION_ASSET_ACTION, @@ -527,7 +525,7 @@ def normalize_runtime_action_names( RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY ) - if normalized_name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: + if normalized_name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY: normalized_names.append( RUNTIME_ACTION_LOAD_DELAYED_MEMORY ) @@ -589,7 +587,7 @@ def build_deep_web_search_payload( RUNTIME_ACTION_CREATE_TODO_LIST: build_create_todo_list_payload, RUNTIME_ACTION_RESOLVE_TODO: build_resolve_action_payload, RUNTIME_ACTION_CHECK_TODO: build_check_todo_payload, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: build_save_delayed_memory_payload, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY: build_save_delayed_memory_payload, RUNTIME_ACTION_LOAD_DELAYED_MEMORY: build_load_delayed_memory_payload, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY: build_resolve_action_payload, RUNTIME_ACTION_ATTACH_FILE: build_resolve_action_payload, @@ -2022,15 +2020,15 @@ def flush_result(self) -> RuntimeActionResult: ) delayed_memory_marker, _ = _runtime_action_marker_config( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + RUNTIME_ACTION_SAVE_DELAYED_MEMORY ) delayed_memory_start_re = compile_runtime_action_start_regexp( delayed_memory_marker, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, ) delayed_memory_end_re = compile_runtime_action_end_regexp( delayed_memory_marker, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, ) delayed_memory_start = delayed_memory_start_re.match( pending @@ -2047,32 +2045,12 @@ def flush_result(self) -> RuntimeActionResult: delayed_memory_start.end(): ].strip() - present_fields = { - str( - match.group(1) - or "" - ).strip().casefold() - for match in DELAYED_MEMORY_FIELD_RE.finditer( - payload - ) - } - - if ( - { - "title", - "summary", - "tags", - "body", - }.issubset( - present_fields - ) - and parse_delayed_memory_content_payload( - payload - ) + if parse_delayed_memory_payload( + payload ): pending = ( pending.rstrip() - + "\n" + + "\n" ) if _unclosed_internal_action_request_start( diff --git a/utils/actions/delayed_memory_actions.py b/utils/actions/delayed_memory_actions.py index c2a9d964..b81b8780 100644 --- a/utils/actions/delayed_memory_actions.py +++ b/utils/actions/delayed_memory_actions.py @@ -1,7 +1,7 @@ from copy import deepcopy from contracts.rules_assembler import ( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, get_runtime_action_display_name, runtime_action_has_close_tag, ) @@ -361,7 +361,7 @@ async def apply_save_delayed_memory_actions( history_text = build_delayed_memory_history_text({ "ok": True, - "action": "save_delayed_memory_content", + "action": "save_delayed_memory", "id": report_id, "title": str( report_value.get( @@ -383,7 +383,7 @@ async def apply_save_delayed_memory_actions( saved_result = { "ok": True, - "action": "save_delayed_memory_content", + "action": "save_delayed_memory", "destination": ( "delayed_memory_reports (Delayed Memory storage)" ), @@ -485,7 +485,7 @@ async def apply_save_delayed_memory_actions( ) and event.get( "name" - ) == "save_delayed_memory_content" + ) == "save_delayed_memory" ]) action_sequence = max( current_action_sequence + 1, @@ -505,19 +505,19 @@ async def apply_save_delayed_memory_actions( action_sequence, ) action_id = build_runtime_action_id( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, action_sequence, ) await emit(with_action_context({ "type": "runtime_action", - "action": "save_delayed_memory_content", + "action": "save_delayed_memory", "id": action_id, "status": "completed", "display_name": get_runtime_action_display_name( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + RUNTIME_ACTION_SAVE_DELAYED_MEMORY ), "close_tag": runtime_action_has_close_tag( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + RUNTIME_ACTION_SAVE_DELAYED_MEMORY ), "text": ( f"Saved delayed memory: {report_title}" diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index fae806ee..3ed6badf 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -18,7 +18,7 @@ RUNTIME_ACTION_DETACH_FILE, RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_RESOLVE_TODO, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY, RUNTIME_ACTION_DEEP_WEB_SEARCH, @@ -105,6 +105,7 @@ build_delayed_memory_report, collect_context_active_memory_slot_ids, collect_context_active_memory_texts, + normalize_update_active_memory_payload_reference, normalize_active_memory_runtime_payload, resolve_runtime_action_user_message, ) @@ -730,7 +731,7 @@ def accept_runtime_action_once_per_message( if ( action.name - == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + == RUNTIME_ACTION_SAVE_DELAYED_MEMORY ): rejected_report = build_delayed_memory_report( context, @@ -833,7 +834,7 @@ def accept_runtime_action_once_per_message( ) continue - if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT: + if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY: save_delayed_memory_key = str( action.payload or "" @@ -936,8 +937,12 @@ def accept_runtime_action_once_per_message( continue if action.name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY: + normalized_payload, _, _ = normalize_update_active_memory_payload_reference( + context, + action.payload, + ) active_memory_id, update_fields = parse_update_active_memory_payload( - action.payload + normalized_payload ) if not active_memory_id or not update_fields: @@ -1639,7 +1644,7 @@ def accept_runtime_action_once_per_message( save_delayed_memory_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT + if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY ] load_delayed_memory_actions = [ diff --git a/utils/actions/save_active_memory_utils.py b/utils/actions/save_active_memory_utils.py index 1671ec68..eafe047d 100644 --- a/utils/actions/save_active_memory_utils.py +++ b/utils/actions/save_active_memory_utils.py @@ -1,3 +1,4 @@ +import json import re from contracts.rules_assembler import ( @@ -68,6 +69,17 @@ def get_save_active_memory_marker_fields( if not marker_fields: return () + if marker_fields.lstrip().startswith("{"): + try: + placeholder = json.loads(marker_fields) + except (TypeError, ValueError, json.JSONDecodeError): + return () + + if isinstance(placeholder, dict) and "conditions" in placeholder: + return ("conditions",) + + return () + fields = [] for field in marker_fields.split("|"): diff --git a/utils/actions/save_delayed_memory_utils.py b/utils/actions/save_delayed_memory_utils.py index 94424ab5..664f6718 100644 --- a/utils/actions/save_delayed_memory_utils.py +++ b/utils/actions/save_delayed_memory_utils.py @@ -330,7 +330,7 @@ def normalize_delayed_memory_tags(value) -> list[str]: return tags -def parse_delayed_memory_content_payload( +def parse_delayed_memory_payload( payload: str, *, created_session_id: str = "", @@ -348,80 +348,108 @@ def parse_delayed_memory_content_payload( if not text: return {} - field_matches = list( - DELAYED_MEMORY_FIELD_RE.finditer( + fields = None + + try: + parsed = json.loads( text ) - ) - - if not field_matches: - return {} - - fields = {} + except json.JSONDecodeError: + parsed = None - for index, match in enumerate( - field_matches + if isinstance( + parsed, + dict, ): - field_name = match.group( - 1 - ).casefold() - inline_value = ( - match.group( - 2 + if "title" in parsed: + fields = parsed + elif len(parsed) == 1: + nested = next( + iter(parsed.values()) + ) + if isinstance( + nested, + dict, + ) and "title" in nested: + fields = nested + + if fields is None: + # Compatibility path for reports produced before the JSON marker + # contract. New prompts never advertise this text-field format. + field_matches = list( + DELAYED_MEMORY_FIELD_RE.finditer( + text ) - or "" - ).strip() - next_start = ( - field_matches[index + 1].start() - if index + 1 < len(field_matches) - else len(text) - ) - block_value = text[ - match.end():next_start - ].strip( - "\n" ) - if field_name == "body": - value = "\n".join( - part - for part in ( - inline_value, - block_value, + if not field_matches: + return {} + + fields = {} + + for index, match in enumerate( + field_matches + ): + field_name = match.group( + 1 + ).casefold() + inline_value = ( + match.group( + 2 ) - if part + or "" ).strip() - elif field_name in { - "anchor_fact_ids", - "facts_ids", - "absorbed_fact_ids", - "long_term_facts_ids", - }: - value = normalize_long_term_fact_ids( - " ".join( + next_start = ( + field_matches[index + 1].start() + if index + 1 < len(field_matches) + else len(text) + ) + block_value = text[ + match.end():next_start + ].strip( + "\n" + ) + + if field_name == "body": + value = "\n".join( part for part in ( inline_value, block_value, ) if part + ).strip() + elif field_name in { + "anchor_fact_ids", + "facts_ids", + "absorbed_fact_ids", + "long_term_facts_ids", + }: + value = normalize_long_term_fact_ids( + " ".join( + part + for part in ( + inline_value, + block_value, + ) + if part + ) ) - ) - elif field_name == "attachments_ids": - value = normalize_delayed_memory_attachment_ids( - " ".join( - part - for part in ( - inline_value, - block_value, + elif field_name == "attachments_ids": + value = normalize_delayed_memory_attachment_ids( + " ".join( + part + for part in ( + inline_value, + block_value, + ) + if part ) - if part ) - ) - else: - value = inline_value + else: + value = inline_value - fields[field_name] = value + fields[field_name] = value title = str( fields.get( @@ -494,7 +522,7 @@ def build_save_delayed_memory_payload( placeholder_payloads=(), ) -> str | None: - report = parse_delayed_memory_content_payload( + report = parse_delayed_memory_payload( query ) diff --git a/utils/actions/update_active_memory_utils.py b/utils/actions/update_active_memory_utils.py index 3914703b..93330caf 100644 --- a/utils/actions/update_active_memory_utils.py +++ b/utils/actions/update_active_memory_utils.py @@ -1,7 +1,9 @@ +import json import re from .action_payload_utils import _build_internal_action_payload from .active_memory_utils import ( + ACTIVE_MEMORY_RESERVED_CUSTOM_FIELD_NAMES, normalize_active_memory_custom_field_name, normalize_active_memory_custom_field_value, ) @@ -32,13 +34,183 @@ def build_update_active_memory_payload( ) +def _parse_update_active_memory_json_payload( + payload: str, +) -> tuple[str, tuple[tuple[str, str], ...]]: + + text = str( + payload or "" + ).strip() + + if not text.startswith("{"): + return "", () + + try: + data = json.loads(text) + except (TypeError, ValueError): + return "", () + + if not isinstance( + data, + dict, + ): + return "", () + + active_memory_id = str( + data.get("active_memory_id") + or data.get("id") + or "" + ).strip().casefold() + + if not ACTIVE_MEMORY_UPDATE_ID_RE.fullmatch( + active_memory_id + ): + return "", () + + raw_fields = ( + data.get("fields") + if "fields" in data + else data.get("updates") + ) + + if raw_fields is None: + raw_fields = { + key: value + for key, value in data.items() + if str(key or "").strip().casefold() + not in { + "active_memory_id", + "id", + } + } + + if not isinstance( + raw_fields, + dict, + ) or not raw_fields: + return "", () + + changes = [] + seen = set() + + for raw_name, raw_value in raw_fields.items(): + raw_field_name = str( + raw_name or "" + ).strip().casefold() + + if raw_field_name in ACTIVE_MEMORY_RESERVED_CUSTOM_FIELD_NAMES: + continue + + if isinstance( + raw_value, + (dict, list), + ): + return "", () + + field_name = normalize_active_memory_custom_field_name( + raw_name + ) + field_value = normalize_active_memory_custom_field_value( + raw_value + ) + + if ( + not field_name + or not field_value + or field_name in seen + ): + return "", () + + changes.append( + ( + field_name, + field_value, + ) + ) + seen.add(field_name) + + return active_memory_id, tuple(changes) + + +def parse_update_active_memory_payload_fields( + payload: str, +) -> tuple[str, tuple[tuple[str, str], ...]]: + """Read the id and submitted values from flat or legacy nested JSON.""" + + text = str(payload or "").strip() + if not text.startswith("{"): + return parse_update_active_memory_payload(text) + + try: + data = json.loads(text) + except (TypeError, ValueError): + return "", () + + if not isinstance(data, dict): + return "", () + + active_memory_id = str( + data.get("active_memory_id") + or data.get("id") + or "" + ).strip().casefold() + if not ACTIVE_MEMORY_UPDATE_ID_RE.fullmatch(active_memory_id): + return "", () + + raw_fields = ( + data.get("fields") + if "fields" in data + else data.get("updates") + ) + if raw_fields is None: + raw_fields = { + key: value + for key, value in data.items() + if str(key or "").strip().casefold() + not in {"active_memory_id", "id"} + } + if not isinstance(raw_fields, dict) or not raw_fields: + return "", () + + fields = [] + seen = set() + for raw_name, raw_value in raw_fields.items(): + if isinstance(raw_value, (dict, list)): + return "", () + + field_name = str(raw_name or "").strip().casefold() + field_value = normalize_active_memory_custom_field_value( + raw_value + ) + if ( + not re.fullmatch(r"[a-z][a-z0-9_]{0,31}", field_name) + or not field_value + or field_name in seen + ): + return "", () + + fields.append((field_name, field_value)) + seen.add(field_name) + + return active_memory_id, tuple(fields) + + def parse_update_active_memory_payload( payload: str, ) -> tuple[str, tuple[tuple[str, str], ...]]: + text = str( + payload or "" + ).strip() + + if text.startswith("{"): + return _parse_update_active_memory_json_payload( + text + ) + lines = [ str(line or "").strip() - for line in str(payload or "").splitlines() + for line in text.splitlines() if str(line or "").strip() ] diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 94f0c844..21da32ed 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -73,7 +73,7 @@ def get_brain_runtime_config(): RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_RESOLVE_TODO, - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + RUNTIME_ACTION_SAVE_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_WEB_SEARCH, build_runtime_action_display_text, @@ -112,7 +112,7 @@ def get_brain_runtime_config(): is_active_memory_record_paused, normalize_active_memory_custom_field_name, normalize_active_memory_custom_field_value, - parse_delayed_memory_content_payload, + parse_delayed_memory_payload, parse_update_active_memory_payload, parse_idle_seconds, normalize_jin_color_payload, @@ -123,6 +123,9 @@ def get_brain_runtime_config(): strip_active_memory_managed_suffixes, set_active_memory_suffix_value, ) +from utils.actions.update_active_memory_utils import ( + parse_update_active_memory_payload_fields, +) from utils.session_actions_history import ( build_active_memory_resolve_failed_history_text, build_asset_action_history_text, @@ -305,7 +308,7 @@ def build_delayed_memory_report( ) ) except json.JSONDecodeError: - report = parse_delayed_memory_content_payload( + report = parse_delayed_memory_payload( payload ) @@ -821,10 +824,15 @@ def build_active_memory_runtime_line( raw_visible_value = suffix_values[0][1] - # Custom state schema is created only from trailing parenthesized - # declarations. A model-authored square suffix would otherwise be - # indistinguishable from a runtime-owned field after persistence. - if collect_active_memory_custom_fields(raw_visible_value): + json_payload = raw_visible_value.lstrip().startswith("{") + + # Legacy plain-text saves may create custom state only from trailing + # parenthesized declarations. A model-authored square suffix would + # otherwise be indistinguishable from runtime-owned state. + if ( + not json_payload + and collect_active_memory_custom_fields(raw_visible_value) + ): return "" visible_value, custom_fields = ( @@ -841,7 +849,7 @@ def build_active_memory_runtime_line( # three-field limit), keep the save conservative instead of silently # creating a schema the model did not actually request. original_visible_value = suffix_values[0][1].strip() - custom_declaration_present = bool( + custom_declaration_present = not json_payload and bool( re.search( r"\(\s*[A-Za-z][A-Za-z0-9_]{0,31}\s*:\s*[^()]+\)\s*$", original_visible_value, @@ -1013,6 +1021,313 @@ def collect_context_active_memory_slot_ids( r"^\s*active_memory(?:_\d+)?\s*:", re.IGNORECASE, ) +UPDATE_ACTIVE_MEMORY_SLOT_KEY_RE = re.compile( + r"^active_memory_[1-9]\d*$", + re.IGNORECASE, +) + +UPDATE_ACTIVE_MEMORY_SLOT_KEY_TOKEN_RE = re.compile( + r"(?]*?))?\s*>\s*$", + re.IGNORECASE, +) + +UPDATE_ACTIVE_MEMORY_CLOSE_TAG_RE = re.compile( + r"^\s*\s*$", + re.IGNORECASE, +) + + +def _collect_context_active_memory_sources( + context, +) -> list[str]: + + active_records = getattr( + context, + "active_memory_records", + None, + ) + return [ + *(active_records or ()), + getattr( + context, + "runtime_memory", + "", + ), + getattr( + context, + "runtime_memory_stable", + "", + ), + ] + + +def _extract_update_active_memory_slot_key( + payload: str, +) -> str: + + for line in str( + payload or "" + ).splitlines(): + text = str( + line or "" + ).strip() + + if not text: + continue + + match = UPDATE_ACTIVE_MEMORY_SLOT_KEY_TOKEN_RE.search( + text + ) + if match is None: + return "" + + return match.group( + 1 + ).casefold() + + return "" + + +def _unwrap_update_active_memory_marker_payload( + payload: str, +) -> str: + + attribute_payload = "" + payload_lines = [] + did_read_first_line = False + + for line in str( + payload or "" + ).splitlines(): + text = str( + line or "" + ).strip() + + if not text: + continue + + if not did_read_first_line: + did_read_first_line = True + match = UPDATE_ACTIVE_MEMORY_OPEN_TAG_RE.fullmatch( + text + ) + + if match is not None: + attribute_payload = str( + match.group(1) + or "" + ).strip() + continue + + if UPDATE_ACTIVE_MEMORY_CLOSE_TAG_RE.fullmatch( + text + ): + continue + + payload_lines.append( + text + ) + + return "\n".join( + part + for part in ( + attribute_payload, + *payload_lines, + ) + if part + ) + + +def _extract_update_active_memory_json_slot_key( + payload: str, +) -> str: + + text = str( + payload or "" + ).strip() + + if not text.startswith("{"): + return "" + + try: + data = json.loads(text) + except (TypeError, ValueError): + return "" + + if not isinstance( + data, + dict, + ): + return "" + + candidate = str( + data.get("active_memory_id") + or data.get("id") + or "" + ).strip().casefold() + + if UPDATE_ACTIVE_MEMORY_SLOT_KEY_RE.fullmatch( + candidate + ): + return candidate + + return "" + + +def _replace_update_active_memory_json_slot_key( + payload: str, + active_memory_id: str, +) -> str: + + text = str( + payload or "" + ).strip() + + if not text.startswith("{"): + return "" + + try: + data = json.loads(text) + except (TypeError, ValueError): + return "" + + if not isinstance( + data, + dict, + ): + return "" + + for key in ( + "active_memory_id", + "id", + ): + candidate = str( + data.get(key) + or "" + ).strip().casefold() + + if UPDATE_ACTIVE_MEMORY_SLOT_KEY_RE.fullmatch( + candidate + ): + data[key] = active_memory_id + return json.dumps( + data, + ensure_ascii=False, + ) + + return "" + + +def _find_active_memory_slot_record_by_key( + context, + active_memory_key: str, +) -> str: + + normalized_key = str( + active_memory_key or "" + ).strip().casefold() + + if not UPDATE_ACTIVE_MEMORY_SLOT_KEY_RE.fullmatch( + normalized_key + ): + return "" + + for source in _collect_context_active_memory_sources( + context + ): + for line in str( + source or "" + ).splitlines(): + key, separator, _ = str( + line or "" + ).partition(":") + + if ( + separator + and key.strip().casefold() == normalized_key + and ACTIVE_MEMORY_RUNTIME_LINE_RE.match(line) + and not is_active_memory_record_paused(line) + ): + return line.strip() + + return "" + + +def normalize_update_active_memory_payload_reference( + context, + payload: str, +) -> tuple[str, str, str]: + + original_payload = _unwrap_update_active_memory_marker_payload( + payload + ) + slot_key = _extract_update_active_memory_slot_key( + original_payload + ) or _extract_update_active_memory_json_slot_key( + original_payload + ) + + if not slot_key: + return original_payload, "", "" + + slot_record = _find_active_memory_slot_record_by_key( + context, + slot_key, + ) + active_memory_ids = sorted( + collect_active_memory_slot_ids( + slot_record + ) + ) + active_memory_id = ( + active_memory_ids[0] + if active_memory_ids + else "" + ) + + if not active_memory_id: + return original_payload, slot_key, "" + + json_payload = _replace_update_active_memory_json_slot_key( + original_payload, + active_memory_id, + ) + if json_payload: + return json_payload, slot_key, active_memory_id + + lines = [] + did_write_id = False + + for line in original_payload.splitlines(): + text = str( + line or "" + ).strip() + + if not text: + continue + + if UPDATE_ACTIVE_MEMORY_CLOSE_TAG_RE.fullmatch( + text + ): + continue + + if not did_write_id: + lines.append( + active_memory_id + ) + did_write_id = True + continue + + lines.append( + text + ) + + return "\n".join(lines), slot_key, active_memory_id def remove_active_memory_slot_from_text( @@ -1085,26 +1400,9 @@ def find_active_memory_slot_record( if not normalized_id: return "" - active_records = getattr( - context, - "active_memory_records", - None, - ) - sources = [ - *(active_records or ()), - getattr( - context, - "runtime_memory", - "", - ), - getattr( - context, - "runtime_memory_stable", - "", - ), - ] - - for source in sources: + for source in _collect_context_active_memory_sources( + context + ): for line in str( source or "" ).splitlines(): @@ -1348,10 +1646,38 @@ async def update_active_memory_runtime_record( if context is None: return result + normalized_payload, requested_reference, resolved_reference_id = ( + normalize_update_active_memory_payload_reference( + context, + payload, + ) + ) + + if requested_reference: + result["requested_id"] = requested_reference + + if requested_reference and not resolved_reference_id: + result["id"] = requested_reference + result["error"] = "active_memory_not_found" + return result + active_memory_id, changes = parse_update_active_memory_payload( - payload + normalized_payload + ) + ( + requested_active_memory_id, + requested_changes, + ) = parse_update_active_memory_payload_fields( + normalized_payload ) - result["id"] = active_memory_id + result["id"] = active_memory_id or requested_active_memory_id + result["requested_changes"] = [ + { + "field": field_name, + "after": field_value, + } + for field_name, field_value in requested_changes + ] if not active_memory_id or not changes: return result @@ -1364,6 +1690,10 @@ async def update_active_memory_runtime_record( result["error"] = "active_memory_not_found" return result + result["previous_title"] = get_active_memory_record_title( + current_record + ) + current_fields = dict( collect_active_memory_custom_fields( current_record @@ -1467,7 +1797,7 @@ async def update_active_memory_runtime_record( "ok": True, "error": "", "id": active_memory_id, - "title": get_active_memory_record_title(updated_record), + "title": result["previous_title"], "record": updated_record, "changes": list(change_results), "updated_at": updated_at, @@ -2635,7 +2965,7 @@ def build_delayed_memory_history_text( if not title: return "" - if action == "save_delayed_memory_content": + if action == "save_delayed_memory": return f"Delayed memory saved: {title}" if action == "load_delayed_memory": diff --git a/utils/context/delayed_memory.py b/utils/context/delayed_memory.py index 25d7d781..e89c8516 100644 --- a/utils/context/delayed_memory.py +++ b/utils/context/delayed_memory.py @@ -117,10 +117,10 @@ def format_delayed_memory_result_sections( ) continue - if action == "save_delayed_memory_content": + if action == "save_delayed_memory": sections.append( ( - "SAVE_DELAYED_MEMORY_CONTENT", + "SAVE_DELAYED_MEMORY", format_delayed_memory_report_result( result ), diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index cc391dd9..2663b8ea 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -212,7 +212,7 @@ def _format_memory_action_context_part( if normalized_action in { "SAVE_ACTIVE_MEMORY", - "SAVE_DELAYED_MEMORY_CONTENT", + "SAVE_DELAYED_MEMORY", }: if detail: return f"{action} - {detail}" diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index fc0c32d3..a125d06a 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -14,6 +14,9 @@ from utils.actions.update_l4_facts_utils import ( parse_update_l4_facts_payload, ) +from utils.actions.update_active_memory_utils import ( + parse_update_active_memory_payload_fields, +) MAX_SESSION_ACTION_HISTORY_ITEMS = 200 @@ -314,7 +317,7 @@ def get_current_action_sequence_started_at( "run_document_reader": "Read document iteratively", "run_python_skill": "Ran Python skill", "sample_wildcard": "Sampled wildcard", - "save_delayed_memory_content": "Saved delayed memory", + "save_delayed_memory": "Saved delayed memory", } @@ -1174,7 +1177,7 @@ def build_delayed_memory_save_rejected_history_text( or "" ).strip() - text = "SAVE_DELAYED_MEMORY_CONTENT - failed" + text = "SAVE_DELAYED_MEMORY - failed" if normalized_title: text = f"{text}: {normalized_title}" @@ -1317,7 +1320,7 @@ def _build_session_action_marker_detail( PAYLOAD_DISTINCT_SESSION_ACTIONS = { "SAVE_ACTIVE_MEMORY", "RESOLVE_ACTIVE_MEMORY", - "SAVE_DELAYED_MEMORY_CONTENT", + "SAVE_DELAYED_MEMORY", "LOAD_DELAYED_MEMORY", "UNLOAD_DELAYED_MEMORY", } @@ -1881,6 +1884,31 @@ def _build_formatted_session_action_marker_parts( ) continue + if action_name == "UPDATE_ACTIVE_MEMORY": + active_memory_ids = _unique_session_action_values( + parse_update_active_memory_payload_fields( + payload + )[0] + for payload in payloads + ) + active_memory_ids = [ + active_memory_id + for active_memory_id in active_memory_ids + if active_memory_id + ] + if active_memory_ids: + part["text"] = ( + f"{action_name}:" + + ",".join(active_memory_ids) + ) + formatted_parts.append( + _with_session_action_marker_count( + part, + count, + ) + ) + continue + if colors: part["colors"] = colors part["context_detail"] = ", ".join( diff --git a/utils/session_restore.py b/utils/session_restore.py index a1afba75..e187bc28 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -35,7 +35,7 @@ ACTION_LABELS = { "SAVE_SESSION": "Saved session", - "SAVE_DELAYED_MEMORY_CONTENT": "Saved delayed memory", + "SAVE_DELAYED_MEMORY": "Saved delayed memory", "LOAD_DELAYED_MEMORY": "Loaded delayed memory", "UNLOAD_DELAYED_MEMORY": "Unloaded delayed memory", "SAVE_ACTIVE_MEMORY": "Saved active memory", @@ -544,7 +544,7 @@ def _tool_result_kind(name: str) -> str: }: return "active_memory" if action_name in { - "SAVE_DELAYED_MEMORY_CONTENT", + "SAVE_DELAYED_MEMORY", "LOAD_DELAYED_MEMORY", "UNLOAD_DELAYED_MEMORY", }: diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 31b2c437..8a41d396 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -1829,8 +1829,7 @@ def is_default_runtime_memory_text( ).lower() return normalized == ( - "this session has just begun. " - "you have no history with the user yet." + "this session has just begun." ).lower() From 05bc5831c71b5e6e058dd004d662ff1a13e50242 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:30:16 +0300 Subject: [PATCH 091/168] fix: tint bootstrap - fix for smooth color apply during session bootstrap --- ui/static/css/base.css | 8 -- ui/static/js/logger/logger.js | 13 ++- ui/static/js/runtime/runtime-session.js | 124 +++++++++++++----------- ui/templates/index.html | 6 +- 4 files changed, 83 insertions(+), 68 deletions(-) diff --git a/ui/static/css/base.css b/ui/static/css/base.css index 86feab84..d8116d04 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -203,14 +203,6 @@ main.scene-searching #scene-search-overlay { transition: opacity var(--panel-collapse-duration) var(--panel-collapse-easing); } -main.panels-collapsed-1 #scene-shade { - --scene-shade-alpha: 0.12; -} - -main.panels-collapsed-2 #scene-shade { - --scene-shade-alpha: 0.20; -} - #console-panel, #memory-panel { --app-header-panel-shift: 0px; diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 96cf50df..e9f79fd2 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -3880,6 +3880,9 @@ function applyRoomState(roomState, options = {}) { const animateRestore = options.animateRestore !== false && !prefersReducedMotion(); + const animateTint = + animateRestore + && options.animateTint !== false; roomStateRestoreSequence += 1; const sequence = roomStateRestoreSequence; @@ -3897,7 +3900,7 @@ function applyRoomState(roomState, options = {}) { .forEach(registerPanelRuntimeActivity); finishStartupCollapseAnimation(); - if (animateRestore) { + if (animateTint) { beginRoomStateTintTransition(sequence); } else { clearRoomStateTintTransition(); @@ -4235,7 +4238,10 @@ function initRoomStatePersistence() { const applyStoredRoomState = () => { return applyRoomState( storedRoomState, - { persist: false } + { + persist: false, + animateTint: false, + } ); }; const avatarState = @@ -4255,7 +4261,8 @@ function initRoomStatePersistence() { && String(avatarState.color || "").trim() ) { sceneTintShift( - applyStoredRoomState + applyStoredRoomState, + avatarState.color ); } else { applyStoredRoomState(); diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index e4cfa386..1a7cc420 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -82,6 +82,7 @@ let hasUnsavedSessionActivity = false; const BOOTSTRAP_SCENE_TINT_SHIFT_MS = 2000; let bootstrapSceneTintShiftSequence = 0; + let activeBootstrapSceneTintColor = ""; function getCurrentSavedSessionId() { return String( @@ -99,11 +100,24 @@ ); } - function applyBootstrapSceneTintShift(apply) { + function applyBootstrapSceneTintShift( + apply, + targetColor = "" + ) { if (typeof apply !== "function") { return false; } + const normalizedTargetColor = + String(targetColor || "").trim().toLowerCase(); + + if ( + normalizedTargetColor + && normalizedTargetColor === activeBootstrapSceneTintColor + ) { + return Boolean(apply()); + } + bootstrapSceneTintShiftSequence += 1; const sequence = bootstrapSceneTintShiftSequence; @@ -120,12 +134,11 @@ return Boolean(apply()); } + activeBootstrapSceneTintColor = + normalizedTargetColor; + const root = document.documentElement; - const previousRootDuration = - root.style.getPropertyValue( - "--scene-jin-tint-transition-duration" - ); const previousMain = sceneMain ? { transition: sceneMain.style.transition, @@ -175,65 +188,62 @@ const targetTintOpacity = rootStyle.getPropertyValue("--scene-jin-tint-alpha").trim(); - root.style.setProperty( - "--scene-jin-tint-transition-duration", - `${BOOTSTRAP_SCENE_TINT_SHIFT_MS}ms` - ); + const transition = + `background-color ${BOOTSTRAP_SCENE_TINT_SHIFT_MS}ms ease`; + + // Do not rely on a custom-property duration changing in the same style + // recalculation as the restored color. On a fresh tab that can collapse + // into one paint and make the inherited room color appear instantly. + // Keep the actually painted colors inline, then explicitly release them + // towards the restored values on the next frame. + if (sceneMain) { + sceneMain.style.transition = transition; + } + + if (sceneTint) { + sceneTint.style.transition = + `${transition}, opacity ${BOOTSTRAP_SCENE_TINT_SHIFT_MS}ms ease`; + } window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - if (sceneMain && targetMainColor) { + if (sceneMain && targetMainColor) { + sceneMain.style.backgroundColor = + targetMainColor; + } + + if (sceneTint && targetTintColor) { + sceneTint.style.backgroundColor = + targetTintColor; + } + + if (sceneTint && targetTintOpacity) { + sceneTint.style.opacity = + targetTintOpacity; + } + + window.setTimeout(() => { + if (sequence !== bootstrapSceneTintShiftSequence) { + return; + } + + activeBootstrapSceneTintColor = ""; + + if (sceneMain) { sceneMain.style.transition = previousMain.transition; sceneMain.style.backgroundColor = - targetMainColor; + previousMain.backgroundColor; } - if (sceneTint && targetTintColor) { + if (sceneTint) { sceneTint.style.transition = previousTint.transition; sceneTint.style.backgroundColor = - targetTintColor; - } - - if (sceneTint && targetTintOpacity) { + previousTint.backgroundColor; sceneTint.style.opacity = - targetTintOpacity; + previousTint.opacity; } - - window.setTimeout(() => { - if (sequence !== bootstrapSceneTintShiftSequence) { - return; - } - - if (previousRootDuration) { - root.style.setProperty( - "--scene-jin-tint-transition-duration", - previousRootDuration - ); - } else { - root.style.removeProperty( - "--scene-jin-tint-transition-duration" - ); - } - - if (sceneMain) { - sceneMain.style.transition = - previousMain.transition; - sceneMain.style.backgroundColor = - previousMain.backgroundColor; - } - - if (sceneTint) { - sceneTint.style.transition = - previousTint.transition; - sceneTint.style.backgroundColor = - previousTint.backgroundColor; - sceneTint.style.opacity = - previousTint.opacity; - } - }, BOOTSTRAP_SCENE_TINT_SHIFT_MS + 80); - }); + }, BOOTSTRAP_SCENE_TINT_SHIFT_MS + 80); }); return applied; @@ -1283,7 +1293,10 @@ const applyRoomState = () => { return window.JinPanels.applyRoomState( bootstrap.room_state, - { persist: false } + { + persist: false, + animateTint: false, + } ); }; const avatarState = @@ -1294,7 +1307,10 @@ : {}; if (String(avatarState.color || "").trim()) { - applyBootstrapSceneTintShift(applyRoomState); + applyBootstrapSceneTintShift( + applyRoomState, + avatarState.color + ); } else { applyRoomState(); } diff --git a/ui/templates/index.html b/ui/templates/index.html index c63978a4..98f8254f 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,7 +10,7 @@ - + @@ -510,7 +510,7 @@ - + @@ -519,7 +519,7 @@ - + From 4614ef7ab4fe17d026cb0e05ab44d37e8607156a Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:51:51 +0300 Subject: [PATCH 092/168] fix: launcher - fix for launcher python detection --- launch_jin.ps1 | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/launch_jin.ps1 b/launch_jin.ps1 index c93c0450..c4b0f19e 100644 --- a/launch_jin.ps1 +++ b/launch_jin.ps1 @@ -387,18 +387,35 @@ function Write-JinConfig { Update-ModelConfig -ConfigPath $configPath -Name "SERVICE_MODEL_UID" -SuggestedModel $suggestedModel } -function Get-PythonCommand { - $python = Get-Command python -ErrorAction SilentlyContinue - if ($python) { - return @($python.Source) +function Test-PythonCommand { + param( + [string]$Executable, + [string[]]$Arguments = @() + ) + + try { + $versionOutput = & $Executable @Arguments --version 2>&1 + $versionText = (@($versionOutput) -join " ").Trim() + + return ($LASTEXITCODE -eq 0 -and $versionText -match '^Python 3(?:\.|\s|$)') + } + catch { + return $false } +} +function Get-PythonCommand { $py = Get-Command py -ErrorAction SilentlyContinue - if ($py) { + if ($py -and (Test-PythonCommand -Executable $py.Source -Arguments @("-3"))) { return @($py.Source, "-3") } - Fail-WithMessage "Python was not found. Install Python 3, then run this script again." + $python = Get-Command python -ErrorAction SilentlyContinue + if ($python -and (Test-PythonCommand -Executable $python.Source)) { + return @($python.Source) + } + + Fail-WithMessage "A working Python 3 installation was not found. Install Python 3 from python.org, then run this script again." } try { @@ -456,7 +473,7 @@ try { if (-not (Test-Path $venvPython)) { Write-Step "Creating .venv..." - $pythonCommand = Get-PythonCommand + $pythonCommand = @(Get-PythonCommand) $pythonExe = $pythonCommand[0] $pythonArgs = @() @@ -465,6 +482,10 @@ try { } & $pythonExe @pythonArgs -m venv $venvPath + + if ($LASTEXITCODE -ne 0) { + Fail-WithMessage "Python failed to create the virtual environment (exit code $LASTEXITCODE)." + } } else { Write-Step ".venv already exists." From 8856e9fc9239eeb485a8a16f1c2318c784ebe2f1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:04:11 +0300 Subject: [PATCH 093/168] chore: ui - add bootstrap instruction for user language - add AGENTS.md and architecture docs - update session actions log card - fix loop validator log timing and better ascii support - fix L-T facts lazy loading in panel - ui fixes --- AGENTS.md | 120 ++++ app_settings.py | 58 ++ clients/search_client.py | 2 +- contracts/jin_size.json | 9 +- docs/JIN_ARCHITECTURE.md | 554 ++++++++++++++++++ docs/JIN_CURRENT_STATE.md | 496 ++++++++++++++++ docs/JIN_DECISIONS.md | 412 +++++++++++++ rules/brain_context_builder.py | 51 +- rules/identity.py | 2 +- rules/runtime.py | 1 + runtime/metabolism.py | 2 +- runtime/stream.py | 50 +- tests/runtime_actions/test_active_memory.py | 121 ++++ tests/runtime_actions/test_stream_filter.py | 334 +++++++++++ tests/test_archived_session_restore.py | 74 +++ ...l_results_bootstrap_checkpoint_contract.py | 53 ++ tests/test_header_autohide_client_contract.py | 32 + tests/test_l4_context_focus_contract.py | 15 + ...time_memory_hover_title_client_contract.py | 71 +++ ...untime_memory_lazy_rows_client_contract.py | 74 +++ ...time_memory_row_payload_client_contract.py | 102 ++++ tests/test_search_flow.py | 100 ++++ tests/test_stream_validator.py | 21 + tests/test_ui_fix_bundle_contract.py | 68 +++ ui/static/js/answer-rating.js | 84 ++- ui/static/js/chat-runtime-actions.js | 21 +- ui/static/js/chat.js | 123 +++- ui/static/js/header-autohide.js | 22 + ui/static/js/logger/session-actions.js | 40 +- ui/static/js/runtime/runtime-memory-model.js | 4 +- ui/static/js/runtime/runtime-memory-view.js | 526 ++++++++++++----- ui/static/js/runtime/runtime-session.js | 47 ++ ui/static/js/socket.js | 37 ++ ui/static/js/socket/input.js | 62 ++ ui/static/js/socket/runtime-actions.js | 10 + ui/static/js/win95-theme.js | 24 +- ui/templates/index.html | 18 +- utils/actions/active_memory_utils.py | 41 +- utils/actions/common_action_utils.py | 193 +++++- utils/actions/update_active_memory_utils.py | 213 +++++++ utils/brain_client_utils.py | 21 - utils/stream_validator.py | 7 + websocket/bootstrap.py | 58 +- 43 files changed, 4068 insertions(+), 305 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/JIN_ARCHITECTURE.md create mode 100644 docs/JIN_CURRENT_STATE.md create mode 100644 docs/JIN_DECISIONS.md create mode 100644 tests/test_clean_tool_results_bootstrap_checkpoint_contract.py create mode 100644 tests/test_runtime_memory_hover_title_client_contract.py create mode 100644 tests/test_runtime_memory_lazy_rows_client_contract.py create mode 100644 tests/test_runtime_memory_row_payload_client_contract.py create mode 100644 tests/test_ui_fix_bundle_contract.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..38887188 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,120 @@ +# AGENTS.md — JIN Core Engine + +This file is the mandatory entry point for any coding agent working in this repository. + +## Read order before changing code + +1. Read `AGENTS.md`. +2. Read the relevant sections of `docs/JIN_ARCHITECTURE.md`. +3. Read `docs/JIN_DECISIONS.md` for product-intent constraints and rejected directions. +4. Read `docs/JIN_CURRENT_STATE.md` for transitional formats, stale legacy, known conflicts, and test status. +5. Inspect the current implementation end-to-end before editing it. + +Do not treat `README.md`, the old root `ARCHITECTURE.md`, old tests, comments, filenames, or search indexes as stronger evidence than the current source tree plus these documents. If sources conflict, report the conflict instead of silently choosing one. + +## Source precedence + +Use this order when deciding what is true: + +1. Current source code for **what is implemented now**. +2. `docs/JIN_DECISIONS.md` for **what the product is intended to mean**. +3. `docs/JIN_CURRENT_STATE.md` for **known migrations/conflicts/temporary compatibility**. +4. Historical docs/tests/comments only as legacy evidence. + +A current implementation can still violate a product decision. Do not hide that. State both sides and ask before changing product semantics. + +## Architectural invariants + +- JIN Core Engine is a model-agnostic cognitive runtime, not a chatbot skin and not a framework tied to one LLM. +- The normal model path is direct: user turn -> `AgentRuntime` -> `BrainNode`. Do not add a pre-Brain routing framework without an explicit task. +- `RuntimeContext` is the in-process live state hub for a runtime session. Do not create parallel sources of truth for state it already owns. +- L2 and L3 are **removed architectural layers**. Do not restore them from old README/tests/indexes. Any surviving L2/L3 names must be classified as compatibility, stale tests/docs, UI residue, or dead legacy before touching them. +- Durable facts belong in L4. Do not reintroduce a durable-L1/L2/L3 memory hierarchy. +- Active Memory, Delayed Memory, L4 facts, persistent Files, live L1/runtime memory, and session checkpoints are different systems with different lifetimes. Do not collapse them into one generic memory store. +- Session continuity must distinguish completed turns from interrupted turns and must preserve original timestamps where available. +- Browser checkpoint `saved_at` is a causal freshness boundary for archive enrichment, not a generic "last touched" timestamp. A field-local mutation must not advance it unless the whole checkpoint is genuinely refreshed. +- In predecessor bootstrap, an explicitly present `tool_results: []` is an authoritative cleared value. Do **not** generalize that tombstone rule to `loaded_memory_ids` or `active_memory_records`; their empty browser values still retain archive-fallback semantics. +- Session-action history is structured continuity data. Preserve supported part metadata across persistence/bootstrap (for example JIN_COLOR `parts[].colors`), not only the visible action text. +- Archived restore intentionally stages historical resources and replays them through the real action path after the one-shot restore response. Do not add a second late apply path. +- Anonymous/shadow mode may read global durable context but must not silently perform restricted persistent writes. + +## Runtime actions + +- `contracts/*.json` are the canonical model-facing contracts for concrete action schemas and action-specific rules. +- Keep `rules/runtime.py` for cross-action sequencing/loop invariants, not duplicated field-by-field action instructions. +- Every new action needs: contract -> parser/normalization -> guard if needed -> dispatcher/handler -> emitted state/result -> tests. +- Streaming can split markers at arbitrary chunk boundaries. Always test complete, split, repeated, incomplete, false-prefix, and flush/stop cases. +- Do not let private action markers leak into visible answer text. +- Do not emit the same action twice because two parser paths recognized the same marker. +- Current Delayed save contract is `` with a JSON body. The old `` key/value format is legacy only. +- Current Active create/update model boundary is flat JSON. `SAVE_ACTIVE_MEMORY` custom fields must come from explicit structured JSON; plain prose such as a trailing `(field: value)` is still prose/conditions, not schema. Legacy nested update payloads and self-closing UPDATE attributes may be accepted locally for compatibility; do not advertise them as preferred syntax. +- Search actions are effective only when the configured provider is actually available (`settings.CAN_SEARCH`). Feature flags alone must not expose `WEB_SEARCH`/`DEEP_WEB_SEARCH`; do not invent client-side API-key shape regexes when the provider has no stable key-shape contract. +- `CLEAN_TOOL_RESULTS` is a no-payload marker. Bare `` and the redundant paired form must execute once; any closing tag is parser noise and must be consumed even across stream chunks. +- `SAVE_SESSION` is not a current runtime-action contract in this snapshot. Treat old references as legacy/session-restore compatibility until proven otherwise. + +## UI / visual language + +Before introducing any visual state, find and reuse the closest existing JIN UI primitive. + +- Do not invent new highlight colors, glows, banners, badges, icons, gradients, or interaction patterns without explicit owner approval. +- Reuse existing DOM structure, CSS variables, typography, radius, spacing, animation, and semantic states whenever possible. +- Color already carries runtime meaning. A new decorative color is not harmless. +- Loaded context, mere ID reference, pin state, pause state, inspect/modal open, and delete/restore are different semantics. Do not map all of them to one generic `highlight()` call. +- For UI fixes, verify the actual rendered DOM/event path, not only data/state mutation. +- For bootstrap/restore visual fixes, locate every writer and prove there is one authoritative final apply path. +- Session-action interruption telemetry is causal UI state: validator/reasoning loops and context/output-limit recovery entries must be recorded/emitted when detected, before automatic follow-up/recovery starts. +- L4 rows use a short default preview (currently 50 characters), but a fact that is bubbled by reference/citation/context-loaded state must show its full value rather than a truncated citation. +- Hover/detail metadata must survive non-semantic refreshes: counter-only runtime-action updates and bootstrap normalization must not silently erase tooltip/swatch data. +- Do not add `backdrop-filter`/blur or new translucent effects casually. Existing shadow/tint depth is deliberate. + +## Change discipline + +- Respect the requested scope. If asked to analyze only, do not edit. +- Prefer the smallest coherent fix over architectural tourism. +- Do not duplicate an existing listener, timer, writer, parser, storage path, or style primitive just to make a symptom disappear. +- Do not mass-format files or change line endings for a narrow fix. +- Do not add dependencies without a concrete need. +- Preserve existing user changes. Do not overwrite unrelated dirty work. +- Compatibility belongs in a localized adapter, not scattered across runtime/UI/rules. +- Do not guess IDs, state owners, ordering, or root causes. + +## Required investigation before a patch + +Trace the affected flow through all relevant stages: + +`source -> normalization -> canonical owner -> persistence -> bootstrap/restore -> prompt/action path -> emitted event -> UI projection -> tests` + +For stateful bugs, find **all readers and all writers**. For visual bugs, find every writer of the relevant CSS variable/DOM state. For runtime actions, find every parser and dispatcher path that can emit the same action. + +## Verification + +At minimum, run the checks that apply to the change: + +- syntax/import check; +- targeted unit/client-contract tests; +- `git diff --check` when git metadata is available; +- targeted search for the removed/renamed format; +- duplicate writer/listener/timer search; +- serialize -> reload -> hydrate round trip for restore/state changes; +- explicit-empty-vs-missing tests for bootstrap fields, plus checkpoint timestamp/lineage invariance for field-only cleanup; +- structured session-action metadata round trip (including JIN_COLOR swatch/hex hover); +- chunk-boundary tests for stream/action parsing; +- legacy-record + modern-record tests for compatibility/timestamps; +- real DOM/render-path check for UI behavior; +- search capability tests with configured/blank/placeholder provider credentials when touching search contracts or prompt exposure; +- patch apply check against the exact source snapshot when delivering `.patch`. + +Do not claim the repository is green unless the relevant tests actually pass. See `docs/JIN_CURRENT_STATE.md` for the current full-suite status of the 2026-08-23 snapshot. + +## Handoff format + +When you finish a coding task, report: + +1. root cause; +2. files changed; +3. exact behavior changed; +4. checks/tests run and their result; +5. remaining assumptions/risks; +6. any conflict between current code and documented product intent. + +Keep this file short. Put architecture detail in `docs/JIN_ARCHITECTURE.md`, durable decisions in `docs/JIN_DECISIONS.md`, and migrations/known conflicts in `docs/JIN_CURRENT_STATE.md`. diff --git a/app_settings.py b/app_settings.py index 344f1f28..71051971 100644 --- a/app_settings.py +++ b/app_settings.py @@ -5,6 +5,51 @@ ) +SERPER_API_KEY_PLACEHOLDERS = { + "mock-serper-api-key", + "your-serper-api-key", + "your_serper_api_key", +} + + +def is_valid_serper_api_key( + api_key: str, +) -> bool: + + normalized_key = str( + api_key + or "" + ).strip() + + if not normalized_key: + return False + + # Serper does not expose a stable client-side key shape contract. + # Availability means "a real key is configured"; the provider validates + # the credential when a request is made. + return ( + normalized_key.casefold() + not in SERPER_API_KEY_PLACEHOLDERS + ) + + +def can_use_configured_search( + *, + provider: str, + serper_api_key: str, +) -> bool: + + return ( + str( + provider + or "" + ).strip().casefold() == "serper" + and is_valid_serper_api_key( + serper_api_key + ) + ) + + @dataclass(frozen=True) class AppSettings: @@ -31,6 +76,7 @@ class AppSettings: SEARCH_SERPER_API_KEY: str SEARCH_MAX_RESULTS: int SEARCH_TIMEOUT: float + CAN_SEARCH: bool settings = AppSettings( @@ -85,4 +131,16 @@ class AppSettings: "SEARCH_TIMEOUT", 20.0, ), + CAN_SEARCH=can_use_configured_search( + provider=getattr( + config, + "SEARCH_PROVIDER", + "serper", + ), + serper_api_key=getattr( + config, + "SEARCH_SERPER_API_KEY", + "", + ), + ), ) diff --git a/clients/search_client.py b/clients/search_client.py index 072f89ee..6f5b4764 100644 --- a/clients/search_client.py +++ b/clients/search_client.py @@ -307,7 +307,7 @@ async def run_search_provider( f"Unsupported search provider: {settings.SEARCH_PROVIDER}" ) - if not settings.SEARCH_SERPER_API_KEY: + if not settings.CAN_SEARCH: raise RuntimeError( "Serper search is not configured" ) diff --git a/contracts/jin_size.json b/contracts/jin_size.json index 6b8ef84f..6e5da0d1 100644 --- a/contracts/jin_size.json +++ b/contracts/jin_size.json @@ -10,13 +10,10 @@ "emit_followup": false }, "rules": [ - "Use to set JIN size (or weight)! Change live JIN avatar size in pixels.", - "Emit marker using exactly this schema 120px 120px ", - "Put only the size payload inside the tag body; do not put it in the opening tag.", - "The first number is width and the second number is height.", + "Use to set JIN Live Avatar size.", + "Emit marker using this schema w:120 h:120 ", "You may emit 120px to make the avatar square.", - "Payload values may include px or omit px.", - "Labeled payload values are also valid, for example w:120px h:120px , w:120 , or h:120 ." + "Payload values may include px, percent, or vw/vh. Default is - px." ], "close_tag": true, "body_placeholder": "120px 120px" diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md new file mode 100644 index 00000000..0969b3fc --- /dev/null +++ b/docs/JIN_ARCHITECTURE.md @@ -0,0 +1,554 @@ +# JIN Core Engine — Current Architecture + +**Verified snapshot:** `jin_core(20260823-114203).zip` +**Intent source used for reconciliation:** `JIN_CORE_CONTEXT_TRANSPLANT_2026-08-23.md` +**Purpose:** describe the architecture that is actually visible in the current source tree, while explicitly separating legacy compatibility from active design. + +This document supersedes the architectural parts of the old root `ARCHITECTURE.md` and `README.md` wherever they still describe L2/L3 as live architectural layers or `SAVE_SESSION` as a current model action. + +--- + +## 1. Product boundary + +JIN Core Engine is an experimental local cognitive runtime for interchangeable OpenAI-compatible models. The runtime, not a particular model, is the product. + +Core product properties: + +- visible/inspectable context and memory; +- visible reasoning when the provider exposes it; +- explicit runtime actions with private model markers; +- session continuity across reloads/tabs/archived sessions; +- persistent files and memory objects with stable IDs; +- runtime state projected into the UI and Live Avatar; +- separate BRAIN and SERVICE roles even when both roles resolve to the same physical model. + +The codebase is intentionally not structured as a generic autonomous-agent framework. The main foreground path remains direct. + +--- + +## 2. Top-level runtime flow + +```text +Browser UI + <-> FastAPI / WebSocket (/ws/chat) + -> RuntimeContext + -> PendingRequestQueue + -> process_message() + -> AgentRuntime + -> BrainNode + -> RuntimeStream + -> visible reasoning/content + -> private runtime actions + -> contracts/*.json + -> guard / normalization + -> utils/actions/dispatcher.py + -> state mutation / tool result + -> optional Brain follow-up in the same request sequence + +Background / secondary paths: + completed foreground turn + -> L1 SERVICE summarization + -> L1 diff + Facts Memory companion state + -> metabolism integration + + browser idle tick + -> L4 extraction/merge pipeline +``` + +Main ownership by package: + +| Area | Current responsibility | +| --- | --- | +| `app.py` | FastAPI app, HTTP APIs, static files, session-restore endpoint, WebSocket router registration | +| `websocket/` | connection lifecycle, queueing, bootstrap/resume, foreground turn orchestration, server->browser events | +| `runtime/runtime_context.py` | live in-process state hub for one logical runtime | +| `agent/` | direct Brain execution and per-turn state | +| `runtime/stream.py` | model stream handling, reasoning/content/action separation, recovery/limits | +| `contracts/` | canonical model-facing runtime-action contracts and action rule assembly | +| `utils/actions/` | payload normalization, action execution, storage/state mutation | +| `rules/brain_context_builder.py` | deterministic Brain prompt assembly from current state | +| `runtime/L1_memory*` | live runtime-memory summarization, snapshots, diffs, interrupted-turn memory path | +| `runtime/L4_memory*` | Facts Memory ingestion, durable L4 extraction/merge, reconciliation, delete/restore | +| `runtime/metabolism.py` | active homeostat/salience system and SERVICE integration | +| `utils/*_store.py` | durable file/report/fact stores | +| `ui/static/js/runtime/` | browser-side runtime state, persistence, session checkpointing, memory UI, avatar | +| `ui/static/js/logger/` | inspectable logger/action/memory projections | + +--- + +## 3. Connection, queue, and turn ordering + +### 3.1 RuntimeContext + +`runtime/runtime_context.py::RuntimeContext` is the central live state object. It holds, among other things: + +- clients and active streams; +- current/previous reasoning and visible answer state; +- runtime action history, action guard confirmations, tool results and follow-up state; +- live L1/runtime memory and snapshots; +- Active Memory records; +- Delayed Memory reports and loaded IDs; +- Facts Memory records and L4 store; +- attached file IDs and sequence attachments; +- session/reconnect/archived-restore metadata; +- L4/background tasks; +- metabolism state; +- avatar color/size/position/speed related state; +- message/turn/sequence counters. + +Do not create a parallel state container for a concept already owned here unless the lifetime is intentionally browser-only or filesystem-persistent. + +### 3.2 PendingRequestQueue + +`websocket/tasks.py::PendingRequestQueue` serializes work. It currently keeps two deques: + +- idle follow-ups; +- regular requests. + +`_get()` returns an idle follow-up before a regular request when one is queued. Foreground work still has explicit guards around background L4 processing. Any change to queue priority must be treated as a behavioral change, not a harmless refactor. + +### 3.3 Foreground turn + +`websocket/messages.py::process_message()` currently performs the foreground lifecycle: + +1. classify ordinary turn vs IDLE follow-up vs action-guard retry vs archived-session resume tick; +2. resolve current attachments; +3. establish turn ID and sequence ID; +4. reset per-turn transient state; +5. on ordinary turns: apply browser idle/Active/pattern/avatar state, prepare metabolism, and auto-load Delayed Memory by user-typed tags; +6. append the user turn to the local chat log; +7. build `AgentState` and execute `AgentRuntime`; +8. persist reasoning log and emit action/session telemetry; +9. append visible JIN output to chat log/recent-turn state; +10. settle immediate metabolism effects; +11. schedule normal or interrupted L1 memory integration. + +A later foreground turn can wait for a pending L1 update through `wait_for_runtime_memory_update()` so the Brain does not race stale live memory. + +--- + +## 4. Agent and model boundary + +`agent/runtime.py::AgentRuntime` is intentionally thin. It logs the flow and invokes `BrainNode` directly. + +```text +user request -> AgentRuntime -> BrainNode +``` + +There is no active planner/router node in front of Brain. + +Physical model endpoints are resolved through the client/config layer. Logical roles remain: + +- **BRAIN** — foreground reasoning, visible answer, runtime decisions; +- **SERVICE** — L1 integration, metabolism/L4/supporting model work. + +`USE_SERVICE_AS_BRAIN=True` may map both logical roles to one endpoint without removing the role distinction. + +--- + +## 5. Brain context assembly + +`rules/brain_context_builder.py::build_brain_context()` is the authoritative prompt assembly path. + +The current high-level order is: + +1. optional `` — absolute first block when non-empty; +2. on archived restore only: session-restore continuation instruction; +3. current concerns; +4. trusted runtime XML / enabled actions; +5. recent tool results; +6. session action history; +7. ordinary turn: attached-files inventory, then Delayed Memory inventory; restore turn: staged resource metadata instead; +8. skills inventory; +9. loaded skill content; +10. runtime-context group: + - explicit user feedback; + - silent metabolism state; + - Active Memory view; + - L1/runtime memory snapshot; + - visible session counters; + - runtime TODO state; + - loaded Delayed Memory bodies; + - L4 long-term memory; + - zero-diff stall alert; +11. archived exact-dialogue priming block, otherwise rolling recent visible messages; +12. archived reasoning dump, otherwise previous-reasoning loop/crop; +13. runtime-action instructions assembled from current contracts; +14. identity block; +15. turn/loop rules. + +Prompt text is a transient projection. Canonical state remains in `RuntimeContext`, browser persistence, and filesystem stores. + +--- + +## 6. Streaming and runtime actions + +### 6.1 RuntimeStream + +`runtime/stream.py::RuntimeStream` owns the Brain stream lifecycle and separates: + +- reasoning; +- visible answer content; +- private runtime-action markers. + +Recovery paths also live around the stream/Brain sequence: repetition protection, context/output-limit continuation, stop/cancel, and action follow-ups. + +### 6.2 Contract-driven action boundary + +Concrete action schemas live in `contracts/*.json`. `contracts/rules_assembler.py` loads them and maps runtime actions to feature flags. + +Current action names in the contract table: + +- `DEEP_WEB_SEARCH` +- `WEB_SEARCH` +- `CLEAN_TOOL_RESULTS` +- `IDLE` +- `JIN_COLOR` +- `JIN_SIZE` +- `JIN_POSITION` +- `JIN_SPEED` +- `UPDATE_L4_FACTS` +- `LOAD_SKILL` +- `UNLOAD_SKILL` +- `ASSET_ACTION` +- `LIST_FILES` +- `ATTACH_FILE` +- `DETACH_FILE` +- `CREATE_TODO_LIST` +- `RESOLVE_TODO` +- `CHECK_TODO` +- `SAVE_DELAYED_MEMORY` +- `LOAD_DELAYED_MEMORY` +- `UNLOAD_DELAYED_MEMORY` +- `SAVE_ACTIVE_MEMORY` +- `RESOLVE_ACTIVE_MEMORY` +- `UPDATE_ACTIVE_MEMORY` + +The default `rules/brain_context_builder.py` feature map currently disables runtime TODO (`CAN_RUNTIME_TODO=False`) while the other listed capabilities are enabled there. Search is an additional effective-capability gate: `WEB_SEARCH` and `DEEP_WEB_SEARCH` are removed from the model-facing action set unless `app_settings.settings.CAN_SEARCH` is true. `CAN_SEARCH` currently means provider `serper` plus a non-empty, non-placeholder configured key; the runtime deliberately does not guess a provider-specific key shape and leaves credential validation to Serper. The search client enforces the same gate before making a request. + +There is **no current `SAVE_SESSION` contract** in this snapshot. + +### 6.3 Parsing compatibility + +`utils/actions/regexp_utils.py` accepts more than the advertised canonical syntax so old/provider-specific marker forms can still be recognized. This compatibility is parser-level and must not be confused with the preferred model contract. + +For close-tag actions, the canonical form remains a paired block. For short actions, legacy inline/colon forms may be recognized when the parser explicitly supports them. + +Important current compatibility boundaries: + +- `CLEAN_TOOL_RESULTS` is intentionally a no-payload bare marker. A redundant `` is consumed as parser-only noise, including when it arrives in a later stream chunk; the paired-looking form still means one action. +- `UPDATE_ACTIVE_MEMORY` is advertised as paired flat JSON, while localized compatibility code can also read a self-closing attribute form. That attribute form is compatibility, not the canonical model contract. +- `SAVE_ACTIVE_MEMORY` does not infer custom fields from parenthesized plain prose. Only explicit JSON root fields are structural custom fields; non-JSON text remains the `conditions` value. + +### 6.4 Guard and dispatcher + +`runtime/action_guard.py` can pause selected state-changing actions for explicit confirmation when behavior-contract trigger requirements are not satisfied. + +`utils/actions/dispatcher.py::apply_runtime_action_calls()` is the central action execution fan-out. It manages action IDs, guard decisions, action-specific handlers, emitted events, trusted tool results, and follow-up semantics. + +Action-specific rules should stay in contracts. `rules/runtime.py` should remain limited to cross-action sequence behavior and recovery rules. + +### 6.5 Session-action telemetry is causal + +Session-action history is not only an end-of-turn summary. `runtime/stream.py` records and emits interruption/recovery entries at the moment the cause is detected so the logger changes before any automatic follow-up begins. This applies to reasoning/content validator loops and model context/output-limit recovery; `context_overflow` is treated as a context-limit finish reason. + +That ordering is part of observability semantics. Moving the emission back to final compaction makes the logger describe the past only after JIN has already continued. + +--- + +## 7. Memory architecture — current model + +The old four-layer L1/L2/L3/L4 architecture is not the current implementation. + +### 7.1 L1 / live runtime memory + +Active backend modules: + +- `runtime/L1_memory.py` +- `runtime/L1_memory_rules.py` +- `runtime/L1_memory_utils.py` + +L1 is the compact live runtime state used for the next turns. It is updated by SERVICE after foreground turns, has snapshots/diffs, and has a distinct interrupted-turn update path. + +It is not a durable long-term memory tier. + +### 7.2 L2 and L3 + +There are no `runtime/L2_memory.py`, `runtime/L3_memory.py`, `runtime/L2_memory_utils.py`, or `runtime/L3_memory_utils.py` modules in the verified snapshot. + +Remaining L2/L3 names occur in stale tests, old docs, comments, UI compatibility text, and historical session fields. Those names are not evidence that the layers still exist architecturally. + +### 7.3 Facts Memory + +Facts Memory is a companion candidate index derived from persisted live runtime snapshots on the browser side. `ui/static/js/runtime/runtime-storage.js` stores per-session facts-memory buckets and synchronizes them to the backend through `facts_memory_store_sync`. + +Backend L4 code normalizes these records and marks analyzed fields. This is an intake/candidate mechanism for L4, not an independent durable product layer equivalent to old L2/L3. + +### 7.4 L4 long-term memory + +Active modules: + +- `runtime/L4_memory.py` +- `runtime/L4_memory_rules.py` +- `runtime/L4_memory_utils.py` +- `utils/long_term_facts_file_store.py` + +L4 owns durable facts. The current L4 path includes: + +- Facts Memory candidate ingestion; +- extraction phase; +- merge/rebase/validation phase; +- explicit-edit protection; +- links to Delayed Memory reports; +- archive/anchor classification; +- delete/restore reconciliation; +- server/browser store synchronization. + +L4 work is scheduled from an explicit browser idle tick (`l4_memory_idle_tick`) and is guarded so it does not begin while foreground work is running or queued. + +### 7.5 Delayed Memory + +Delayed Memory stores larger structured reports in `memory/delayed/*.json`, with file-store logic in `utils/delayed_memory_file_store.py`. + +Current save contract: + +```text + +{ + "title": "", + "summary": "", + "tags": [], + "body": "", + "anchor_fact_ids": [], + "facts_ids": [], + "attachments_ids": [] +} + +``` + +Important semantics: + +- report inventory and loaded report body are different prompt concepts; +- reports can link L4 facts and persistent files; +- `anchor_fact_ids` must be a subset of `facts_ids` under the current contract; +- loaded/pinned/referenced/inspected are different states; +- old key/value `SAVE_DELAYED_MEMORY_CONTENT` form is legacy only. + +### 7.6 Active Memory + +Active Memory is live unresolved structured state/commitments, stored independently from L1 text. + +Current model-facing boundary: + +```json +{"conditions":"...","custom_field":"..."} +``` + +`UPDATE_ACTIVE_MEMORY` also prefers flat JSON at the root after `active_memory_id`. + +Creation custom fields are explicit structure only: JSON root fields beside `conditions` are accepted (up to the current three-custom-field cap), while non-JSON text is preserved as conditions and is not mined for `(field: value)` suffixes. Duplicate normalized JSON keys follow normal last-value-wins behavior before the cap is applied. + +Compatibility code still accepts legacy nested `fields`/`updates`, older line-based update payloads, and a self-closing UPDATE attribute form. Those are localized reader compatibility. Internally/browser-side, Active Memory is still represented in a transitional string-record format with metadata suffixes; that is an implementation detail, not the desired model-facing schema. + +Paused Active records are removed from the Brain prompt. Metabolism may reorder the prompt projection by salience without changing canonical storage order. + +### 7.7 Persistent Files + +Persistent uploads are owned by `utils/attached_files_store.py` and `assets/files/` plus its index. Stable IDs are reused by: + +- prompt attachments; +- Delayed report links; +- file actions; +- UI memory/file projections; +- Live Avatar/link highlighting. + +--- + +## 8. Metabolism + +`runtime/metabolism.py` is a real active runtime subsystem in this snapshot, not merely a future night-cycle idea. + +It currently implements: + +- continuous homeostat levels; +- immediate foreground impulse before Brain generation; +- outcome settling after a turn; +- active-memory salience; +- delayed-memory scoring; +- L4 context-focus/significance scoring; +- lexical/association signals; +- temperature modulation; +- debounced SERVICE integration after committed L1 updates; +- browser state emission/persistence support. + +This does **not** prove that the older concept of a full nightly self-review/consolidation cycle is complete. That remains a separate product direction and must not be inferred from the existence of `metabolism.py`. + +--- + +## 9. Persistence and session continuity + +JIN deliberately splits persistence by owner/lifetime. + +| State | Current storage/owner | +| --- | --- | +| live runtime checkpoint / browser-facing session state | browser storage via runtime/session modules | +| Active Memory | browser runtime storage; anonymous mode can isolate it | +| Facts Memory candidate buckets | browser storage per facts-memory session ID | +| Delayed Memory | `memory/delayed/*.json` | +| L4 facts | `memory/facts/long_term_facts.json` | +| persistent files | `assets/files/` + file index | +| visible chat / reasoning logs | `logs/` | +| live in-process orchestration | `RuntimeContext` | + +### 9.1 Soft runtime resume + +`runtime_resume` reconnects browser runtime state to a live/new backend context without being an archived-session checkout. + +### 9.2 Session bootstrap / live checkpoint + +`ui/static/js/runtime/runtime-session.js` persists live checkpoints and sends `session_bootstrap` data when appropriate. The backend normalizes and hydrates the browser-provided snapshot in `websocket/bootstrap.py`. + +For a predecessor/browser checkpoint, `websocket/bootstrap.py::enrich_session_bootstrap_from_archive()` may enrich dialogue, reasoning, action history, files, counters, and selected runtime state from the raw archived log. The browser checkpoint `saved_at` is the freshness boundary: if the archive tail predates that checkpoint, archive-derived dialogue/reasoning/actions/files are intentionally not mixed into newer browser state. Therefore `saved_at` must represent a real whole-checkpoint refresh, not any incidental field mutation. + +`CLEAN_TOOL_RESULTS` has an explicit field-local persistence rule. On successful cleanup, the browser rewrites only `session_snapshot.tool_results` to `[]` inside the existing checkpoint and preserves checkpoint timestamp/lineage verbatim. During enrichment, the mere presence of `tool_results` (including `[]`) is authoritative, so old archived search/tool output cannot resurrect in a new tab. This exact-empty rule is intentionally **not** shared by `loaded_memory_ids` or `active_memory_records`; their empty browser collections still use the established archive fallback behavior. + +Session-action parts are structured continuity data. Bootstrap normalization currently preserves `text/detail/message/id` plus recognized `colors`; color metadata is normalized to lowercase `#rrggbb` (including expansion from `#rgb`) so restored JIN_COLOR actions keep the same swatch and hex hover metadata as live actions. + +Old L3-era fields can still appear in tests/compatibility paths, but there is no active L3 memory module. + +### 9.3 Archived session restore + +Archived restore is a distinct path: + +1. HTTP endpoint `/api/sessions/{session_id}/restore` builds payload from logs via `utils/session_restore.py`. +2. Browser renders archived visible messages and sends a `session_bootstrap`/restore payload. +3. Backend sets `runtime_session_restore_priming` and stages historical resource IDs/metadata. +4. A hidden `archived_session_resume` Brain tick receives restore-specific context. +5. The restore prompt includes the newest complete visible dialog pairs plus a bounded recent reasoning dump. +6. Historical loaded Delayed IDs, attached files, and avatar state are not blindly applied before the restore greeting. +7. `BrainNode` consumes the staged restore envelope and replays those resources through the normal runtime-action dispatcher. +8. The WebSocket tail performs defensive cleanup only; it must not apply the same resources a second time. + +Constants in the current source limit the restore reasoning dump to two recent reasoning items with a per-item character cap, while restored visible dialogue uses the normal recent-pair limit. + +`utils/session_restore.py` still understands historical `SAVE_SESSION` labels in archived logs. That is restore compatibility, not proof of a current `SAVE_SESSION` runtime action. + +### 9.4 Timestamp invariant + +Modern records/snapshots should retain original creation timestamps across serialize -> reload -> hydrate. `now()` is only a fallback for truly legacy records without a timestamp. Rendering/loading must not rewrite historical time. + +--- + +## 10. Anonymous / shadow mode + +`runtime/anonymous_mode.py` and browser storage logic implement anonymous/shadow behavior. + +Backend behavior: + +- runtime is marked anonymous and persistent writes restricted; +- global Delayed Memory can still be hydrated/read; +- Delayed file-store mutation is disabled; +- `UPDATE_L4_FACTS` and `SAVE_DELAYED_MEMORY` are explicitly restricted; +- persistent asset-write actions are restricted; +- read-only/runtime-local actions remain available where safe. + +Browser behavior can isolate session-facing stores in private/incognito mode. Do not broaden or narrow persistence semantics by assumption; trace both JS storage selection and backend write guards. + +--- + +## 11. Stop, interruption, and recovery + +`websocket/tasks.py::cancel_current_task()`: + +- marks the active turn interrupted; +- can distinguish discard vs interrupted-memory update; +- aborts active runtime actions; +- closes active model streams; +- cancels the task; +- can schedule the interrupted L1 update. + +`process_message()` later routes interrupted turns through `schedule_interrupted_runtime_memory_update()` instead of pretending the turn completed normally. + +The Brain/stream layer also contains recovery for reasoning repetition and model/context output limits. Follow-up ticks preserve sequence identity rather than starting a new user request. The corresponding session-action interruption entry is emitted before that recovery/follow-up starts, not deferred until the final answer. + +--- + +## 12. UI synchronization and Live Avatar + +The browser is a projection of runtime state, not an independent semantic authority. + +Important shared identities include: + +- Active Memory IDs; +- Delayed report IDs; +- L4 fact IDs; +- file IDs; +- runtime action IDs; +- turn/sequence IDs. + +The Live Avatar and memory panels consume the same identities but may show different **strength** depending on cause: + +- object actually loaded into context -> active/strong state; +- object merely referenced/cited by ID -> weak linked state; +- object only inspected in a modal -> no implied context load. + +Do not collapse these into a generic highlight state. + +The L4 panel deliberately separates compact browsing from surfaced evidence: ordinary fact rows use a 50-character value preview, while a row bubbled by runtime reference, explicit reasoning citation, or context-loaded state renders its full value. The storage value is never truncated; this is projection-only behavior. + +Session-action logger rows are also a projection of structured history. The compact logger keeps the most recent five items in chronological order with their original numbering and reuses the existing attached-files header/button primitive for `FULL`. JIN_COLOR parts render one swatch per applied color and expose the normalized hex on hover; bootstrap must preserve the `colors` payload for this to work after reload. Runtime-action bubble details are retained across counter-only updates so a count refresh cannot erase existing hover metadata. + +### Header auto-hide + +`ui/static/js/header-autohide.js` currently uses: + +- reveal-zone height = 2 x measured header height; +- show debounce = **333 ms** in this snapshot; +- hide delay = **1000 ms**; +- panel occlusion logic so moving over panels/avatar does not trigger the reveal path incorrectly. + +The product intent recorded on 2026-08-21 was approximately 250 ms reveal debounce. The code/intention mismatch is tracked in `JIN_CURRENT_STATE.md`; do not silently change it while working on an unrelated task. + +### Visual rule + +New UI states must reuse the closest existing JIN primitive. The UI deliberately avoids generic SaaS success/error visual language, arbitrary new glow colors, and decorative blur. + +--- + +## 13. Extension rules + +### New runtime action + +Start at `contracts/*.json`, then connect normalization/handler logic in `utils/actions/`, guard semantics if necessary, event/tool-result/follow-up behavior, and tests. + +### New durable state + +Define exactly one canonical owner, stable identity, persistence location, bootstrap/hydration path, mutation path, reconciliation rules, and browser projection. + +### New model context source + +Keep canonical state outside the prompt builder. Serialize it in `utils/context/` or `rules/brain_context_builder.py` at a deliberate position. + +### New UI behavior + +Use existing semantic IDs/events and existing CSS/DOM primitives first. New visual language requires explicit justification. + +--- + +## 14. Known architectural traps + +Do not infer architecture from these alone: + +- old root `README.md` / `ARCHITECTURE.md`; +- `tests/test_l2_memory.py` and `tests/test_l3_session_memory.py`; +- `SAVE_SESSION` references in old tests/log parsing; +- comments mentioning “L1/L2/L3 glow”; +- stale CodeGraph/search indexes; +- historical field names like `runtime_l3_session_memory`; +- treating any write to a browser checkpoint as permission to advance `saved_at`; +- assuming an empty collection always means "missing" during archive enrichment; +- sanitizing session actions down to text while dropping structured UI metadata such as `parts[].colors`. + +The verified filesystem has no active L2/L3 modules and no current `SAVE_SESSION` contract. + +For the exact transition/conflict list, see `docs/JIN_CURRENT_STATE.md`. diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md new file mode 100644 index 00000000..43b61af7 --- /dev/null +++ b/docs/JIN_CURRENT_STATE.md @@ -0,0 +1,496 @@ +# JIN Core Engine — Current State / Migration Notes + +**Snapshot inspected:** `jin_core(20260823-114203).zip` +**Inspection date:** 2026-08-23 +**Context reference:** `JIN_CORE_CONTEXT_TRANSPLANT_2026-08-23.md` + +This is the document to read before touching transitional code. It lists what is true in the inspected snapshot, what is legacy residue, and where product intent and implementation currently differ. + +--- + +## 1. Executive state + +The runtime is already on the post-L2/L3 architecture in the actual source tree, but the repository still contains substantial historical residue: + +- old root documentation still describes four live layers; +- L2/L3 tests still exist and import modules that no longer exist; +- old session names/fields remain in compatibility/tests; +- `SAVE_SESSION` remains in historical tests/log parsing but is not a current action contract; +- Active Memory has a modern flat JSON action boundary but an older string-record internal/browser representation; +- search action exposure now follows real configured provider availability rather than feature flags alone; +- `CLEAN_TOOL_RESULTS` now has explicit cleared-state bootstrap semantics so old tool results cannot resurrect without falsifying checkpoint freshness; +- session-action logger metadata survives bootstrap, including JIN_COLOR swatches/hex hover; +- the full test suite is currently far from green on this snapshot. + +New agents must not “repair” these contradictions by restoring the old architecture. + +--- + +## 2. Verified current topology + +### Backend/runtime + +Present and active: + +- `app.py` +- `websocket/` +- `agent/runtime.py` +- `agent/nodes/brain.py` +- `runtime/runtime_context.py` +- `runtime/stream.py` +- `runtime/L1_memory.py`, `L1_memory_rules.py`, `L1_memory_utils.py` +- `runtime/L4_memory.py`, `L4_memory_rules.py`, `L4_memory_utils.py` +- `runtime/metabolism.py` +- `runtime/anonymous_mode.py` +- `contracts/*.json` +- `utils/actions/*` +- `utils/context/*` +- `utils/session_restore.py` + +Not present: + +- `runtime/L2_memory.py` +- `runtime/L2_memory_utils.py` +- `runtime/L2_memory_rules.py` +- `runtime/L3_memory.py` +- `runtime/L3_memory_utils.py` +- `runtime/L3_memory_rules.py` + +Direct import check on the snapshot returns `ModuleNotFoundError` for the missing L2/L3 modules. + +--- + +## 3. L2/L3 legacy conflict + +### Product intent + +L2 and L3 were removed as architectural layers on 2026-08-19/20. + +### Current source + +The runtime package agrees: the modules are gone. + +### Legacy residue still present + +- root `README.md` still presents “The Four-Layer Memory Model”; +- root `ARCHITECTURE.md` still describes `runtime/L2_memory.py` and `runtime/L3_memory.py` as active; +- `tests/test_l2_memory.py` and `tests/test_l3_session_memory.py` still import the deleted modules; +- additional tests reference `runtime_l3_session_memory` and old session semantics; +- some JS comments/log filters mention L2/L3; +- `runtime-storage.js` explicitly contains one-time compatibility wording for checkpoints created before L3 removal. + +### Rule for agents + +Do not restore deleted modules to satisfy stale tests/docs. Classify each remaining reference as: + +1. required backward compatibility; +2. harmless historical wording; +3. stale test/documentation that should eventually be removed or rewritten; +4. accidental live dependency. + +Only category 4 is a runtime bug. + +--- + +## 4. Session continuity / `SAVE_SESSION` transition + +### Current implementation + +Current session continuity is split across: + +- browser live checkpoint persistence (`runtime-session.js` / `runtime-storage.js`); +- `runtime_resume` soft resume; +- `session_bootstrap` state hydration; +- archived session payload built from logs (`utils/session_restore.py`); +- hidden `archived_session_resume` priming tick; +- staged resource replay through the normal runtime-action dispatcher. + +### Current action layer + +`contracts/rules_assembler.py::ACTION_CONFIG_KEYS` contains no `SAVE_SESSION`, and there is no `contracts/save_session.json`. + +### Legacy residue + +`SAVE_SESSION` still appears in: + +- old tests; +- old behavior-probe expectations; +- `utils/session_restore.py::ACTION_LABELS` so historical archived logs can be interpreted; +- old L3/session terminology in test fixtures. + +### Rule for agents + +Treat `SAVE_SESSION` as historical/restore compatibility in this snapshot. Do not re-add a model action just to make old tests pass. + +--- + +## 5. Current action contract set + +The contract assembler currently maps these actions: + +```text +DEEP_WEB_SEARCH +WEB_SEARCH +CLEAN_TOOL_RESULTS +IDLE +JIN_COLOR +JIN_SIZE +JIN_POSITION +JIN_SPEED +UPDATE_L4_FACTS +LOAD_SKILL +UNLOAD_SKILL +ASSET_ACTION +LIST_FILES +ATTACH_FILE +DETACH_FILE +CREATE_TODO_LIST +RESOLVE_TODO +CHECK_TODO +SAVE_DELAYED_MEMORY +LOAD_DELAYED_MEMORY +UNLOAD_DELAYED_MEMORY +SAVE_ACTIVE_MEMORY +RESOLVE_ACTIVE_MEMORY +UPDATE_ACTIVE_MEMORY +``` + +`utils/actions/dispatcher.py` contains execution branches for the same action family. + +Default Brain action flags currently set `CAN_RUNTIME_TODO=False`; the other mapped feature flags in `rules/brain_context_builder.py` are enabled. `WEB_SEARCH` and `DEEP_WEB_SEARCH` are then filtered again by `settings.CAN_SEARCH`, so they are not model-visible unless provider `serper` has a non-empty, non-placeholder configured key. `config.example.py` exposes the search settings with `mock-serper-api-key`; that placeholder intentionally keeps search disabled until a real local key is configured. The local availability check intentionally does not impose an invented key-length/shape regex; Serper remains the credential authority. + +`CLEAN_TOOL_RESULTS` is a no-payload action. The canonical bare `` marker and a redundant paired-looking `...` form resolve to one cleanup; the closing tag is consumed as parser noise, including when split across stream chunks. + +`JIN_SIZE` contract version 2 currently advertises ` w:120 h:120 `; a single value such as ` 120px ` means square size. Size values may use px, percent, or vw/vh, with px as the default unit. + +--- + +## 6. Delayed Memory transition + +### Current canonical contract + +`contracts/save_delayed_memory.json` is version 4 and requires a JSON body inside ` ... `. + +Required fields: + +- `title` +- `summary` +- `tags` +- `body` + +Relationship fields: + +- `anchor_fact_ids` +- `facts_ids` +- `attachments_ids` + +The contract explicitly requires exact existing IDs and `anchor_fact_ids` as a subset of `facts_ids`. + +### Legacy + +Old key/value bodies and `` may still be normalized by compatibility code/data history but must not be documented as the preferred form. + +--- + +## 7. Active Memory transition + +### Current model-facing contract + +`SAVE_ACTIVE_MEMORY` version 4: + +```json +{"conditions":"CONDITIONS","custom_field_name":"VALUE"} +``` + +`UPDATE_ACTIVE_MEMORY` version 2: + +```json +{"active_memory_id":"existing id VALUE","field_name":"NEW_VALUE"} +``` + +The create parser now treats custom fields as explicit JSON structure only. A non-JSON body is preserved as the complete `conditions` value; parenthesized prose such as `(date: tomorrow)` is no longer reinterpreted as a custom field. JSON custom fields are capped at three after normalized duplicate keys use last-value-wins behavior. + +The update parser accepts: + +- current flat root fields; +- legacy `fields` object; +- legacy `updates` object; +- older line-based payloads; +- a self-closing attribute compatibility form such as ``. + +The self-closing attribute form is compatibility only; the model-facing contract remains paired flat JSON. + +### Current internal representation + +Active records are still stored/transported in a string-oriented record format with metadata suffixes. Prompt assembly refreshes metadata, removes paused items, and may rank the prompt view by metabolism salience. + +### Risk + +Do not “finish the migration” by replacing the internal storage shape in an unrelated task. The flat JSON decision applies to the action/model boundary; storage migration needs its own end-to-end plan. + +--- + +## 8. Prompt assembly — verified order + +Current `build_brain_context()` order is intentionally structured. Important anchors: + +- `CURRENT_RUNTIME_SETTINGS` is first when non-empty; +- restore instruction is next only for restore priming; +- current concerns and trusted runtime XML precede tool/session/action state; +- file + delayed inventories sit near the top on ordinary turns; +- Active/L1/loaded Delayed/L4 live together in the runtime-context group; +- restored exact dialog replaces the normal recent-dialog window for the one-shot restore path; +- action contracts remain present even on restore ticks, subject to effective capability filtering; +- both search contracts disappear from the prompt when `settings.CAN_SEARCH` is false; +- identity and loop rules are at the bottom. + +Any prompt-order change can alter behavior materially. Do not reorder sections for aesthetics. + +--- + +## 9. Bootstrap / archived restore — verified behavior + +### Browser predecessor bootstrap / archive enrichment + +A browser checkpoint may be enriched from the predecessor raw log, but only when the archive is not older than the checkpoint freshness boundary. `saved_at` therefore means “this whole checkpoint is current through here,” not “some field was touched recently.” Advancing it for a one-field cleanup can suppress otherwise valid archive enrichment of dialogue/reasoning/session-actions/files. + +The repaired `CLEAN_TOOL_RESULTS` path now mutates only the existing checkpoint's `session_snapshot.tool_results` to `[]`; it preserves `saved_at`, session lineage, and every unrelated checkpoint field. On bootstrap, explicit presence of `tool_results: []` is authoritative and blocks archive fallback for that field. Empty `loaded_memory_ids` and `active_memory_records` deliberately keep their prior archive-fallback semantics. + +Session-action normalization also preserves structured `parts[].colors` metadata. Valid `#rgb`/`#rrggbb` colors are normalized to lowercase six-digit hex, allowing restored JIN_COLOR history to render the same color square and hex hover as live history. + +### Archived restore + +Current restore code deliberately prevents the “double apply” class of bugs. + +Verified behavior: + +- archived visible dialogue is rebuilt from logs; +- the newest complete USER/JIN pairs are used for the restore context; +- a bounded recent reasoning dump is included; +- loaded Delayed IDs, attached files, and avatar state are staged; +- restore Brain response occurs before normal resource reactivation; +- `BrainNode.replay_session_restore_resource_actions()` consumes the staged envelope and applies it through the real action dispatcher; +- WebSocket tail only clears defensive state and explicitly warns against mutating/emitting resource state a second time. + +This is a strong architectural clue for future restore fixes: find duplicate writers before adding another apply. + +--- + +## 10. L4 / Facts Memory current path + +### Facts Memory + +Browser runtime storage creates per-session facts-memory candidate buckets from persisted runtime snapshots and synchronizes them to backend with `facts_memory_store_sync`. + +### L4 + +L4 has a real staged pipeline with: + +- pending candidate collection; +- extraction; +- merge/rebase; +- validation/recovery/backoff; +- explicit-edit protection; +- report-reference remapping; +- archive/anchor logic; +- delete/restore; +- file-store persistence. + +### L4 UI projection + +Ordinary L4 rows display a 50-character value preview. When a fact is bubbled because it was referenced, explicitly cited from reasoning, or loaded into context, the row displays the complete value with no preview truncation. This does not mutate or reorder the stored fact; it is a visibility rule for surfaced evidence. + +### Scheduling + +L4 background work is started by browser `l4_memory_idle_tick`. Server refuses to begin it while a foreground task is active or the pending request queue is non-empty. + +This is the current performance/ordering contract. Do not move L4 onto every foreground turn to “make facts faster” without proving latency and ordering behavior. + +--- + +## 11. Metabolism status + +`runtime/metabolism.py` is large and actively wired into the foreground/L1 flow. + +Verified active pieces include: + +- pre-Brain foreground impulse; +- post-turn settling; +- Active/Delayed/L4 significance/salience scoring; +- temperature adjustment; +- recent-turn memory; +- association learning; +- SERVICE update scheduled after successful L1 diff commit; +- bootstrap state support. + +### Not proven + +The older concept of a full nightly self-review cycle that reads cropped reasoning/context and emits a morning cleanup report is **not proven complete** by this snapshot. Do not describe it as finished architecture without finding the actual scheduler/storage/report path. + +--- + +## 12. Anonymous mode status + +Backend `runtime/anonymous_mode.py` currently: + +- marks runtime persistent writes restricted; +- disables Delayed file-store writes while allowing read hydration; +- blocks `UPDATE_L4_FACTS`; +- blocks `SAVE_DELAYED_MEMORY`; +- blocks persistent asset-write actions; +- leaves read-only/runtime-local paths available as appropriate. + +Browser runtime storage also contains isolation logic for anonymous/private windows. + +The intended product behavior remains: anonymous has its own Active/session state but can still see global L4/Delayed context. Verify both browser and backend sides before changing this. + +--- + +## 13. UI / visual state mismatches and active decisions + +### 13.1 Header reveal timing + +Current `ui/static/js/header-autohide.js`: + +```text +SHOW_DELAY_MS = 333 +HIDE_DELAY_MS = 1000 +``` + +Owner intent from the latest UX pass was approximately 250 ms reveal and 1 s hide. + +**State:** implementation/product-intent mismatch. Do not fix incidentally. + +### 13.2 FRAME naming + +Owner prefers `FRAME` over generic `STATE` for the question -> answer -> live-context cycle. + +**State:** preference recorded, but this snapshot does not establish a safe complete rename target. Do not global-replace `RUNTIME`/`STATE`. + +### 13.3 Visual reuse + +Hard owner rule remains active: reuse existing visual primitives; no invented highlight colors/glows/badges/strips unless explicitly approved. + +### 13.4 Loaded vs referenced highlight + +Strong loaded-context highlighting and weak ID-reference highlighting are separate semantics. Opening a modal is not a context load. This area has had regressions and must be traced through both panel state and avatar state. + +### 13.5 Session Actions logger + +The compact logger now shows the most recent five actions in chronological order while retaining their original history numbering; `FULL` uses the existing attached-files header/button visual primitive rather than a new style. JIN_COLOR entries depend on `parts[].colors` for the color square and hex hover, so that metadata is part of the bootstrap contract, not disposable presentation data. + +Validator/reasoning-loop and context/output-limit entries are emitted immediately when the interruption is detected, before automatic recovery/follow-up. `context_overflow` is included in context-limit finish reasons. This timing is intentional: the logger should reflect the loop while it is happening, not only after the final JIN response. + +### 13.6 Runtime action hover stability + +Runtime-action bubbles persist their detail in DOM dataset state. Counter-only updates reuse the existing detail/title instead of clearing it, so an aggregate count refresh cannot erase hover information. + +### 13.7 Interaction fixes landed in this snapshot + +- the latest manually chosen reasoning collapsed/expanded preference is persisted and reused by new reasoning blocks; +- live-turn top-lock releases once the user turn reaches the viewport bottom, after which normal overflow autoscroll can resume; +- clicking usable form padding focuses the JIN user input; +- answer-rating controls use directional hover titles (`Dislike answer` / `Like answer`) and do not put a count tooltip on the rated bubble; +- Win95 theme localStorage reads/writes are guarded so restricted storage contexts do not break theme switching. + +--- + +## 14. Test suite status on this exact snapshot + +Command run: + +```text +python -m unittest discover -s tests +``` + +Result on the inspected archive: + +```text +Ran 837 tests in 28.927s +FAILED (failures=67, errors=32, skipped=9) +``` + +Important implications: + +- the repository is **not green** in this snapshot; +- some collection/import errors are directly explained by stale L2/L3 tests importing deleted modules; +- several `tests/runtime_actions/*` modules also fail import because they still depend on the removed `clients.brain_client.should_execute_save_session` symbol; +- many additional failures/errors remain beyond those stale imports, including older prompt/follow-up/session-action/search expectations; +- do not use “full suite currently fails” as permission to ignore targeted regressions; +- for any patch, run the smallest relevant target set and report exact pass/fail counts. + +Do not resurrect L2/L3 merely to make stale tests import again. The test suite itself needs a deliberate cleanup/migration task. + +--- + +## 15. Old documentation status + +### Root `README.md` + +Currently stale in major architecture sections: + +- still describes L1/L2/L3/L4 as four active layers; +- still describes L2 pattern updates and L3 session digest; +- still describes old post-turn L1/L2 behavior; +- lists current features mixed with removed architecture. + +### Root `ARCHITECTURE.md` + +Also stale: + +- describes `runtime/L2_memory.py` and `runtime/L3_memory.py` as active files; +- describes Brain context as including live L2/L3 state; +- describes `SAVE_SESSION`/L3 flow as current. + +### Rule + +Use `docs/JIN_ARCHITECTURE.md` + `docs/JIN_DECISIONS.md` + this file as the current architecture baseline. Updating/removing old docs should be a separate explicit cleanup diff so product changes and documentation cleanup are reviewable. + +--- + +## 16. Search/index caveat + +During the failed Codex documentation attempt, CodeGraph continued returning deleted L2/L3 paths even after sync while the actual archive filesystem did not contain those modules. + +Treat any repository index as a navigation aid only. For existence/current behavior, verify the actual file and symbol in the working tree. + +--- + +## 17. Working-tree ownership caveat + +The Codex session that preceded these documents reported unrelated existing changes in the user's working tree (including `rules/runtime.py`, `pack_for_patch.bat`, and `ui/static/images/schema_old.jpg`). The provided ZIP does not include `.git`, so dirty status cannot be independently reconstructed here. + +Rule for future agents: assume pre-existing files/changes are user-owned; do not overwrite or “clean” unrelated work unless explicitly asked. + +--- + +## 18. Open / known-unknown items + +Do not present these as settled without fresh code evidence: + +- whether the full night self-review concept is implemented outside the inspected paths; +- which remaining L2/L3-named compatibility fields are still required for real historical data; +- whether all old `SAVE_SESSION` tests should be deleted or rewritten around checkpoints; +- final intended internal storage format for Active Memory after the flat-JSON boundary migration; +- exact final semantics/name for the UI `FRAME` label; +- whether reveal debounce should now be restored from 333 ms to the earlier 250 ms preference; +- final canonical list of supported noncanonical action marker aliases after compatibility cleanup; +- which current full-suite failures are regressions versus intentionally stale tests; +- whether every linked-highlight/pin/pause/delete path is currently synchronized between panel, prompt-loaded state, and avatar. + +When one of these becomes the task, investigate first and record the resolved decision in `JIN_DECISIONS.md`. + +--- + +## 19. Recommended near-term documentation cleanup order + +When the owner explicitly asks for cleanup: + +1. migrate/delete stale L2/L3 tests so the suite represents the post-L2/L3 architecture; +2. rewrite root `ARCHITECTURE.md` to point to or mirror `docs/JIN_ARCHITECTURE.md`; +3. update the README memory/session sections; +4. remove misleading old `SAVE_SESSION` feature claims while preserving archive compatibility code; +5. audit old comments/UI logger filters for L2/L3 wording; +6. only then remove compatibility fields that are proven unused by real archived data. + +Do not combine this cleanup with unrelated runtime behavior patches. diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md new file mode 100644 index 00000000..bc390564 --- /dev/null +++ b/docs/JIN_DECISIONS.md @@ -0,0 +1,412 @@ +# JIN Core Engine — Durable Decisions + +**Decision baseline:** reconciled on 2026-08-23 against `jin_core(20260823-114203).zip` and the accumulated project context. + +This file records product/architecture intent that should survive refactors. It is not a changelog and not a dump of historical experiments. + +Status vocabulary: + +- **Accepted / implemented** — intent and current code agree. +- **Accepted / transitional** — decision is current, but compatibility/old representation still exists. +- **Accepted / not fully implemented** — product decision exists, but current source does not completely realize it. +- **Rejected** — do not reintroduce without an explicit new decision. + +--- + +## D001 — The runtime is the product + +**Status:** Accepted / implemented + +JIN Core Engine is a model-agnostic cognitive runtime. BRAIN/SERVICE model choices are configuration, not architecture. + +**Why:** continuity, visible memory, actions, restore, UI semantics, and runtime state must survive model replacement. + +**Rejected alternative:** designing the codebase around one named model or treating JIN as a thin chatbot wrapper. + +--- + +## D002 — Direct foreground Brain path + +**Status:** Accepted / implemented + +The normal foreground path remains: + +```text +user -> AgentRuntime -> BrainNode +``` + +No generic planner/router is inserted before Brain by default. + +**Why:** lower latency, fewer hidden decisions, easier causality, less framework overhead. + +**Rejected alternative:** adopting a large external agent harness/framework before v1.0 simply because it has similar abstractions. + +--- + +## D003 — Inspectability and continuity are core product semantics + +**Status:** Accepted / implemented + +Memory/context/reasoning/actions/state must be inspectable enough that the user can understand what shaped a response and where continuity came from. + +**Why:** “Without context, there is no JIN.” JIN should re-enter a conversation as a continuing runtime, not as a fresh chatbot with a generic summary. + +**Rejected alternative:** opaque hidden RAG/state injection with no visible causality. + +--- + +## D004 — L2 and L3 are removed architectural layers + +**Status:** Accepted / implemented, with heavy legacy residue + +L2 and L3 are no longer live architectural layers. The current conceptual set is: + +- live L1/runtime frame; +- L4 durable facts; +- Active Memory; +- Delayed Memory; +- Files; +- runtime/session checkpoints. + +**Why:** L2/L3 created overlapping ownership and stale conceptual layers after newer continuity/memory mechanisms evolved. + +**Rejected alternative:** resurrecting L2/L3 because old README/tests are more complete than current docs. + +--- + +## D005 — Durable L1 is rejected; durable facts go to L4 + +**Status:** Accepted / implemented + +L1 is live/operational state. Durable user/project facts belong in L4 rather than a persistent L1-like layer. + +**Why:** one durable fact owner is easier to reconcile, inspect, age, link, and clean. + +**Rejected alternative:** another long-lived “live memory” store parallel to L4. + +--- + +## D006 — Memory systems remain purpose-specific + +**Status:** Accepted / implemented + +Active Memory, Delayed Memory, L4, live L1, Files, and checkpoints are not interchangeable layers. + +**Why:** they have different lifetimes, loading rules, UI semantics, and mutation paths. + +**Rejected alternative:** one growing generic memory transcript/store. + +--- + +## D007 — Runtime-action schemas belong in contracts + +**Status:** Accepted / transitional + +Concrete action syntax, fields, and action-specific rules belong in `contracts/*.json`. General runtime rules should describe only cross-action sequencing/invariants. + +**Why:** one model-facing source of truth prevents contradictory prompts and makes action evolution testable. + +**Rejected alternative:** duplicating the same action schema in `rules/runtime.py`, handlers, README, and prompt snippets. + +--- + +## D008 — Delayed Memory save uses JSON under `` + +**Status:** Accepted / implemented with legacy parser compatibility + +Canonical form: + +```text + +{ + "title": "", + "summary": "", + "tags": [], + "body": "", + "anchor_fact_ids": [], + "facts_ids": [], + "attachments_ids": [] +} + +``` + +**Why:** explicit structured schema, safer validation, clearer ID relationships. + +**Rejected alternative:** advertising `` key/value body as current syntax. + +--- + +## D009 — Active Memory model boundary is flat JSON + +**Status:** Accepted / transitional + +Create and update payloads should be plain root-level JSON. `conditions` is the default required create field; update uses `active_memory_id` plus changed fields at the root. + +**Why:** simpler model contract and less parser ambiguity. + +**Compatibility rule:** old nested `fields`/`updates`, older line-based updates, and a self-closing UPDATE attribute form may be accepted by localized compatibility code but should not be taught as the primary format. SAVE custom fields come only from explicit JSON root fields; plain prose/parenthesized text must not be silently promoted into schema. + +--- + +## D010 — Compatibility adapters stay local + +**Status:** Accepted / partially implemented + +When legacy data must still load, normalize it at a narrow boundary. Do not spread legacy conditionals through prompt assembly, UI, persistence, and action rules. + +**Why:** transition debt otherwise becomes permanent architecture. + +**Rejected alternative:** keeping multiple live formats everywhere “just in case.” + +--- + +## D011 — Current session continuity does not depend on a model `SAVE_SESSION` action + +**Status:** Accepted / implemented in current snapshot + +Live/session continuity is built around browser/runtime checkpoints, reconnect/bootstrap, archived chat/reasoning logs, and restore metadata. There is no current `SAVE_SESSION` contract. + +Historical `SAVE_SESSION` labels/fields can still be read for old archives/tests. + +**Why:** session continuity is runtime infrastructure and should not depend on the model remembering to emit a special save marker. + +**Rejected alternative:** reintroducing `SAVE_SESSION` from old docs/tests without a new product decision. + +--- + +## D012 — Archived restore uses a staged one-shot priming path + +**Status:** Accepted / implemented + +Archived restore first gives Brain bounded exact historical context, then reactivates historical resources through the normal action pipeline. Resource state is not applied independently again at the WebSocket tail. + +**Why:** prevents duplicate apply/races and preserves a natural continuity greeting before old resources become normal live state. + +**Rejected alternative:** multiple independent restore writers that all “ensure” final state. + +--- + +## D013 — Interrupted turn is not a completed turn + +**Status:** Accepted / implemented + +Stop/cancel/repetition/context-limit interruption must preserve separate lifecycle semantics. Partial reasoning/answer plus user silence must not be silently serialized as a normal finished exchange. + +**Why:** bootstrap continuity must reflect what actually happened. + +--- + +## D014 — Historical timestamps are data, not render-time defaults + +**Status:** Accepted / implementation must be protected + +Serialize/deserialize must preserve entity/snapshot timestamps. `now()` is only a fallback for a truly legacy record with no timestamp. + +**Why:** ordering, age, metabolism, and visible history become false if reload mutates time. + +**Rejected alternative:** stamping restored rows with current time for convenience. + +--- + +## D015 — Anonymous/shadow mode can read durable context but restricts persistent writes + +**Status:** Accepted / implemented + +Anonymous windows keep isolated session-facing state while still being able to read global durable context where intended. Persistent L4/Delayed/asset mutation is restricted by the backend. + +**Why:** private experimentation should not contaminate durable user memory, while still keeping JIN useful. + +**Rejected alternative:** either making anonymous mode completely blind or letting it silently mutate global stores. + +--- + +## D016 — UI must reuse JIN's existing visual language + +**Status:** Accepted / mandatory + +Before adding a bubble, banner, highlight, tooltip, hover state, button, or feedback effect, find the closest existing JIN primitive and reuse it. + +**Why:** color/glow/geometry already encode runtime semantics. Random new visuals create false meanings and make the system look like mixed UI kits. + +**Explicitly rejected:** inventing a new blue/turquoise/green highlight just because a feature is new; generic SaaS success/error badges; cheerful icon sets; unnecessary blur. + +--- + +## D017 — Loaded, referenced, inspected, pinned, paused, and deleted are distinct UI semantics + +**Status:** Accepted / implemented in parts, regression-sensitive + +Examples: + +- full object loaded into prompt -> strong/active context signal; +- ID merely cited/referenced -> weak linked signal; +- modal opened -> inspect only, not context load; +- pin/unpin -> persistence/loading preference; +- pause -> excluded from context without delete; +- delete/restore -> lifecycle mutation. + +**Why:** visual causality must match runtime causality. + +**Rejected alternative:** calling one generic highlight routine on every UI interaction. + +--- + +## D018 — UI interaction language should converge, not proliferate + +**Status:** Accepted / partially implemented + +Memory/file items use a shared interaction language where applicable: long press for delete, half-long pause, click to restore/pending/open according to the existing component semantics. + +**Why:** fewer special-case controls and a more coherent object language. + +**Rejected alternative:** adding a separate conventional delete button to every item while the gesture contract already exists. + +--- + +## D019 — Consecutive JIN visual actions should read as one sequence + +**Status:** Accepted / implementation-specific + +Consecutive color/size/position/speed actions should be visually sequenced rather than looking like unrelated mechanical phases. Position movement should ease in/out while respecting semantic speed. + +**Why:** the Live Avatar is a runtime expression channel, not four disconnected CSS toggles. + +**Rejected alternative:** adding a second parallel animation engine when the existing sequence runner can be extended. + +--- + +## D020 — Do not invent a second source of truth to fix restore/UI bugs + +**Status:** Accepted / mandatory + +For bootstrap, tint, timestamp, linked-highlight, and session bugs, first enumerate every writer and establish ordering. + +**Why:** many prior regressions were duplicate-apply problems. + +**Explicitly rejected:** second listeners, second tint writes, duplicate guards without identity analysis, render-time `Date.now()`, state-only hover payloads that never reach DOM. + +--- + +## D021 — `CURRENT_RUNTIME_SETTINGS` is the absolute first optional prompt block + +**Status:** Accepted / implemented + +`rules/brain_context_builder.py` owns `CURRENT_RUNTIME_SETTINGS_CONTENT`. If it is empty, the block is omitted. If present, it precedes every other prompt section, including restore-specific instructions. + +**Why:** this block is an explicit runtime-level override/context injector and must have deterministic placement. + +--- + +## D022 — Keep heavy/background work off the foreground critical path where possible + +**Status:** Accepted / implemented in principle + +Optimization priority: + +1. delete redundant calls; +2. safely combine calls without changing ordering; +3. cache stable results; +4. move non-required work to idle/background; +5. keep only causally required operations on the foreground path. + +**Why:** chat latency is a product concern. + +**Constraint:** never trade ordering, memory correctness, recoverability, or observability for fewer calls. + +--- + +## D023 — No large framework rewrite before v1.0 + +**Status:** Accepted + +The existing JIN skeleton already has state, named prompt sections, dynamic context, actions, continuity, pruning/tool-result handling, observability, and model roles. + +**Why:** a framework migration before v1.0 creates risk without proving a product gain. + +**Rejected alternative:** rewriting JIN around DeepSeek-style/other agent harnesses because their vocabulary looks similar. + +--- + +## D024 — `FRAME` is the preferred conceptual direction over generic `STATE`, but rename is not automatic + +**Status:** Accepted / not fully implemented + +The owner prefers `FRAME` for the concrete question -> answer -> live-context cycle because `STATE` is too broad. + +**Constraint:** do not perform a blind RUNTIME/STATE -> FRAME rename. First identify the exact UI label and its semantic scope. + +--- + +## D025 — Header auto-hide behavior: delayed reveal, slower hide + +**Status:** Accepted / code currently differs on reveal delay + +Product intent recorded on 2026-08-21: + +- reveal only after hover dwell of roughly 250 ms; +- hide roughly 1 s after leaving; +- reveal catch zone must not fire merely because the pointer passes over the avatar/panels. + +Current code uses 333 ms reveal and 1000 ms hide. This mismatch is documented, not silently resolved here. + +--- + +## D026 — Search actions require real configured provider capability + +**Status:** Accepted / implemented + +`WEB_SEARCH` and `DEEP_WEB_SEARCH` are model-visible only when both their runtime feature flags and `settings.CAN_SEARCH` allow them. For the current Serper integration, local capability means provider `serper` plus a non-empty, non-placeholder configured API key. The placeholder in `config.example.py` is configuration documentation, not an enabled capability. + +**Why:** advertising an action that cannot execute creates fake affordance and failed loops. Conversely, Serper does not define a stable client-side key shape, so arbitrary length/prefix regexes can incorrectly hide valid credentials. + +**Rejected alternatives:** feature flags alone deciding search availability; hard-coding guessed Serper key formats instead of letting the provider validate credentials. + +--- + +## D027 — `CLEAN_TOOL_RESULTS` is an authoritative field-local tombstone, not a checkpoint refresh + +**Status:** Accepted / implemented + +Successful cleanup persists `session_snapshot.tool_results = []` in the existing browser checkpoint. That explicit empty value is authoritative during predecessor bootstrap and must not be repopulated from older archived tool results. + +The cleanup must preserve the checkpoint's `saved_at`, lineage, and unrelated fields. `saved_at` is the freshness boundary used to decide whether archived dialogue/reasoning/session-actions/files are safe to mix into browser state; touching it for one cleared field can suppress the rest of bootstrap. + +This exact-empty rule is intentionally scoped to `tool_results`. Empty `loaded_memory_ids` and `active_memory_records` retain their existing archive-fallback behavior. + +**Rejected alternatives:** calling the full live-checkpoint persistence path after CLEAN; treating every empty collection as either universally authoritative or universally missing. + +--- + +## D028 — Session-action interruption telemetry is emitted at cause time + +**Status:** Accepted / implemented + +Validator/reasoning loops and context/output-limit recovery must append and emit their session-action entry immediately when the interruption is detected, before automatic continuation/follow-up runs. `context_overflow` belongs to the context-limit recovery family. + +**Why:** Session Actions is causal runtime telemetry. If the event appears only after the final answer, the UI gives a false ordering of what the runtime was doing. + +**Rejected alternative:** recording the interruption only during end-of-turn/final response compaction. + +--- + +## D029 — Structured Session Action metadata is continuity data + +**Status:** Accepted / implemented + +Bootstrap sanitization must preserve recognized structured action-part metadata required by the logger, not only human-readable text. For JIN_COLOR, `parts[].colors` round-trips through bootstrap and is normalized to lowercase `#rrggbb` so restored rows keep color swatches and hex hover text. + +**Why:** text-only restoration can look superficially correct while silently destroying UI semantics after reload. + +**Rejected alternative:** reducing persisted/restored action parts to `text/detail/message/id` when additional recognized metadata drives the existing projection. + +--- + +## D030 — Surfaced L4 evidence expands; ordinary rows stay compact + +**Status:** Accepted / implemented + +The ordinary L4 panel uses a compact 50-character value preview. A fact bubbled by runtime reference, explicit reasoning citation, or context-loaded state displays the full value with no truncation. + +**Why:** the panel should remain scan-friendly by default, while evidence JIN actually surfaced must be readable in full. The expansion is a UI projection and must not mutate canonical L4 storage/order. + +**Rejected alternatives:** truncating surfaced citations; expanding every L4 row all the time. diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index b3b00aff..ffedaaaa 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -13,11 +13,18 @@ LOW_DIFF_RULES, MIDDLE_DIFF_RULES, NORMAL_DIFF_RULES from contracts.rules_assembler import ( build_runtime_action_instructions, - get_enabled_runtime_actions, + get_enabled_runtime_actions as get_contract_enabled_runtime_actions, +) +from app_settings import ( + settings, ) CURRENT_RUNTIME_SETTINGS_CONTENT = ("") +SEARCH_RUNTIME_ACTION_FLAGS = ( + "CAN_DEEP_WEB_SEARCH", + "CAN_WEB_SEARCH", +) SERVICE_AS_BRAIN_RUNTIME_ACTIONS = { "CAN_DEEP_WEB_SEARCH": True, @@ -51,6 +58,44 @@ "CAN_UPDATE_L4_FACTS": True, } + +def search_actions_available() -> bool: + + return bool( + getattr( + settings, + "CAN_SEARCH", + False, + ) + ) + + +def get_effective_runtime_actions( + runtime_actions=None, +) -> dict: + + effective_actions = dict( + runtime_actions + or {} + ) + + if not search_actions_available(): + for flag_name in SEARCH_RUNTIME_ACTION_FLAGS: + effective_actions[flag_name] = False + + return effective_actions + + +def get_enabled_runtime_actions( + runtime_actions=None, +) -> tuple[str, ...]: + + return get_contract_enabled_runtime_actions( + get_effective_runtime_actions( + runtime_actions + ) + ) + LOADED_DELAYED_MEMORY_CONTEXT_FIELDS = ( "title", "summary", @@ -1115,7 +1160,9 @@ def build_brain_context( prompt_parts.append( build_runtime_xml( context, - runtime_actions, + get_effective_runtime_actions( + runtime_actions + ), ) ) diff --git a/rules/identity.py b/rules/identity.py index bfafb425..d23fa445 100644 --- a/rules/identity.py +++ b/rules/identity.py @@ -48,7 +48,7 @@ "Don't just wait for commands; engage.\n" "Verify the essence through cautious clarification when internal weights suggest a direction based on sparse signals.\n" "\n" - "Visual/draw request: pick closest modality (e.g. table, ASCII, emoji, markdown, ... )\n" + "Visual/draw request: Don't overthink; Don't polish; Avoid useless deliberation; Just deliver as quick as possible; Pick closest modality (e.g. table, ASCII, emoji, markdown and others).\n" "\n" "Runtime memory keeps continuity through:\n" "- L1 for current session facts - Runtime Memory\n" diff --git a/rules/runtime.py b/rules/runtime.py index 6a2b2585..4314e9b3 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -53,6 +53,7 @@ "Do not invent missing context and do not treat this bootstrap as permission to start " "new work or runtime actions. Respond briefly and naturally, acknowledge that JIN is " "back in place, and make it easy for the user to continue from exactly where we left off and move on.\n" + "Use the language of the most recent non-empty user message in PREVIOUS_CHAT_MESSAGES.\n" "" ) diff --git a/runtime/metabolism.py b/runtime/metabolism.py index 1522179a..307d8073 100644 --- a/runtime/metabolism.py +++ b/runtime/metabolism.py @@ -1970,7 +1970,7 @@ def build_metabolism_brain_context( f"active_memory_salience_top: {top_active_line}", ] if instruction: - parts.append(f"Instructions: {instruction}") + parts.append(f"Possible reaction: {instruction}") parts.append("") return "\n".join(parts) diff --git a/runtime/stream.py b/runtime/stream.py index 223a2b6b..074a11f1 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -110,6 +110,7 @@ CONTEXT_LIMIT_FINISH_REASONS = frozenset({ "context_length", "context_limit", + "context_overflow", }) GENERATION_LIMIT_FINISH_REASONS = ( @@ -750,12 +751,12 @@ def classify_generation_limit( def mark_context_limit_recovery( self, finish_reason: str, - ) -> None: + ) -> bool: if not self.should_follow_up_on_context_limit( finish_reason ): - return + return False self.context_limit_recovery_armed = True stage = self.detect_context_limit_stage() @@ -794,6 +795,8 @@ def mark_context_limit_recovery( ), ) + return True + async def close_active_streams(self): active_streams = getattr( @@ -874,10 +877,10 @@ def mark_validator_interruption( def record_validator_interruption_history( self, validator=None, - ) -> None: + ) -> bool: if not self.is_brain_context(): - return + return False if validator is None: validator = getattr( @@ -928,6 +931,8 @@ def record_validator_interruption_history( history_text, ) + return True + async def filter_runtime_action_content( self, content: str, @@ -3039,12 +3044,19 @@ async def run( # ------------------------------------------------- if chunk_type == "finish": - self.mark_context_limit_recovery( - chunk.get( - "finish_reason", - "", + context_limit_recorded = ( + self.mark_context_limit_recovery( + chunk.get( + "finish_reason", + "", + ) ) ) + if context_limit_recorded: + await emit_session_actions_update( + self.context, + current_sequence=True, + ) continue @@ -3085,14 +3097,21 @@ async def run( self.mark_validator_interruption( self.stream.thinking_validator ) + history_recorded = ( + self.record_validator_interruption_history( + self.stream.thinking_validator + ) + ) + if history_recorded: + await emit_session_actions_update( + self.context, + current_sequence=True, + ) await self.close_active_streams() await self.close_generator( generator ) - self.record_validator_interruption_history( - self.stream.thinking_validator - ) await self.stream.finish( emit=self.emit_to_chat @@ -3165,12 +3184,19 @@ async def run( ): self.capture_runtime_turn_response() self.mark_validator_interruption() + history_recorded = ( + self.record_validator_interruption_history() + ) + if history_recorded: + await emit_session_actions_update( + self.context, + current_sequence=True, + ) await self.close_active_streams() await self.close_generator( generator ) - self.record_validator_interruption_history() await self.stream.finish( emit=self.emit_to_chat diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 8aedc9cd..1c02b943 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -728,6 +728,47 @@ def test_extracts_update_active_memory_json_block_from_active_memory_capability( ) + def test_extracts_update_active_memory_self_closing_attribute_marker(self): + + marker = ( + '' + ) + + result = extract_runtime_actions( + ( + "before " + + marker + + " after" + ), + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + self.assertEqual(result.text, "before after") + self.assertEqual( + result.actions, + ( + RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=( + 'active_memory_id="abc123" ' + 'last_update="23 august" current_photos=2 ' + 'last_photo_id="def456"' + ), + ), + ), + ) + self.assertEqual( + result.removed_markers, + ( + marker, + ), + ) + + def test_save_active_memory_materializes_hidden_custom_state_fields(self): context = FakeContext() @@ -1167,6 +1208,86 @@ def test_update_active_memory_accepts_json_inside_raw_marker(self): ) + def test_update_active_memory_accepts_self_closing_attribute_marker(self): + + context = FakeContext() + context.emitter = FakeEmitter() + context.timestamp = "2026-08-23T10:00:00" + context.session_id = "state-session" + context.turn_number = 11 + + asyncio.run( + apply_runtime_action_calls( + context, + ( + RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload=( + '{"conditions":"Track workspace photo state.",' + '"last_update":"22 august",' + '"current_photos":"1",' + '"last_photo_id":"qamzck"}' + ), + ), + ), + ) + ) + active_memory_id = context.emitter.events[0]["active_memory_id"] + context.emitter.events.clear() + context.timestamp = "2026-08-23T10:01:00" + + result = extract_runtime_actions( + ( + f'' + ), + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + applied_count = asyncio.run( + apply_runtime_action_calls( + context, + result.actions, + ) + ) + + self.assertEqual(result.text, "") + self.assertEqual(applied_count, 1) + record = context.active_memory_records[0] + self.assertIn("[ last_update: 23 august ]", record) + self.assertIn("[ current_photos: 2 ]", record) + self.assertIn("[ last_photo_id: 8vyf97 ]", record) + self.assertIn( + "[ updated_at: 2026-08-23T10:01:00 ]", + record, + ) + event = context.emitter.events[0] + self.assertEqual(event["status"], "completed") + self.assertEqual(event["active_memory_id"], active_memory_id) + self.assertEqual( + event["active_memory_changes"], + [ + { + "field": "last_update", + "before": "22 august", + "after": "23 august", + }, + { + "field": "current_photos", + "before": "1", + "after": "2", + }, + { + "field": "last_photo_id", + "before": "qamzck", + "after": "8vyf97", + }, + ], + ) + + def test_update_active_memory_rejects_new_field_atomically(self): context = FakeContext() diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index 40a24e7b..a19fc67a 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -574,6 +574,37 @@ def test_extracts_clean_tool_results_marker(self): ) + def test_clean_tool_results_redundant_close_tag_is_same_action(self): + + result = extract_runtime_actions( + " ", + enabled_actions=[ + RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + ], + ) + + self.assertEqual( + result.text, + "", + ) + self.assertEqual( + result.actions, + ( + RuntimeActionCall( + name=RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + payload="", + ), + ), + ) + self.assertEqual( + result.removed_markers, + ( + "", + "", + ), + ) + + def test_repeated_clean_tool_results_markers_remain_countable(self): result = extract_runtime_actions( @@ -655,6 +686,87 @@ def test_extracts_self_closing_runtime_markers_without_blocks(self): ) + def test_extracts_self_closing_update_active_memory_attributes(self): + + marker = ( + '' + ) + + result = extract_runtime_actions( + marker, + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + self.assertEqual( + result.text, + "", + ) + self.assertEqual( + result.actions, + ( + RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=( + 'active_memory_id="abc123" ' + 'last_update="23 august" current_photos=2 ' + 'last_photo_id="8vyf97"' + ), + ), + ), + ) + self.assertEqual( + result.removed_markers, + ( + marker, + ), + ) + + + def test_dedupes_duplicate_self_closing_update_active_memory_attributes(self): + + marker = ( + '' + ) + + result = extract_runtime_actions( + marker + marker, + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + self.assertEqual( + result.text, + "", + ) + self.assertEqual( + result.actions, + ( + RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=( + 'active_memory_id="abc123" ' + 'last_update="23 august" current_photos=2 ' + 'last_photo_id="8vyf97"' + ), + ), + ), + ) + self.assertEqual( + result.removed_markers, + ( + marker, + marker, + ), + ) + + def test_preserves_marker_when_action_disabled(self): result = extract_runtime_actions( @@ -995,6 +1107,58 @@ def test_stream_filter_handles_split_clean_tool_results_marker(self): ) + def test_stream_filter_strips_redundant_clean_tool_results_close_tag(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=[ + RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + ], + ) + + first = stream_filter.filter( + "visible answer\n\n " + ) + second = stream_filter.filter( + "" + ) + + self.assertEqual( + first.text, + "visible answer", + ) + self.assertEqual( + first.actions, + ( + RuntimeActionCall( + name=RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + ), + ), + ) + self.assertEqual( + second.text, + "", + ) + self.assertEqual( + second.actions, + (), + ) + self.assertEqual( + third.text, + "", + ) + self.assertEqual( + third.actions, + (), + ) + self.assertEqual( + stream_filter.flush(), + "", + ) + + def test_stream_filter_handles_split_bracketed_web_search_marker(self): stream_filter = RuntimeActionStreamFilter( @@ -1272,6 +1436,176 @@ def test_stream_filter_holds_confirmed_action_until_close(self): ) + def test_stream_filter_starts_partial_update_active_memory_attribute_marker(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + result = stream_filter.filter( + '' + ) + + result = stream_filter.filter( + marker + ) + + self.assertEqual( + result.started_actions, + ( + RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload="", + ), + ), + ) + self.assertEqual( + result.actions, + ( + RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=( + 'active_memory_id="abc123" ' + 'last_update="23 august" current_photos=2 ' + 'last_photo_id="8vyf97"' + ), + ), + ), + ) + + + def test_stream_filter_extracts_update_active_memory_attributes_across_chunks(self): + + marker_text = ( + '' + ) + expected_action = RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=( + 'active_memory_id="abc123" ' + 'last_update="23 august" current_photos=2 ' + 'last_photo_id="8vyf97"' + ), + ) + + for split_at in range(1, len(marker_text)): + with self.subTest(split_at=split_at): + stream_filter = RuntimeActionStreamFilter( + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + first = stream_filter.filter( + marker_text[:split_at] + ) + second = stream_filter.filter( + marker_text[split_at:] + ) + final = stream_filter.flush_result() + + self.assertEqual( + ( + first.text + + second.text + + final.text + ), + "", + ) + self.assertEqual( + ( + *first.actions, + *second.actions, + *final.actions, + ), + ( + expected_action, + ), + ) + + + def test_stream_filter_drops_incomplete_update_active_memory_attribute_marker(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + result = stream_filter.filter( + 'hello ' + ) + + result = extract_runtime_actions( + marker, + enabled_actions=[ + "CAN_SAVE_ACTIVE_MEMORY", + ], + ) + + self.assertEqual( + result.text, + marker, + ) + self.assertEqual( + result.actions, + (), + ) + + def test_stream_filter_preserves_disabled_action_marker(self): stream_filter = RuntimeActionStreamFilter( diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index 333b4df5..27814f8d 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -693,6 +693,80 @@ def test_browser_checkpoint_does_not_mix_in_older_raw_dialogue(self): self.assertNotIn("restore_reasoning_dump", enriched) self.assertTrue(enriched["archived_session_restore"]) + def test_browser_checkpoint_explicit_empty_tool_results_stays_empty(self): + archived = { + "source_session_id": "archive-session", + "messages": [ + { + "ts": "2026-08-23T10:30:00+03:00", + "role": "jin", + "text": "current tail", + }, + ], + "tool_results": [ + { + "kind": "search", + "result": "stale search result", + "created_at": 1.0, + }, + ], + } + + with patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=archived, + ): + enriched = enrich_session_bootstrap_from_archive( + { + "type": "session_bootstrap", + "source_session_id": "archive-session", + "saved_at": "2026-08-23T10:29:00+03:00", + "runtime_memory": "active_topic: current", + "tool_results": [], + } + ) + + self.assertIn("tool_results", enriched) + self.assertEqual(enriched["tool_results"], []) + + def test_empty_memory_collections_keep_archive_fallback_semantics(self): + archived = { + "source_session_id": "archive-session", + "messages": [ + { + "ts": "2026-08-23T10:30:00+03:00", + "role": "jin", + "text": "current tail", + }, + ], + "loaded_memory_ids": ["delayed-1"], + "active_memory_records": [ + {"id": "active-1", "conditions": "keep me"}, + ], + } + + with patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=archived, + ): + enriched = enrich_session_bootstrap_from_archive( + { + "type": "session_bootstrap", + "source_session_id": "archive-session", + "saved_at": "2026-08-23T10:29:00+03:00", + "runtime_memory": "active_topic: current", + "loaded_memory_ids": [], + "active_memory_records": [], + } + ) + + self.assertEqual(enriched["loaded_memory_ids"], ["delayed-1"]) + self.assertEqual( + enriched["active_memory_records"], + [{"id": "active-1", "conditions": "keep me"}], + ) + + def test_browser_checkpoint_still_uses_raw_dialogue_when_log_reaches_save(self): archived = { "source_session_id": "archive-session", diff --git a/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py new file mode 100644 index 00000000..37b9a275 --- /dev/null +++ b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py @@ -0,0 +1,53 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_SESSION_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-session.js" +) +RUNTIME_ACTIONS_JS = ( + ROOT / "ui" / "static" / "js" / "socket" / "runtime-actions.js" +) +DISPATCHER_PY = ROOT / "utils" / "actions" / "dispatcher.py" + + +class CleanToolResultsBootstrapCheckpointContractTests(unittest.TestCase): + + def test_clean_only_mutates_tool_results_in_existing_browser_checkpoint(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + start = source.index("function clearPersistedToolResultsCheckpoint()") + end = source.index("function buildCheckpointRuntimeSnapshot", start) + block = source[start:end] + + self.assertIn("readLatestSavedSessionSnapshot()", block) + self.assertIn("...previousCheckpoint", block) + self.assertIn("...previousSessionSnapshot", block) + self.assertIn("tool_results: []", block) + self.assertNotIn("new Date().toISOString()", block) + self.assertNotIn("saved_at:", block) + self.assertNotIn("writeLatestSavedRuntimeMemory", block) + + def test_clean_runtime_action_does_not_rewrite_full_live_checkpoint(self): + source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") + anchor = 'action === "clean_tool_results"' + start = source.index(anchor) + end = source.index("if (displayText.trim())", start) + block = source[start:end] + + self.assertIn("clearPersistedToolResultsCheckpoint", block) + self.assertNotIn("persistLiveSessionCheckpoint", block) + self.assertNotIn("data.session_snapshot", block) + + def test_backend_clean_completion_no_longer_ships_full_session_snapshot(self): + source = DISPATCHER_PY.read_text(encoding="utf-8") + start = source.index('[RUNTIME ACTION] clean_tool_results requested') + end = source.index("await emit_rejected_active_memory_results", start) + block = source[start:end] + + self.assertNotIn("build_runtime_session_checkpoint", block) + self.assertNotIn('"session_snapshot": session_snapshot', block) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_header_autohide_client_contract.py b/tests/test_header_autohide_client_contract.py index 61851949..afd7f865 100644 --- a/tests/test_header_autohide_client_contract.py +++ b/tests/test_header_autohide_client_contract.py @@ -103,6 +103,38 @@ def test_panels_occlude_the_hidden_header_hover_catch_zone(self): source, ) + def test_chat_content_occludes_the_hidden_header_hover_catch_zone(self): + source = HEADER_JS.read_text(encoding="utf-8") + + self.assertIn( + 'const chatHistory = document.getElementById("chat-history");', + source, + ) + self.assertIn( + "let pointerOccludedByChatContent = false;", + source, + ) + self.assertIn( + "function pointerIsOnChatContent(target)", + source, + ) + for selector in ( + ".jin-chat-avatar", + ".jin-chat-bubble", + ".jin-think-content", + ".jin-runtime-action-row > *", + ".jin-session-restore-divider", + ): + self.assertIn(selector, source) + self.assertIn( + "&& !pointerOccludedByChatContent", + source, + ) + self.assertIn( + "pointerOccludedByChatContent = pointerIsOnChatContent(target);", + source, + ) + def test_temporary_header_shift_does_not_pollute_saved_room_geometry(self): source = LOGGER_JS.read_text(encoding="utf-8") diff --git a/tests/test_l4_context_focus_contract.py b/tests/test_l4_context_focus_contract.py index 0589a7a4..28f361c8 100644 --- a/tests/test_l4_context_focus_contract.py +++ b/tests/test_l4_context_focus_contract.py @@ -9,6 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] MEMORY_VIEW_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +MEMORY_MODEL_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-model.js" AVATAR_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" THINK_CITATIONS_JS = ROOT / "ui" / "static" / "js" / "think-citations.js" TRACE_MODAL_JS = ROOT / "ui" / "static" / "js" / "logger" / "trace-modal.js" @@ -142,6 +143,20 @@ def test_l4_visual_focus_is_explicit_reasoning_citation_only(self): self.assertIn('addLine("l4",', citations) self.assertIn('citationType: "l4_citation"', citations) + def test_bubbled_l4_rows_show_full_value_while_default_preview_is_50_chars(self): + memory_view = MEMORY_VIEW_JS.read_text(encoding="utf-8") + memory_model = MEMORY_MODEL_JS.read_text(encoding="utf-8") + + self.assertIn( + "const RUNTIME_MEMORY_VALUE_DISPLAY_MAX_CHARS = 50;", + memory_model, + ) + self.assertIn("function isLongTermMemoryRowBubbled(row)", memory_view) + self.assertIn('row.classList.contains("runtime-memory-citation-hit")', memory_view) + self.assertIn('row.classList.contains("runtime-memory-context-loaded-hit")', memory_view) + self.assertIn("syncLongTermMemoryRowValueDisplay(row);", memory_view) + self.assertIn("runtimeMemoryValueFullText", memory_view) + def test_context_modal_renders_clean_fact_and_ordered_report_fallback(self): source = TRACE_MODAL_JS.read_text(encoding="utf-8") css = MEMORY_CSS.read_text(encoding="utf-8") diff --git a/tests/test_runtime_memory_hover_title_client_contract.py b/tests/test_runtime_memory_hover_title_client_contract.py new file mode 100644 index 00000000..913884f0 --- /dev/null +++ b/tests/test_runtime_memory_hover_title_client_contract.py @@ -0,0 +1,71 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-memory-view.js" +) +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimeMemoryHoverTitleClientContractTests(unittest.TestCase): + + def test_hover_titles_live_outside_dom_until_mouseenter(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "const runtimeMemoryHoverTitleSources = new WeakMap();", + source, + ) + self.assertIn( + 'node.addEventListener("mouseenter", () => {', + source, + ) + self.assertIn( + 'node.setAttribute("title", title);', + source, + ) + self.assertIn( + 'node.addEventListener("mouseleave", () => {', + source, + ) + self.assertIn( + 'node.removeAttribute("title");', + source, + ) + + def test_runtime_memory_view_does_not_prepopulate_native_titles(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertNotIn(".title =", source) + self.assertEqual( + source.count('setAttribute("title", title)'), + 1, + ) + + def test_rows_and_values_bind_hover_only_titles(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "bindRuntimeMemoryHoverTitle(\n row,\n hoverTitle", + source, + ) + self.assertIn( + "bindRuntimeMemoryHoverTitle(\n valueSpan,\n hoverTitle", + source, + ) + + def test_memory_view_cache_key_is_bumped_for_hover_titles(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("hover-title=1", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_memory_lazy_rows_client_contract.py b/tests/test_runtime_memory_lazy_rows_client_contract.py new file mode 100644 index 00000000..12d898d0 --- /dev/null +++ b/tests/test_runtime_memory_lazy_rows_client_contract.py @@ -0,0 +1,74 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-memory-view.js" +) +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimeMemoryLazyRowsClientContractTests(unittest.TestCase): + + def test_memory_panels_materialize_rows_in_batches_of_forty(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "const RUNTIME_MEMORY_LAZY_BATCH_SIZE = 40;", + source, + ) + self.assertIn( + "function beginRuntimeMemoryLazyCollection(items, renderItem)", + source, + ) + self.assertIn( + "for (let index = start; index < end; index += 1)", + source, + ) + self.assertGreaterEqual( + source.count("beginRuntimeMemoryLazyCollection("), + 4, + ) + + def test_lazy_rows_are_not_precreated_and_hidden_in_dom(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertNotIn( + 'row.style.display =\n index < runtimeMemoryVisibleRowCount', + source, + ) + self.assertIn( + "runtimeMemoryLazyAppendBatch = appendBatch;", + source, + ) + self.assertIn( + "runtimeMemoryLazyAppendBatch();", + source, + ) + + def test_next_batch_loads_only_near_panel_bottom(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "remaining <= RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX", + source, + ) + self.assertNotIn( + "runtimeMemoryFirstScrollBatchLoaded", + source, + ) + + def test_memory_view_cache_key_is_bumped_for_lazy_materialization(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("lazy-rows=2", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_memory_row_payload_client_contract.py b/tests/test_runtime_memory_row_payload_client_contract.py new file mode 100644 index 00000000..db3306cc --- /dev/null +++ b/tests/test_runtime_memory_row_payload_client_contract.py @@ -0,0 +1,102 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-memory-view.js" +) +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimeMemoryRowPayloadClientContractTests(unittest.TestCase): + + def test_rich_row_payload_is_kept_out_of_dom_datasets(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "const runtimeMemoryRowState = new WeakMap();", + source, + ) + self.assertIn( + "function setRuntimeMemoryRowState(row, values)", + source, + ) + self.assertNotIn( + "row.dataset.runtimeMemoryLineIdentity =", + source, + ) + self.assertNotIn( + "row.dataset.runtimeMemoryLineKey =", + source, + ) + self.assertNotIn( + "row.dataset.runtimeMemoryLineText =", + source, + ) + self.assertNotIn( + "row.dataset.runtimeMemoryValueDefaultText =", + source, + ) + self.assertNotIn( + "row.dataset.runtimeMemoryValueFullText =", + source, + ) + self.assertNotIn( + "[data-memory-reference-aliases]", + source, + ) + self.assertNotIn( + "[data-runtime-memory-line-key]", + source, + ) + + def test_hover_and_highlight_payload_still_uses_same_semantics(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "runtimeMemoryLineIdentity: lineIdentity", + source, + ) + self.assertIn( + "runtimeMemoryLineKey:", + source, + ) + self.assertIn( + "runtimeMemoryLineText:", + source, + ) + self.assertIn( + "runtimeMemoryValueDefaultText:", + source, + ) + self.assertIn( + "runtimeMemoryValueFullText:", + source, + ) + self.assertIn( + ").filter(row => getMemoryReferenceAliases(row).length);", + source, + ) + self.assertIn( + "getRuntimeMemoryRowCitationState(row)", + source, + ) + self.assertIn( + "bindRuntimeMemoryHoverTitle(\n row,\n hoverTitle", + source, + ) + + def test_memory_view_cache_key_is_bumped_for_row_payload_cleanup(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("row-payload=1", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_flow.py b/tests/test_search_flow.py index 363e33f3..e2ffeb2f 100644 --- a/tests/test_search_flow.py +++ b/tests/test_search_flow.py @@ -1,5 +1,7 @@ import unittest from typing import cast +from types import SimpleNamespace +from unittest.mock import patch import httpx @@ -27,8 +29,13 @@ WebSocketLogger, ) from rules.brain_context_builder import ( + build_brain_context, + get_enabled_runtime_actions, SERVICE_AS_BRAIN_RUNTIME_ACTIONS, ) +from app_settings import ( + is_valid_serper_api_key, +) class FakeWebSocket: @@ -364,6 +371,99 @@ def tearDown(self): self._original_service_deep_web_search ) + def test_serper_key_validation_accepts_any_configured_real_key(self): + + for value in ( + "7037beeecf37f7555e12ac2c06904634a283ff0", + "r/7037beeecf37f7555e12ac2c06904634a283ff0", + "serper-live-key-with-provider-defined-format", + ): + with self.subTest(value=value): + self.assertTrue( + is_valid_serper_api_key( + value + ) + ) + + for value in ( + "", + " ", + "mock-serper-api-key", + "your-serper-api-key", + "your_serper_api_key", + ): + with self.subTest(value=value): + self.assertFalse( + is_valid_serper_api_key( + value + ) + ) + + def test_configured_serper_key_keeps_search_actions_in_context(self): + + runtime_actions = { + "CAN_WEB_SEARCH": True, + "CAN_DEEP_WEB_SEARCH": True, + } + + with patch( + "rules.brain_context_builder.settings", + SimpleNamespace(CAN_SEARCH=True), + ): + prompt = build_brain_context( + runtime_actions=runtime_actions + ) + + self.assertEqual( + get_enabled_runtime_actions( + runtime_actions + ), + ( + "DEEP_WEB_SEARCH", + "WEB_SEARCH", + ), + ) + + self.assertIn( + "Use WEB_SEARCH", + prompt, + ) + self.assertIn( + "Use DEEP_WEB_SEARCH", + prompt, + ) + + def test_invalid_serper_key_hides_search_actions_from_context(self): + + runtime_actions = { + "CAN_WEB_SEARCH": True, + "CAN_DEEP_WEB_SEARCH": True, + } + + with patch( + "rules.brain_context_builder.settings", + SimpleNamespace(CAN_SEARCH=False), + ): + prompt = build_brain_context( + runtime_actions=runtime_actions + ) + + self.assertEqual( + get_enabled_runtime_actions( + runtime_actions + ), + (), + ) + + self.assertNotIn( + "Use WEB_SEARCH", + prompt, + ) + self.assertNotIn( + "Use DEEP_WEB_SEARCH", + prompt, + ) + def test_found_search_result_contains_results(self): result = normalize_search_results( diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index b24468c7..d05f5dc0 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -352,6 +352,27 @@ def test_stream_validator_stops_repeated_complex_symbolic_motif_in_mixed_reasoni assert validator.last_failure_loop_preview == "(😼) ⚡" +def test_stream_validator_allows_repeated_ascii_art_rows(): + validator = StreamValidator() + line = "| | | | | | | |\n" + + text = collect( + validator, + [ + line + for _ in range( + MAX_REPEAT_SYMBOLIC_MOTIFS + 4 + ) + ], + ) + + assert text == line * ( + MAX_REPEAT_SYMBOLIC_MOTIFS + 4 + ) + assert validator.last_failure_reason is None + assert validator.last_failure_loop_preview == "" + + def test_stream_validator_allows_bare_two_emoji_lines_even_when_repeated(): validator = StreamValidator() line = "😂 ⚡\n" diff --git a/tests/test_ui_fix_bundle_contract.py b/tests/test_ui_fix_bundle_contract.py new file mode 100644 index 00000000..89dcf43c --- /dev/null +++ b/tests/test_ui_fix_bundle_contract.py @@ -0,0 +1,68 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +SOCKET_JS = ROOT / "ui" / "static" / "js" / "socket.js" +INPUT_JS = ROOT / "ui" / "static" / "js" / "socket" / "input.js" +ANSWER_RATING_JS = ROOT / "ui" / "static" / "js" / "answer-rating.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class UiFixBundleContractTests(unittest.TestCase): + + def test_reasoning_uses_latest_manual_collapsed_state(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn("let jinThinkCollapsedPreference = true;", source) + self.assertIn("const initialThinkCollapsed =", source) + self.assertIn("jinThinkCollapsedPreference =", source) + self.assertIn("persist: true", source) + + def test_live_turn_releases_top_lock_and_autoscrolls_on_overflow(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn("function liveUserTurnReachedViewportBottom()", source) + self.assertIn("metrics.bottomSpace <= 1", source) + self.assertIn("if (liveUserTurnReachedViewportBottom()) {", source) + self.assertIn("releaseLiveUserTurnTopLock();", source) + self.assertIn("liveTurnOverflowAutoscroll =", source) + + def test_input_focus_and_form_padding_click(self): + socket_source = SOCKET_JS.read_text(encoding="utf-8") + input_source = INPUT_JS.read_text(encoding="utf-8") + + self.assertIn("function focusJinUserInput(", socket_source) + self.assertIn("if (!active) {", socket_source) + self.assertIn("focusJinUserInput({", socket_source) + self.assertIn("function focusChatInputFromFormPointer(", input_source) + self.assertIn('chatForm.addEventListener(\n "mousedown",', input_source) + + def test_rating_hover_is_directional_and_bubble_has_no_count_title(self): + source = ANSWER_RATING_JS.read_text(encoding="utf-8") + + self.assertIn('minus: "Dislike answer"', source) + self.assertIn('plus: "Like answer"', source) + self.assertIn("syncBubbleRatingZoneTitles(bubble);", source) + click_alt_block = source[ + source.index("function setBubbleRatingClickAlt"): + source.index("function clearBubbleRatingIntensity") + ] + self.assertIn('bubble.removeAttribute("title");', click_alt_block) + self.assertNotIn('bubble.setAttribute("title", label);', click_alt_block) + + def test_cache_busters_cover_updated_ui_files(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("&hover-labels=2", source) + self.assertIn( + "&reasoning-state=1&live-turn-overflow-scroll=2", + source, + ) + self.assertIn("&input-focus=2", source) + self.assertIn("&input-focus=3", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/js/answer-rating.js b/ui/static/js/answer-rating.js index 647a2453..45227876 100644 --- a/ui/static/js/answer-rating.js +++ b/ui/static/js/answer-rating.js @@ -17,6 +17,14 @@ const ratingVisualClasses = ratingSelectionClasses.filter( (className) => className !== "jin-rating-committed" ); + const ratingCountLabels = { + minus: "Dislikes", + plus: "Likes", + }; + const ratingHoverLabels = { + minus: "Dislike answer", + plus: "Like answer", + }; const ratingBubbleSelector = ".jin-chat-bubble-rateable, .jin-chat-bubble-service, .jin-chat-bubble-brain"; const activeRatingBubbleSelector = @@ -437,7 +445,57 @@ } } - function setBubbleRatingClickAlt(bubble, count) { + function getBubbleRatingCountKey(ratingValue) { + const value = String(ratingValue || ""); + if (!value) { + return ""; + } + + return `rating${value[0].toUpperCase()}${value.slice(1)}Count`; + } + + function getBubbleRatingValueCount(bubble, ratingValue) { + const key = getBubbleRatingCountKey(ratingValue); + if (!bubble || !key) { + return 0; + } + + const value = Number(bubble.dataset[key] || 0); + return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0; + } + + function formatRatingValueLabel(bubble, ratingValue) { + const count = getBubbleRatingValueCount(bubble, ratingValue); + const countLabel = ratingCountLabels[ratingValue]; + + if (countLabel && count > 0) { + return `${countLabel}: ${count}`; + } + + return ratingHoverLabels[ratingValue] || ""; + } + + function syncBubbleRatingZoneTitles(bubble) { + if (!bubble) { + return; + } + + const zones = bubble.querySelector(":scope > .jin-rating-hover-zones"); + if (!zones) { + return; + } + + Array.from(zones.children || []).forEach((zone) => { + const ratingValue = zone.dataset.ratingValue || ""; + const label = formatRatingValueLabel(bubble, ratingValue); + if (label) { + zone.title = label; + zone.setAttribute("aria-label", label); + } + }); + } + + function setBubbleRatingClickAlt(bubble, count, ratingValue = "") { if (!bubble) { return; } @@ -451,11 +509,14 @@ return; } - const label = String(Math.trunc(value)); + const ratingLabel = ratingCountLabels[ratingValue]; + const label = ratingLabel + ? `${ratingLabel}: ${Math.trunc(value)}` + : String(Math.trunc(value)); bubble.dataset.ratingClickAlt = label; bubble.setAttribute("alt", label); bubble.setAttribute("aria-label", label); - bubble.setAttribute("title", label); + bubble.removeAttribute("title"); } function clearBubbleRatingIntensity(bubble) { @@ -530,6 +591,7 @@ if (bubble.querySelector(":scope > .jin-rating-hover-zones")) { markBubbleRatingL1State(bubble); + syncBubbleRatingZoneTitles(bubble); return; } @@ -544,17 +606,16 @@ zones.setAttribute("aria-hidden", "true"); [ - ["jin-rating-zone jin-rating-zone-minus", "minus", "negative feedback hover zone"], + ["jin-rating-zone jin-rating-zone-minus", "minus", "Dislike answer"], ["jin-rating-zone jin-rating-zone-neutral", "disable", "disable rating"], - ["jin-rating-zone jin-rating-zone-plus", "plus", "positive feedback hover zone"], + ["jin-rating-zone jin-rating-zone-plus", "plus", "Like answer"], ].forEach(([className, ratingValue, label]) => { const zone = document.createElement("div"); zone.className = className; zone.dataset.ratingValue = ratingValue; zone.dataset.ratingHover = label; - if (ratingValue === "disable") { - zone.title = "disable rating"; - } + zone.title = label; + zone.setAttribute("aria-label", label); zone.addEventListener("click", (event) => { event.preventDefault(); @@ -606,7 +667,7 @@ window.jinAnswerRatingCounts = globalCounts; const bubbleClickCount = Number(bubble.dataset.ratingClickCount || 0) + 1; - const bubbleRatingCountKey = `rating${ratingValue[0].toUpperCase()}${ratingValue.slice(1)}Count`; + const bubbleRatingCountKey = getBubbleRatingCountKey(ratingValue); const previousRating = bubble.dataset.ratingSelected || null; const activeRatingClickCount = Number(bubble.dataset[bubbleRatingCountKey] || 0) + 1; @@ -632,8 +693,9 @@ bubble.classList.remove(pressClass); }, 680); - setBubbleRatingClickAlt(bubble, activeRatingClickCount); - zones.title = String(activeRatingClickCount); + setBubbleRatingClickAlt(bubble, activeRatingClickCount, ratingValue); + zones.title = ""; + syncBubbleRatingZoneTitles(bubble); const ratingDetail = { rating: ratingValue, diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 5befd643..14523f3e 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -2446,10 +2446,26 @@ function updateRuntimeActionRow( options ); - const detail = + const incomingDetail = String( options.detail || "" ).trim(); + const storedDetail = + String( + row.dataset.runtimeActionDetail || "" + ).trim(); + const detail = + incomingDetail + || ( + options.counterOnly === true + ? storedDetail + : "" + ); + + if (incomingDetail) { + row.dataset.runtimeActionDetail = + incomingDetail; + } if (detail) { label.title = detail; @@ -2465,6 +2481,7 @@ function updateRuntimeActionRow( "cursor-help" ); } else { + delete row.dataset.runtimeActionDetail; label.removeAttribute( "title" ); @@ -3071,6 +3088,8 @@ function appendRuntimeAction( ).trim(); if (detail) { + row.dataset.runtimeActionDetail = + detail; label.title = detail; row.title = detail; label diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index ec76f89d..ae5b09bb 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -23,6 +23,7 @@ const MEMORY_REFERENCE_HIGHLIGHT_EVENT = let liveUserTurnAnchor = null; let keepLiveUserTurnAtTop = false; +let jinThinkCollapsedPreference = true; function isChatRenderForeground() { @@ -452,6 +453,35 @@ function updateLiveUserTurnBottomSpace() { return; } + const metrics = + getLiveUserTurnViewportMetrics(); + + if (!metrics) { + chatHistory.style.removeProperty( + "--jin-live-turn-bottom-space" + ); + + return; + } + + chatHistory.style.setProperty( + "--jin-live-turn-bottom-space", + `${Math.ceil(metrics.bottomSpace)}px` + ); + +} + + +function getLiveUserTurnViewportMetrics() { + + if ( + !chatHistory + || !liveUserTurnAnchor + || !liveUserTurnAnchor.isConnected + ) { + return null; + } + const anchorRect = liveUserTurnAnchor.getBoundingClientRect(); @@ -501,9 +531,34 @@ function updateLiveUserTurnBottomSpace() { availableHeight - occupiedHeight ); - chatHistory.style.setProperty( - "--jin-live-turn-bottom-space", - `${Math.ceil(bottomSpace)}px` + return { + anchorRect, + bottomSpace, + overflow: + Math.max( + 0, + occupiedHeight - availableHeight + ), + }; + +} + + +function liveUserTurnReachedViewportBottom() { + + if ( + !keepLiveUserTurnAtTop + || !chatHistory + ) { + return false; + } + + const metrics = + getLiveUserTurnViewportMetrics(); + + return Boolean( + metrics + && metrics.bottomSpace <= 1 ); } @@ -519,13 +574,20 @@ function scrollLiveUserTurnToTop() { return; } + const metrics = + getLiveUserTurnViewportMetrics(); + + if (!metrics) { + return; + } + updateLiveUserTurnBottomSpace(); const historyRect = chatHistory.getBoundingClientRect(); const anchorRect = - liveUserTurnAnchor.getBoundingClientRect(); + metrics.anchorRect; const targetTop = chatHistory.scrollTop @@ -536,7 +598,7 @@ function scrollLiveUserTurnToTop() { chatHistory.scrollTop = Math.max( 0, - targetTop + targetTop + metrics.overflow ); } @@ -865,8 +927,22 @@ function flushStreamFrame() { updateLiveUserTurnBottomSpace(); - if ( - autoscroll + let liveTurnOverflowAutoscroll = + false; + + if (liveUserTurnReachedViewportBottom()) { + releaseLiveUserTurnTopLock(); + liveTurnOverflowAutoscroll = + true; + } + + if (keepLiveUserTurnAtTop) { + scrollLiveUserTurnToTop(); + } else if ( + ( + autoscroll + || liveTurnOverflowAutoscroll + ) && chatHistory ) { chatHistory.scrollTop = @@ -1830,8 +1906,15 @@ function createStreamGroup( const thinkContent = document.createElement("div"); + const initialThinkCollapsed = + Boolean( + jinThinkCollapsedPreference + ); + thinkContent.className = - "jin-think-content is-collapsed"; + initialThinkCollapsed + ? "jin-think-content is-collapsed" + : "jin-think-content"; thinkContent.setAttribute( "role", @@ -1845,7 +1928,9 @@ function createStreamGroup( thinkContent.setAttribute( "aria-expanded", - "false" + initialThinkCollapsed + ? "false" + : "true" ); thinkContent.setAttribute( @@ -1853,13 +1938,19 @@ function createStreamGroup( "Toggle thinking block" ); - let collapsed = true; + let collapsed = + initialThinkCollapsed; - const setCollapsed = (nextCollapsed) => { + const setCollapsed = (nextCollapsed, options = {}) => { collapsed = nextCollapsed; + if (options.persist === true) { + jinThinkCollapsedPreference = + collapsed; + } + thinkContent.classList.toggle( "is-collapsed", collapsed @@ -1944,7 +2035,10 @@ function createStreamGroup( } setCollapsed( - !collapsed + !collapsed, + { + persist: true, + } ); } ); @@ -1963,7 +2057,10 @@ function createStreamGroup( event.preventDefault(); setCollapsed( - !collapsed + !collapsed, + { + persist: true, + } ); } diff --git a/ui/static/js/header-autohide.js b/ui/static/js/header-autohide.js index 2ccff072..d31e9bf4 100644 --- a/ui/static/js/header-autohide.js +++ b/ui/static/js/header-autohide.js @@ -4,6 +4,7 @@ const header = document.getElementById("app-header"); const consolePanel = document.getElementById("console-panel"); const memoryPanel = document.getElementById("memory-panel"); + const chatHistory = document.getElementById("chat-history"); if (!header) { return; @@ -18,6 +19,13 @@ const HEADER_HEIGHT_VAR = "--app-header-height"; const PANEL_SHIFT_VAR = "--app-header-panel-shift"; const DEFAULT_PANEL_GAP = 8; + const CHAT_CONTENT_SELECTOR = [ + ".jin-chat-avatar", + ".jin-chat-bubble", + ".jin-think-content", + ".jin-runtime-action-row > *", + ".jin-session-restore-divider", + ].join(", "); const SHOW_DELAY_MS = 333; const HIDE_DELAY_MS = 1000; @@ -26,6 +34,7 @@ let lastPointerY = Number.POSITIVE_INFINITY; let interactionHeld = false; let pointerOccludedByPanel = false; + let pointerOccludedByChatContent = false; let visible = false; let panelSyncFrameId = null; let showTimerId = null; @@ -163,6 +172,7 @@ return interactionHeld || ( !pointerOccludedByPanel + && !pointerOccludedByChatContent && lastPointerY <= revealZoneHeight ); } @@ -237,6 +247,16 @@ ); } + function pointerIsOnChatContent(target) { + return Boolean( + chatHistory + && target + && typeof target.closest === "function" + && chatHistory.contains(target) + && target.closest(CHAT_CONTENT_SELECTOR) + ); + } + function pointerIsOnHeldSurface(target) { return Boolean( target @@ -255,6 +275,7 @@ fallbackY ); pointerOccludedByPanel = pointerIsOnOccludingPanel(target); + pointerOccludedByChatContent = pointerIsOnChatContent(target); } function handlePointerMove(event) { @@ -287,6 +308,7 @@ function handleDocumentLeave() { lastPointerY = Number.POSITIVE_INFINITY; pointerOccludedByPanel = false; + pointerOccludedByChatContent = false; interactionHeld = false; refreshVisibility(); } diff --git a/ui/static/js/logger/session-actions.js b/ui/static/js/logger/session-actions.js index 696fe61c..df85a10f 100644 --- a/ui/static/js/logger/session-actions.js +++ b/ui/static/js/logger/session-actions.js @@ -8,7 +8,6 @@ const sessionActionsLogState = { logDiv: null, tagSpan: null, list: null, - actions: null, fullButton: null, bottomMoveStreamKey: "", }; @@ -868,17 +867,17 @@ function ensureSessionActionsLog() { tagSpan.className = "text-zinc-300 font-bold logger-tag block"; - const list = + const header = document.createElement("div"); - list.className = - "mt-1 text-zinc-400 space-y-1"; + header.className = + "jin-attached-files-header"; - const actions = + const list = document.createElement("div"); - actions.className = - "mt-2 flex flex-wrap items-center gap-2 hidden"; + list.className = + "mt-1 text-zinc-400 space-y-1"; const fullButton = document.createElement("button"); @@ -887,30 +886,35 @@ function ensureSessionActionsLog() { "button"; fullButton.className = - "inline-flex items-center rounded border border-zinc-600/40 px-2 py-1 text-[10px] uppercase tracking-wider text-zinc-300 hover:bg-zinc-700/40 transition"; + "jin-attached-files-attach-button hidden"; fullButton.textContent = - "full"; + "FULL"; + + fullButton.setAttribute( + "aria-label", + "Show full session actions" + ); fullButton.addEventListener( "click", showSessionActionsModal ); - actions.appendChild( - fullButton + header.appendChild( + tagSpan ); - logDiv.appendChild( - tagSpan + header.appendChild( + fullButton ); logDiv.appendChild( - list + header ); logDiv.appendChild( - actions + list ); sessionActionsLogState.logDiv = @@ -922,9 +926,6 @@ function ensureSessionActionsLog() { sessionActionsLogState.list = list; - sessionActionsLogState.actions = - actions; - sessionActionsLogState.fullButton = fullButton; @@ -1004,7 +1005,6 @@ function updateSessionActionsLog( .slice( previewStartIndex ) - .reverse() .map(({ item, index }) => buildSessionActionRow( item, @@ -1013,7 +1013,7 @@ function updateSessionActionsLog( ) ); - sessionActionsLogState.actions.classList.toggle( + sessionActionsLogState.fullButton.classList.toggle( "hidden", items.length <= SESSION_ACTIONS_PREVIEW_LIMIT ); diff --git a/ui/static/js/runtime/runtime-memory-model.js b/ui/static/js/runtime/runtime-memory-model.js index 95019801..5d8def4f 100644 --- a/ui/static/js/runtime/runtime-memory-model.js +++ b/ui/static/js/runtime/runtime-memory-model.js @@ -2,7 +2,7 @@ window.JinRuntime = window.JinRuntime || {}; - const RUNTIME_MEMORY_VALUE_DISPLAY_MAX_CHARS = 100; + const RUNTIME_MEMORY_VALUE_DISPLAY_MAX_CHARS = 50; function splitCompoundRuntimeMemoryLine(line) { @@ -1067,7 +1067,7 @@ } - // Truncates long displayed JIN answers for the runtime memory panel, e.g. 120 characters -> first 100 characters plus "...". + // Truncates long displayed JIN answers for the runtime memory panel, e.g. 120 characters -> first 50 characters plus "...". function truncateJinResponseForDisplay(value) { return truncateRuntimeMemoryValueForDisplay( diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 64523dad..99aec936 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -46,11 +46,14 @@ : () => ""; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = "jin:memory-reference-highlight"; - const MEMORY_REFERENCE_ALIAS_DATASET_KEY = + const MEMORY_REFERENCE_ALIAS_STATE_KEY = "memoryReferenceAliases"; const memoryReferenceHighlightState = { persistentText: "", }; + // Rich row payload stays in JS, not serialized into data-* attributes. + // WeakMap lets detached/lazy-unloaded rows release their metadata naturally. + const runtimeMemoryRowState = new WeakMap(); const activeThinkMemoryCitationSources = new Map(); let memoryReferenceEventsBound = false; let runtimeMemorySortTransitionSequence = 0; @@ -125,13 +128,14 @@ ? memoryPanel.querySelector(".memory-scroll") : null; - const RUNTIME_MEMORY_LAZY_BATCH_SIZE = 20; + const RUNTIME_MEMORY_LAZY_BATCH_SIZE = 40; const RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX = 160; let runtimeMemoryLazyMode = ""; - let runtimeMemoryVisibleRowCount = RUNTIME_MEMORY_LAZY_BATCH_SIZE; + let runtimeMemoryLazyTotalCount = 0; + let runtimeMemoryLazyRenderedCount = 0; + let runtimeMemoryLazyAppendBatch = null; let runtimeMemoryLastScrollTop = 0; - let runtimeMemoryFirstScrollBatchLoaded = false; let runtimeMemoryLastBatchRevealAt = 0; const MEMORY_PANEL_COLLAPSE_SYNC_EVENT = @@ -370,46 +374,67 @@ collectMetadataAliases: collectMemoryMetadataReferenceAliases, }); - function setMemoryReferenceAliases(row, aliases) { + function getRuntimeMemoryRowState(row, create = false) { if (!row) { + return null; + } + + let state = runtimeMemoryRowState.get(row) || null; + + if (!state && create) { + state = Object.create(null); + runtimeMemoryRowState.set(row, state); + } + + return state; + } + + function setRuntimeMemoryRowState(row, values) { + if (!row || !values || typeof values !== "object") { return; } - const normalizedAliases = - normalizeMemoryReferenceAliases(aliases); + Object.assign( + getRuntimeMemoryRowState(row, true), + values + ); + } - if (!normalizedAliases.length) { - delete row.dataset[ - MEMORY_REFERENCE_ALIAS_DATASET_KEY - ]; + function setMemoryReferenceAliases(row, aliases) { + if (!row) { return; } - row.dataset[ - MEMORY_REFERENCE_ALIAS_DATASET_KEY - ] = JSON.stringify(normalizedAliases); + setRuntimeMemoryRowState( + row, + { + [MEMORY_REFERENCE_ALIAS_STATE_KEY]: + normalizeMemoryReferenceAliases(aliases), + } + ); } function getMemoryReferenceAliases(row) { - if (!row || !row.dataset) { - return []; - } + const state = getRuntimeMemoryRowState(row); + const aliases = + state && state[MEMORY_REFERENCE_ALIAS_STATE_KEY]; - const raw = row.dataset[ - MEMORY_REFERENCE_ALIAS_DATASET_KEY - ]; + return Array.isArray(aliases) + ? aliases + : []; + } - if (!raw) { - return []; - } + function getRuntimeMemoryRowCitationState(row) { + const state = getRuntimeMemoryRowState(row); - try { - return normalizeMemoryReferenceAliases( - JSON.parse(raw) - ); - } catch (error) { - return []; - } + return { + lineIdentity: + String(state && state.runtimeMemoryLineIdentity || ""), + lineKey: + String(state && state.runtimeMemoryLineKey || ""), + lineText: + String(state && state.runtimeMemoryLineText || ""), + }; } function getActiveMemoryReferenceText() { @@ -439,6 +464,53 @@ ); } + function isLongTermMemoryRowBubbled(row) { + return Boolean( + row + && row.classList.contains("runtime-memory-l4-row") + && ( + row.classList.contains("runtime-memory-reference-hit") + || row.classList.contains("runtime-memory-citation-hit") + || row.classList.contains("runtime-memory-context-loaded-hit") + ) + ); + } + + function syncLongTermMemoryRowValueDisplay(row) { + if (!row || !row.classList.contains("runtime-memory-l4-row")) { + return; + } + + const valueSpan = + row.querySelector(".runtime-memory-value"); + const valueTextNode = + valueSpan + ? Array.from(valueSpan.childNodes) + .find(node => node.nodeType === 3) + : null; + + if (!valueTextNode) { + return; + } + + const state = + getRuntimeMemoryRowState(row); + const defaultText = + String(state && state.runtimeMemoryValueDefaultText || ""); + const fullText = + String( + state && state.runtimeMemoryValueFullText + || defaultText + ); + + valueTextNode.nodeValue = + ` ${ + isLongTermMemoryRowBubbled(row) + ? fullText + : defaultText + }`; + } + function clearRuntimeMemoryHighlightClasses() { if (!runtimeMemoryText || !runtimeMemoryText.isConnected) { return; @@ -458,6 +530,7 @@ "runtime-memory-citation-hit", "runtime-memory-external-hover-hit" ); + syncLongTermMemoryRowValueDisplay(row); }); } @@ -484,33 +557,50 @@ ] || null; } - function getRuntimeMemoryLazyRows() { - if (!runtimeMemoryText) { - return []; - } + function applyRuntimeMemoryLazyVisibility() { + // Rows are materialized in batches now; there are no hidden overflow + // rows sitting in the DOM to toggle. Keep this hook for the existing + // highlight/sort pipeline, which still calls it after reordering. + } - return Array.from( - runtimeMemoryText.querySelectorAll( - ".runtime-memory-line:not(.runtime-memory-user-idle)" - ) - ); + function clearRuntimeMemoryLazyCollection() { + runtimeMemoryLazyTotalCount = 0; + runtimeMemoryLazyRenderedCount = 0; + runtimeMemoryLazyAppendBatch = null; } - function applyRuntimeMemoryLazyVisibility() { - const rows = getRuntimeMemoryLazyRows(); + function beginRuntimeMemoryLazyCollection(items, renderItem) { + const source = Array.isArray(items) ? items : []; - rows.forEach((row, index) => { - row.style.display = - index < runtimeMemoryVisibleRowCount - ? "" - : "none"; - }); + clearRuntimeMemoryLazyCollection(); + runtimeMemoryLazyTotalCount = source.length; + + const appendBatch = () => { + const start = runtimeMemoryLazyRenderedCount; + const end = Math.min( + runtimeMemoryLazyTotalCount, + start + RUNTIME_MEMORY_LAZY_BATCH_SIZE + ); + + for (let index = start; index < end; index += 1) { + renderItem(source[index], index); + } + + runtimeMemoryLazyRenderedCount = end; + + if (runtimeMemoryLazyRenderedCount >= runtimeMemoryLazyTotalCount) { + runtimeMemoryLazyAppendBatch = null; + } + + return end > start; + }; + + runtimeMemoryLazyAppendBatch = appendBatch; + appendBatch(); } function resetRuntimeMemoryLazyRows(options = {}) { - runtimeMemoryVisibleRowCount = - RUNTIME_MEMORY_LAZY_BATCH_SIZE; - runtimeMemoryFirstScrollBatchLoaded = false; + clearRuntimeMemoryLazyCollection(); runtimeMemoryLastScrollTop = 0; runtimeMemoryLastBatchRevealAt = 0; @@ -521,8 +611,6 @@ if (memoryScroll) { memoryScroll.scrollTop = 0; } - - applyRuntimeMemoryLazyVisibility(); } function syncRuntimeMemoryLazyMode(displayMode) { @@ -539,18 +627,20 @@ } function revealNextRuntimeMemoryLazyBatch() { - const rows = getRuntimeMemoryLazyRows(); + if (typeof runtimeMemoryLazyAppendBatch !== "function") { + return false; + } - if (runtimeMemoryVisibleRowCount >= rows.length) { + const appended = runtimeMemoryLazyAppendBatch(); + + if (!appended) { return false; } - runtimeMemoryVisibleRowCount = Math.min( - rows.length, - runtimeMemoryVisibleRowCount + RUNTIME_MEMORY_LAZY_BATCH_SIZE - ); runtimeMemoryLastBatchRevealAt = Date.now(); - applyRuntimeMemoryLazyVisibility(); + applyMemoryReferenceHighlights({ + animateSort: false, + }); return true; } @@ -570,12 +660,6 @@ return; } - if (!runtimeMemoryFirstScrollBatchLoaded) { - runtimeMemoryFirstScrollBatchLoaded = true; - revealNextRuntimeMemoryLazyBatch(); - return; - } - const remaining = memoryScroll.scrollHeight - memoryScroll.clientHeight @@ -591,16 +675,11 @@ !memoryScroll || isRuntimeMemoryViewSuspended() || Number(event && event.deltaY || 0) <= 0 + || Date.now() - runtimeMemoryLastBatchRevealAt < 80 ) { return; } - if (!runtimeMemoryFirstScrollBatchLoaded) { - runtimeMemoryFirstScrollBatchLoaded = true; - revealNextRuntimeMemoryLazyBatch(); - return; - } - const remaining = memoryScroll.scrollHeight - memoryScroll.clientHeight @@ -610,7 +689,6 @@ revealNextRuntimeMemoryLazyBatch(); } } - function bindRuntimeMemoryLazyScroll() { if (!memoryScroll || memoryScroll.dataset.lazyMemoryBound === "1") { return; @@ -720,9 +798,9 @@ getActiveMemoryReferenceText(); const rows = Array.from( runtimeMemoryText.querySelectorAll( - `[data-memory-reference-aliases]` + ".runtime-memory-line:not(.runtime-memory-user-idle)" ) - ); + ).filter(row => getMemoryReferenceAliases(row).length); const aliasUsage = buildMemoryReferenceAliasUsage(rows); @@ -925,6 +1003,10 @@ ) ); + rows.forEach( + syncLongTermMemoryRowValueDisplay + ); + if (rows.length < 2) { applyRuntimeMemoryLazyVisibility(); return; @@ -1032,16 +1114,24 @@ const citationRows = Array.from( runtimeMemoryText.querySelectorAll( - "[data-runtime-memory-line-key]" + ".runtime-memory-line:not(.runtime-memory-user-idle)" ) - ); + ).filter((row) => { + const citationState = + getRuntimeMemoryRowCitationState(row); + + return Boolean( + citationState.lineIdentity + || citationState.lineKey + || citationState.lineText + || normalizeActiveMemoryId(row.dataset.activeMemoryId) + ); + }); const lineKeyUsage = new Map(); citationRows.forEach((row) => { - const lineKey = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineKey - ); + const { lineKey } = + getRuntimeMemoryRowCitationState(row); if (!lineKey) { return; @@ -1054,22 +1144,12 @@ }); citationRows.forEach((row) => { - const lineIdentity = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineIdentity - ); + const { lineIdentity, lineKey, lineText } = + getRuntimeMemoryRowCitationState(row); const activeMemoryId = normalizeActiveMemoryId( row.dataset.activeMemoryId ); - const lineKey = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineKey - ); - const lineText = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineText - ); const exactTextMatch = Boolean( lineText && activeIdentities.lineTexts.has(lineText) @@ -1950,6 +2030,53 @@ .join("\n"); } + + const runtimeMemoryHoverTitleSources = new WeakMap(); + const runtimeMemoryHoverTitleBoundNodes = new WeakSet(); + + function resolveRuntimeMemoryHoverTitle(node) { + if (!node) { + return ""; + } + + const source = runtimeMemoryHoverTitleSources.get(node); + const value = + typeof source === "function" + ? source() + : source; + + return String(value || "").trim(); + } + + function bindRuntimeMemoryHoverTitle(node, source) { + if (!node) { + return; + } + + runtimeMemoryHoverTitleSources.set(node, source); + node.removeAttribute("title"); + + if (runtimeMemoryHoverTitleBoundNodes.has(node)) { + return; + } + + runtimeMemoryHoverTitleBoundNodes.add(node); + + node.addEventListener("mouseenter", () => { + const title = resolveRuntimeMemoryHoverTitle(node); + + if (title) { + node.setAttribute("title", title); + } else { + node.removeAttribute("title"); + } + }); + + node.addEventListener("mouseleave", () => { + node.removeAttribute("title"); + }); + } + function setRuntimeDiffUpdate(data) { runtimeDiffHistory.diffs = data && data.diffs || []; @@ -2122,8 +2249,10 @@ const tokenCount = estimateRuntimeMemoryTokens(metricText); - runtimeMemoryTitle.title = - `${charCount} chars / ~${tokenCount} tokens`; + bindRuntimeMemoryHoverTitle( + runtimeMemoryTitle, + `${charCount} chars / ~${tokenCount} tokens` + ); } function updateRuntimeMemoryTitleMetricsFromText(text) { @@ -2140,8 +2269,10 @@ const tokenCount = estimateRuntimeMemoryTokens(metricText); - runtimeMemoryTitle.title = - `${charCount} chars / ~${tokenCount} tokens`; + bindRuntimeMemoryHoverTitle( + runtimeMemoryTitle, + `${charCount} chars / ~${tokenCount} tokens` + ); } function clampRuntimeMemoryHistoryIndex() { @@ -2810,8 +2941,10 @@ `${memoryModel.stripMemoryTextMetaForDisplay(rawMemory).trim()}\n`; if (rawMemory.trim()) { - runtimeMemoryText.title = - formatRuntimeMemoryHoverTitle(rawMemory); + bindRuntimeMemoryHoverTitle( + runtimeMemoryText, + () => formatRuntimeMemoryHoverTitle(rawMemory) + ); } if (showLiveUserIdle) { @@ -2848,7 +2981,7 @@ persistGlow = false, options = {} ) { - lines.forEach((line, index) => { + beginRuntimeMemoryLazyCollection(lines, (line, index) => { const row = document.createElement("div"); @@ -2872,11 +3005,6 @@ normalizeRuntimeCitationIdentity( line.citation_identity ); - - if (lineIdentity) { - row.dataset.runtimeMemoryLineIdentity = - lineIdentity; - } const activeMemoryId = normalizeActiveMemoryId( line && line.active_memory_id @@ -2886,14 +3014,21 @@ row.dataset.activeMemoryId = activeMemoryId; } - row.dataset.runtimeMemoryLineKey = - normalizeRuntimeCitationIdentity( - line.key || "note" - ); - row.dataset.runtimeMemoryLineText = - normalizeRuntimeCitationIdentity( - `${line.key || "note"}: ${line.value || ""}` - ); + + setRuntimeMemoryRowState( + row, + { + runtimeMemoryLineIdentity: lineIdentity, + runtimeMemoryLineKey: + normalizeRuntimeCitationIdentity( + line.key || "note" + ), + runtimeMemoryLineText: + normalizeRuntimeCitationIdentity( + `${line.key || "note"}: ${line.value || ""}` + ), + } + ); if (line && line.context_loaded === true) { row.classList.add( @@ -2998,10 +3133,12 @@ numberSpan.classList.add( "runtime-memory-fact-report-link" ); - numberSpan.title = - reportTitle - ? `Open delayed memory report: ${reportTitle}` - : "Open delayed memory report"; + bindRuntimeMemoryHoverTitle( + numberSpan, + reportTitle + ? `Open delayed memory report: ${reportTitle}` + : "Open delayed memory report" + ); numberSpan.addEventListener("pointerdown", (event) => { event.stopPropagation(); }); @@ -3032,6 +3169,23 @@ valueSpan.textContent = ` ${valuePresentation.text}`; + + if (options.interactiveLongTermMemory) { + setRuntimeMemoryRowState( + row, + { + runtimeMemoryValueDefaultText: + String(valuePresentation.text || ""), + runtimeMemoryValueFullText: + String( + memoryModel.splitMemoryMeta(line.value || "").text + || valuePresentation.text + || "" + ).replace(/\\n/g, " ↵ "), + } + ); + } + valueSpan.style.fontWeight = String( runtimeMemoryValueFontWeight(line) @@ -3062,14 +3216,22 @@ const hoverTitle = formatRuntimeMemoryHoverTitle(fullRawLine); - row.title = - hoverTitle; - valueSpan.title = - hoverTitle; + bindRuntimeMemoryHoverTitle( + row, + hoverTitle + ); + bindRuntimeMemoryHoverTitle( + valueSpan, + hoverTitle + ); row.appendChild(keySpan); row.appendChild(valueSpan); + if (options.interactiveLongTermMemory) { + syncLongTermMemoryRowValueDisplay(row); + } + if (options.interactiveActiveMemory) { configureActiveMemoryRow( row, @@ -3725,7 +3887,7 @@ runtimeMemoryPosition.textContent = String(records.length); } - records.forEach((record, index) => { + beginRuntimeMemoryLazyCollection(records, (record, index) => { const row = document.createElement("div"); const fileId = String(record.id || "").trim().toLowerCase(); const linkedState = linkedStateByFileId.get(fileId) || null; @@ -3741,16 +3903,28 @@ row.dataset.memoryHighlightSortIndex = String(index); row.setAttribute("role", "button"); row.setAttribute("tabindex", "0"); - row.title = String(record.name || "attachment"); + bindRuntimeMemoryHoverTitle( + row, + String(record.name || "attachment") + ); row.dataset.fileId = fileId; if (avatarMemoryHoverId) { row.dataset.avatarMemoryHoverId = avatarMemoryHoverId; } - if (fileId) { - row.dataset.runtimeMemoryLineKey = normalizeRuntimeCitationIdentity(fileId); - } - row.dataset.runtimeMemoryLineText = normalizeRuntimeCitationIdentity( - [record.name, record.stored_name, record.context_path].filter(Boolean).join(" · ") + setRuntimeMemoryRowState( + row, + { + runtimeMemoryLineKey: + fileId + ? normalizeRuntimeCitationIdentity(fileId) + : "", + runtimeMemoryLineText: + normalizeRuntimeCitationIdentity( + [record.name, record.stored_name, record.context_path] + .filter(Boolean) + .join(" · ") + ), + } ); setMemoryReferenceAliases( row, @@ -3772,7 +3946,10 @@ pinButton.innerHTML = ''; pinButton.classList.toggle("delayed-memory-modal-pin-active", Boolean(record.pinned)); pinButton.setAttribute("aria-pressed", record.pinned ? "true" : "false"); - pinButton.title = String(record.id || ""); + bindRuntimeMemoryHoverTitle( + pinButton, + String(record.id || "") + ); pinButton.setAttribute( "aria-label", record.pinned @@ -3795,7 +3972,10 @@ const separator = document.createElement("span"); separator.className = "runtime-memory-delayed-separator"; separator.textContent = "·"; - separator.title = pinButton.title; + bindRuntimeMemoryHoverTitle( + separator, + () => resolveRuntimeMemoryHoverTitle(pinButton) + ); separator.addEventListener("pointerdown", (event) => event.stopPropagation()); separator.addEventListener("click", togglePinnedFile); @@ -3860,7 +4040,7 @@ "title" ); - reports.forEach((report, index) => { + beginRuntimeMemoryLazyCollection(reports, (report, index) => { const title = String(report.title || "").trim(); @@ -3941,10 +4121,17 @@ valueSpan.textContent = ` ${summary}`; - row.title = + const hoverTitle = `${title}: ${summary}`.trim(); - valueSpan.title = - row.title; + + bindRuntimeMemoryHoverTitle( + row, + hoverTitle + ); + bindRuntimeMemoryHoverTitle( + valueSpan, + hoverTitle + ); row.dataset.delayedMemoryId = normalizeRuntimeCitationIdentity( reportId || report._storage_key @@ -4018,7 +4205,10 @@ "click", toggleDelayedMemoryPinned ); - separatorSpan.title = pinButton.title; + bindRuntimeMemoryHoverTitle( + separatorSpan, + () => resolveRuntimeMemoryHoverTitle(pinButton) + ); separatorSpan.addEventListener( "pointerdown", (event) => event.stopPropagation() @@ -4693,8 +4883,10 @@ "button"; optionButton.className = "delayed-memory-modal-fact-option"; - optionButton.title = - option.title; + bindRuntimeMemoryHoverTitle( + optionButton, + option.title + ); id.className = "delayed-memory-modal-fact-option-id"; @@ -5184,14 +5376,16 @@ : "Pin delayed memory" ) ); - button.title = - loaded - ? "Unload delayed memory from context" - : ( - pinned - ? "Unpin delayed memory" - : "Pin delayed memory" - ); + bindRuntimeMemoryHoverTitle( + button, + loaded + ? "Unload delayed memory from context" + : ( + pinned + ? "Unpin delayed memory" + : "Pin delayed memory" + ) + ); } function clearDelayedMemoryModalEditSaveTimer() { @@ -5526,8 +5720,10 @@ "Delete delayed memory" ); - delayedMemoryModalDeleteButton.title = - "Hold to delete delayed memory"; + bindRuntimeMemoryHoverTitle( + delayedMemoryModalDeleteButton, + "Hold to delete delayed memory" + ); delayedMemoryModalDeleteButton.innerHTML = ''; @@ -5799,8 +5995,10 @@ sessionId.length > 9 ? `${sessionId.slice(0, 9)}...` : sessionId; - item.title = - sessionId; + bindRuntimeMemoryHoverTitle( + item, + sessionId + ); item.setAttribute( "aria-label", `restore session ${sessionId}` @@ -5937,7 +6135,10 @@ item.className = "delayed-memory-modal-tag"; item.textContent = tag; - item.title = "Hold to remove tag"; + bindRuntimeMemoryHoverTitle( + item, + "Hold to remove tag" + ); item.setAttribute("tabindex", "0"); configureRuntimeMemoryDeleteHold( @@ -6133,8 +6334,10 @@ ); item.textContent = factId; - item.title = - title; + bindRuntimeMemoryHoverTitle( + item, + title + ); item.dataset.delayedMemoryFactId = factId; item.setAttribute( @@ -6513,8 +6716,10 @@ optionButton.type = "button"; optionButton.className = "delayed-memory-modal-fact-option delayed-memory-modal-attachment-option"; - optionButton.title = - `${option.record.name || "attachment"} · ${option.fileId}`; + bindRuntimeMemoryHoverTitle( + optionButton, + `${option.record.name || "attachment"} · ${option.fileId}` + ); id.className = "delayed-memory-modal-fact-option-id"; @@ -6704,9 +6909,12 @@ } else { item.textContent = fileId; } - item.title = record - ? `${record.name || "attachment"} · ${fileId}` - : `${fileId} · missing file`; + bindRuntimeMemoryHoverTitle( + item, + record + ? `${record.name || "attachment"} · ${fileId}` + : `${fileId} · missing file` + ); item.dataset.delayedMemoryAttachmentId = fileId; item.setAttribute("tabindex", "0"); @@ -6810,8 +7018,10 @@ body.className = "jin-context-card-body delayed-memory-modal-card-body"; - header.title = - "Click to collapse / expand"; + bindRuntimeMemoryHoverTitle( + header, + "Click to collapse / expand" + ); header.tabIndex = 0; header.setAttribute("role", "button"); header.setAttribute("aria-expanded", "true"); diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 1a7cc420..64c64372 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -592,6 +592,52 @@ return true; } + function clearPersistedToolResultsCheckpoint() { + if ( + (typeof shouldIsolateAnonymousStorage === "function" + && shouldIsolateAnonymousStorage()) + || (typeof isAnonymousModeEnabled === "function" + && isAnonymousModeEnabled()) + ) { + return false; + } + + const previousCheckpoint = + readLatestSavedSessionSnapshot(); + + if ( + !previousCheckpoint + || typeof previousCheckpoint !== "object" + || Array.isArray(previousCheckpoint) + ) { + return false; + } + + const previousSessionSnapshot = + ( + previousCheckpoint.session_snapshot + && typeof previousCheckpoint.session_snapshot === "object" + && !Array.isArray(previousCheckpoint.session_snapshot) + ) + ? previousCheckpoint.session_snapshot + : {}; + + // CLEAN_TOOL_RESULTS mutates only one bootstrap field. Preserve the + // checkpoint timestamp/lineage verbatim: advancing saved_at here makes + // the browser checkpoint look newer than the raw chat-log tail, which + // suppresses archive enrichment for dialogue/reasoning/actions/files. + writeLatestSavedSessionSnapshot({ + ...previousCheckpoint, + session_snapshot: { + ...previousSessionSnapshot, + tool_results: [], + }, + }); + + return true; + } + + function buildCheckpointRuntimeSnapshot(snapshot) { const persistedSnapshot = buildPersistedRuntimeSnapshot( @@ -1584,6 +1630,7 @@ } session.persistLiveSessionCheckpoint = persistLiveSessionCheckpoint; + session.clearPersistedToolResultsCheckpoint = clearPersistedToolResultsCheckpoint; session.getRuntimeMemoryForSoftReconnect = getRuntimeMemoryForSoftReconnect; session.getInitialRuntimeMemoryBootstrap = getInitialRuntimeMemoryBootstrap; session.isReconnectInitialRuntimeMemoryUpdate = isReconnectInitialRuntimeMemoryUpdate; diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index b460f329..6c739a76 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -115,6 +115,33 @@ window.isJinGenerationRunning = function () { }; +function focusJinUserInput( + options = {} +) { + + if ( + !userInput + || generationRunning + || document.visibilityState === "hidden" + ) { + return false; + } + + try { + userInput.focus({ + preventScroll: options.preventScroll !== false, + }); + } catch (error) { + userInput.focus(); + } + + return true; + +} + +window.focusJinUserInput = + focusJinUserInput; + function isWebSocketOpen() { return ( ws @@ -348,6 +375,16 @@ function setGenerationState( ); } + if (!active) { + requestAnimationFrame( + () => { + focusJinUserInput({ + preventScroll: true, + }); + } + ); + } + if (!sendButton) { return; } diff --git a/ui/static/js/socket/input.js b/ui/static/js/socket/input.js index 19d4ca16..4134dcb9 100644 --- a/ui/static/js/socket/input.js +++ b/ui/static/js/socket/input.js @@ -101,6 +101,10 @@ chatForm.addEventListener( function (e) { if (!generationRunning) { + focusChatInputFromFormPointer( + e + ); + return; } @@ -111,6 +115,64 @@ chatForm.addEventListener( } ); +function shouldFocusChatInputFromFormPointer( + e +) { + + if ( + generationRunning + || e.button !== 0 + ) { + return false; + } + + if ( + e.target + && e.target.closest + && e.target.closest( + "button, label, input, textarea, select, a, [role='button']" + ) + ) { + return false; + } + + return true; + +} + +function focusChatInputFromFormPointer( + e +) { + + if ( + !shouldFocusChatInputFromFormPointer( + e + ) + ) { + return false; + } + + e.preventDefault(); + + if (window.focusJinUserInput) { + window.focusJinUserInput({ + preventScroll: true, + }); + } else { + userInput.focus({ + preventScroll: true, + }); + } + + return true; + +} + +chatForm.addEventListener( + "mousedown", + focusChatInputFromFormPointer +); + // HIDDEN BUTTON CLICK // -------------------------------------------------- diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 62d560e4..1a21537c 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -2141,6 +2141,16 @@ function handleRuntimeAction( || status === "complete" || status === "done" ) { + if ( + action === "clean_tool_results" + && window.JinRuntime + && window.JinRuntime.session + && typeof window.JinRuntime.session.clearPersistedToolResultsCheckpoint + === "function" + ) { + window.JinRuntime.session.clearPersistedToolResultsCheckpoint(); + } + if (displayText.trim()) { const appended = appendRuntimeAction( action, diff --git a/ui/static/js/win95-theme.js b/ui/static/js/win95-theme.js index d8e818e8..f4b404e4 100644 --- a/ui/static/js/win95-theme.js +++ b/ui/static/js/win95-theme.js @@ -3,9 +3,29 @@ const themeClass = "theme-win95"; const titleButton = document.getElementById("app-title"); + function readStoredTheme() { + try { + return window.localStorage + ? window.localStorage.getItem(themeKey) + : null; + } catch (error) { + return null; + } + } + + function writeStoredTheme(enabled) { + try { + if (window.localStorage) { + window.localStorage.setItem(themeKey, enabled ? "1" : "0"); + } + } catch (error) { + // Theme switching should still work in restricted browser contexts. + } + } + function applyWin95Theme(enabled) { document.body.classList.toggle(themeClass, enabled); - localStorage.setItem(themeKey, enabled ? "1" : "0"); + writeStoredTheme(enabled); if ( window.JinPanels @@ -17,7 +37,7 @@ } } - applyWin95Theme(localStorage.getItem(themeKey) === "1"); + applyWin95Theme(readStoredTheme() === "1"); if (titleButton) { titleButton.addEventListener("click", function () { diff --git a/ui/templates/index.html b/ui/templates/index.html index 98f8254f..70b32726 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -12,7 +12,7 @@ - + @@ -484,12 +484,12 @@ - + - + @@ -513,31 +513,31 @@ - + - + - + - + - + - + diff --git a/utils/actions/active_memory_utils.py b/utils/actions/active_memory_utils.py index 2feebff5..81173e7c 100644 --- a/utils/actions/active_memory_utils.py +++ b/utils/actions/active_memory_utils.py @@ -81,10 +81,6 @@ re.IGNORECASE, ) -ACTIVE_MEMORY_TRAILING_CUSTOM_FIELD_RE = re.compile( - r"\(\s*([A-Za-z][A-Za-z0-9_]{0,31})\s*:\s*([^()]*)\)\s*$", -) - ACTIVE_MEMORY_CUSTOM_FIELD_LIMIT = 3 ACTIVE_MEMORY_CUSTOM_FIELD_VALUE_MAX_LENGTH = 256 @@ -188,42 +184,7 @@ def extract_active_memory_creation_custom_fields( return conditions, custom_fields - reversed_fields = [] - seen = set() - - while True: - match = ACTIVE_MEMORY_TRAILING_CUSTOM_FIELD_RE.search( - text - ) - if match is None: - break - - field_name = normalize_active_memory_custom_field_name( - match.group(1) - ) - field_value = normalize_active_memory_custom_field_value( - match.group(2) - ) - - # Parentheses remain ordinary condition text unless both the key and - # value form a valid custom state declaration. This keeps prose such as - # "(inside the apartment or view from the window)" untouched. - if not field_name or not field_value: - break - - if field_name in seen: - return str(value or "").strip(), () - - seen.add(field_name) - reversed_fields.append((field_name, field_value)) - text = text[:match.start()].rstrip() - - custom_fields = tuple(reversed(reversed_fields)) - - if len(custom_fields) > ACTIVE_MEMORY_CUSTOM_FIELD_LIMIT: - return str(value or "").strip(), () - - return text.strip(), custom_fields + return text.strip(), () def collect_active_memory_custom_fields( diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index bc3ea141..4a5d8867 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -123,6 +123,21 @@ def format_runtime_trigger_words_message( RUNTIME_ACTION_JIN_SPEED, }) +UPDATE_ACTIVE_MEMORY_START_RE = re.compile( + r"<\s*UPDATE_ACTIVE_MEMORY(?=\s|>)", + re.IGNORECASE, +) + +UPDATE_ACTIVE_MEMORY_SELF_CLOSING_ATTRIBUTE_RE = re.compile( + ( + r"<\s*(?PUPDATE_ACTIVE_MEMORY)" + r"\s+" + r"(?P(?:[^<>\"']+|\"[^\"]*\"|'[^']*')*?)" + r"\s*/\s*>" + ), + re.IGNORECASE | re.DOTALL, +) + def _runtime_action_allows_inline_payload( action_name: str, @@ -192,6 +207,23 @@ def _find_all_runtime_action_matches( if match.payload.strip() ) + if action_name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY: + action_matches = ( + *_find_update_active_memory_attribute_matches( + text + ), + *action_matches, + ) + + if action_name == RUNTIME_ACTION_CLEAN_TOOL_RESULTS: + action_matches = ( + *action_matches, + *_find_clean_tool_results_closing_matches( + text, + private_marker, + ), + ) + matches.extend( action_matches ) @@ -201,6 +233,72 @@ def _find_all_runtime_action_matches( ) +def _find_clean_tool_results_closing_matches( + text: str, + private_marker: str, +) -> tuple[RuntimeActionRegexpMatch, ...]: + """Treat a redundant CLEAN_TOOL_RESULTS close tag as parser-only noise.""" + + matches = [] + end_pattern = compile_runtime_action_end_regexp( + private_marker, + RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + ) + + for match in end_pattern.finditer( + str( + text + or "" + ) + ): + matches.append( + RuntimeActionRegexpMatch( + start=match.start(), + end=match.end(), + raw=match.group(0), + name=RUNTIME_ACTION_CLEAN_TOOL_RESULTS, + source="compat_closing", + ) + ) + + return tuple( + matches + ) + + +def _find_update_active_memory_attribute_matches( + text: str, +) -> tuple[RuntimeActionRegexpMatch, ...]: + + matches = [] + + for match in UPDATE_ACTIVE_MEMORY_SELF_CLOSING_ATTRIBUTE_RE.finditer( + str( + text + or "" + ) + ): + matches.append( + RuntimeActionRegexpMatch( + start=match.start(), + end=match.end(), + raw=match.group(0), + name=str( + match.group("name") + or "" + ).strip().upper(), + payload=str( + match.group("payload") + or "" + ).strip(), + ) + ) + + return tuple( + matches + ) + + def _find_deep_web_search_block_matches( text: str, private_marker: str, @@ -1173,6 +1271,18 @@ def replace_runtime_action_marker( match: RuntimeActionRegexpMatch, ) -> str: + if match.source == "compat_closing": + if not preserve_action_text: + removed_markers.append( + match.raw + ) + + return ( + match.raw + if preserve_action_text + else "" + ) + return handle_marker( match.raw, match.name, @@ -1226,6 +1336,17 @@ def _enabled_action_start_markers( marker ) + if action_name == RUNTIME_ACTION_CLEAN_TOOL_RESULTS: + marker_name, _ = extract_private_marker_parts( + private_marker + ) + closing_marker = f"" + + if closing_marker not in markers: + markers.append( + closing_marker + ) + return tuple( markers ) @@ -1485,6 +1606,18 @@ def _unclosed_internal_action_request_start( for action_name in normalize_runtime_action_names( enabled_actions ): + if action_name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY: + marker_start = ( + _unclosed_update_active_memory_attribute_start( + text + ) + ) + + if marker_start is not None: + marker_starts.append( + marker_start + ) + private_marker, close_tag = _runtime_action_marker_config( action_name ) @@ -1513,6 +1646,42 @@ def _unclosed_internal_action_request_start( ) +def _unclosed_update_active_memory_attribute_start( + text: str, +) -> int | None: + + value = str( + text + or "" + ) + + if not value: + return None + + marker_start = value.rfind( + "<" + ) + + if marker_start < 0: + return None + + candidate = value[ + marker_start: + ] + + if ">" in candidate: + return None + + if re.fullmatch( + r"<\s*UPDATE_ACTIVE_MEMORY(?:\s+[^<>]*)?", + candidate, + re.IGNORECASE | re.DOTALL, + ) is None: + return None + + return marker_start + + def _asset_action_stream_payload_has_action( candidate: str, ) -> bool: @@ -1629,7 +1798,16 @@ def _build_started_actions( marker_start, ) - if opening_match is None: + update_attribute_start = ( + action_name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY + and UPDATE_ACTIVE_MEMORY_START_RE.match( + text, + marker_start, + ) + is not None + ) + + if opening_match is None and not update_attribute_start: continue if ( @@ -1689,6 +1867,19 @@ def _find_started_actions( match.start() ) + # UPDATE_ACTIVE_MEMORY also supports the compact self-closing + # attribute form, e.g. + # . + # Detect the opening name itself, before the attribute payload + # is complete, so its pending bubble lights up while streaming. + if action_name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY: + for match in UPDATE_ACTIVE_MEMORY_START_RE.finditer( + text + ): + marker_starts.append( + match.start() + ) + started_actions = [] for marker_start in sorted( diff --git a/utils/actions/update_active_memory_utils.py b/utils/actions/update_active_memory_utils.py index 93330caf..47d86d4c 100644 --- a/utils/actions/update_active_memory_utils.py +++ b/utils/actions/update_active_memory_utils.py @@ -14,6 +14,28 @@ re.IGNORECASE, ) +UPDATE_ACTIVE_MEMORY_SELF_CLOSING_MARKER_RE = re.compile( + ( + r"^\s*<\s*UPDATE_ACTIVE_MEMORY" + r"(?P\s+(?:[^<>\"']+|\"[^\"]*\"|'[^']*')*?)" + r"\s*/\s*>\s*$" + ), + re.IGNORECASE | re.DOTALL, +) + +UPDATE_ACTIVE_MEMORY_ATTRIBUTE_RE = re.compile( + ( + r"(?P[a-z][a-z0-9_]*)" + r"\s*=\s*" + r"(?:" + r"\"(?P[^\"]*)\"" + r"|'(?P[^']*)'" + r"|(?P[^\s\"'<>]+)" + r")" + ), + re.IGNORECASE | re.DOTALL, +) + UPDATE_ACTIVE_MEMORY_FAILURE_REASONS = { "invalid_update_active_memory_payload": "invalid payload", "active_memory_not_found": "incorrect id", @@ -34,6 +56,181 @@ def build_update_active_memory_payload( ) +def _unwrap_update_active_memory_attribute_payload( + payload: str, +) -> str: + + text = str( + payload or "" + ).strip() + + match = UPDATE_ACTIVE_MEMORY_SELF_CLOSING_MARKER_RE.fullmatch( + text + ) + + if match is None: + return text + + return str( + match.group("attributes") + or "" + ).strip() + + +def _read_update_active_memory_attribute_pairs( + payload: str, +) -> tuple[tuple[str, str], ...] | None: + + text = _unwrap_update_active_memory_attribute_payload( + payload + ) + + if ( + not text + or text.startswith("{") + or not re.match( + r"^\s*[a-z][a-z0-9_]*\s*=", + text, + re.IGNORECASE, + ) + ): + return None + + pairs = [] + position = 0 + + while position < len(text): + spacing_match = re.match( + r"\s*", + text[position:], + ) + if spacing_match is not None: + position += spacing_match.end() + + if position >= len(text): + break + + match = UPDATE_ACTIVE_MEMORY_ATTRIBUTE_RE.match( + text, + position, + ) + if match is None: + return () + + value = ( + match.group("double") + if match.group("double") is not None + else ( + match.group("single") + if match.group("single") is not None + else match.group("bare") + ) + ) + pairs.append( + ( + str( + match.group("name") + or "" + ), + str( + value + or "" + ), + ) + ) + position = match.end() + + return tuple( + pairs + ) + + +def _parse_update_active_memory_attribute_payload( + payload: str, + *, + include_reserved_fields: bool = False, +) -> tuple[str, tuple[tuple[str, str], ...]] | None: + + pairs = _read_update_active_memory_attribute_pairs( + payload + ) + + if pairs is None: + return None + + if not pairs: + return "", () + + active_memory_id = "" + changes = [] + seen = set() + + for raw_name, raw_value in pairs: + raw_field_name = str( + raw_name or "" + ).strip().casefold() + + if raw_field_name in { + "active_memory_id", + "id", + }: + if active_memory_id: + return "", () + + active_memory_id = str( + raw_value or "" + ).strip().casefold() + continue + + if include_reserved_fields: + field_name = raw_field_name + if not re.fullmatch( + r"[a-z][a-z0-9_]{0,31}", + field_name, + ): + return "", () + else: + if raw_field_name in ACTIVE_MEMORY_RESERVED_CUSTOM_FIELD_NAMES: + continue + + field_name = normalize_active_memory_custom_field_name( + raw_name + ) + + field_value = normalize_active_memory_custom_field_value( + raw_value + ) + + if ( + not field_name + or not field_value + or field_name in seen + ): + return "", () + + changes.append( + ( + field_name, + field_value, + ) + ) + seen.add( + field_name + ) + + if not ACTIVE_MEMORY_UPDATE_ID_RE.fullmatch( + active_memory_id + ): + return "", () + + if not changes: + return "", () + + return active_memory_id, tuple( + changes + ) + + def _parse_update_active_memory_json_payload( payload: str, ) -> tuple[str, tuple[tuple[str, str], ...]]: @@ -138,6 +335,15 @@ def parse_update_active_memory_payload_fields( """Read the id and submitted values from flat or legacy nested JSON.""" text = str(payload or "").strip() + + attribute_result = _parse_update_active_memory_attribute_payload( + text, + include_reserved_fields=True, + ) + + if attribute_result is not None: + return attribute_result + if not text.startswith("{"): return parse_update_active_memory_payload(text) @@ -203,6 +409,13 @@ def parse_update_active_memory_payload( payload or "" ).strip() + attribute_result = _parse_update_active_memory_attribute_payload( + text + ) + + if attribute_result is not None: + return attribute_result + if text.startswith("{"): return _parse_update_active_memory_json_payload( text diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 21da32ed..d8c61255 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -826,9 +826,6 @@ def build_active_memory_runtime_line( json_payload = raw_visible_value.lstrip().startswith("{") - # Legacy plain-text saves may create custom state only from trailing - # parenthesized declarations. A model-authored square suffix would - # otherwise be indistinguishable from runtime-owned state. if ( not json_payload and collect_active_memory_custom_fields(raw_visible_value) @@ -844,24 +841,6 @@ def build_active_memory_runtime_line( if not visible_value: return "" - # A trailing custom-field declaration is all-or-nothing. If custom-looking - # syntax was present but could not be accepted (duplicate/reserved/over the - # three-field limit), keep the save conservative instead of silently - # creating a schema the model did not actually request. - original_visible_value = suffix_values[0][1].strip() - custom_declaration_present = not json_payload and bool( - re.search( - r"\(\s*[A-Za-z][A-Za-z0-9_]{0,31}\s*:\s*[^()]+\)\s*$", - original_visible_value, - ) - ) - if ( - custom_declaration_present - and visible_value == original_visible_value - and not custom_fields - ): - return "" - suffix_items = [ ( suffix_values[0][0], diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 9d6d9e0b..49e7f22f 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -796,6 +796,13 @@ def normalize_symbolic_motif( ): return "" + # Pure ASCII art commonly repeats structural rows (pipes, + # slashes, underscores, etc.) on purpose. Keep this visual + # guard scoped to non-ASCII symbolic motifs so ASCII layouts + # cannot be mistaken for a reasoning loop. + if stripped.isascii(): + return "" + symbol_chars = [ char for char in stripped diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 8a41d396..160c364d 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -472,6 +472,36 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: normalized_part["message"] = message if part_id: normalized_part["id"] = part_id + + raw_colors = part.get("colors", []) + if isinstance(raw_colors, (str, bytes)): + raw_colors = [raw_colors] + if isinstance(raw_colors, list): + colors = [] + for raw_color in raw_colors: + color = clean_bootstrap_memory( + raw_color, + limit=16, + ).lower() + match = re.fullmatch( + r"#?([0-9a-f]{3}|[0-9a-f]{6})", + color, + re.IGNORECASE, + ) + if match is None: + continue + color = match.group(1).lower() + if len(color) == 3: + color = "".join( + char * 2 + for char in color + ) + normalized_color = f"#{color}" + if normalized_color not in colors: + colors.append(normalized_color) + if colors: + normalized_part["colors"] = colors + normalized_parts.append(normalized_part) if normalized_parts: @@ -2340,6 +2370,10 @@ def enrich_session_bootstrap_from_archive( # Browser persistence is the exact checkpoint when available. Fall back # to archived PREVIOUS_RUNTIME_STATE/resources only when the browser half # is absent. + exact_collection_fields = { + "tool_results", + } + for field in ( "runtime_memory", "runtime_memory_updates", @@ -2347,10 +2381,26 @@ def enrich_session_bootstrap_from_archive( "active_memory_records", "tool_results", ): - if enriched.get(field) in (None, "", [], {}): - value = archived.get(field) - if value not in (None, "", [], {}): - enriched[field] = value + # CLEAN_TOOL_RESULTS deliberately persists tool_results=[]; that empty + # collection is authoritative and must not be repopulated from an older + # archived prompt on the next-tab boot. Other restore collections keep + # their legacy fallback semantics. + if field in exact_collection_fields: + browser_has_value = ( + field in enriched + and enriched.get(field) is not None + ) + else: + browser_has_value = ( + enriched.get(field) not in (None, "", [], {}) + ) + + if browser_has_value: + continue + + value = archived.get(field) + if value not in (None, "", [], {}): + enriched[field] = value enriched["source_session_id"] = source_session_id if archived.get("source_session_date"): From 676522dee90e8aa429ac768fbaca0f2c8174aeb8 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:25:19 +0300 Subject: [PATCH 094/168] chore: ui - update L-T facts rendering with better DOM structure - add optionable failed action follow-up --- agent/nodes/brain.py | 213 +++++ contracts/asset_action.json | 3 +- contracts/attach_file.json | 5 +- contracts/check_todo.json | 3 +- contracts/clean_tool_results.json | 3 +- contracts/create_todo_list.json | 3 +- contracts/deep_web_search.json | 3 +- contracts/detach_file.json | 5 +- contracts/idle.json | 3 +- contracts/jin_color.json | 6 +- contracts/jin_position.json | 6 +- contracts/jin_size.json | 6 +- contracts/jin_speed.json | 6 +- contracts/list_files.json | 5 +- contracts/load_delayed_memory.json | 3 +- contracts/load_skill.json | 3 +- contracts/resolve_active_memory.json | 3 +- contracts/resolve_todo.json | 3 +- contracts/rules_assembler.py | 28 +- contracts/save_active_memory.json | 13 +- contracts/save_delayed_memory.json | 3 +- contracts/unload_delayed_memory.json | 3 +- contracts/unload_skill.json | 3 +- contracts/update_active_memory.json | 11 +- contracts/update_l4_facts.json | 3 +- contracts/web_search.json | 3 +- runtime/L4_memory.py | 309 +++++++- runtime/L4_memory_utils.py | 171 +++- runtime/runtime_context.py | 3 + tests/runtime_actions/test_active_memory.py | 2 +- tests/test_active_memory_failure_followup.py | 267 +++++++ tests/test_l4_fact_age_client_contract.py | 59 +- tests/test_l4_memory.py | 255 ++++++ ...t_memory_reference_sync_client_contract.py | 2 +- ...time_memory_hover_title_client_contract.py | 12 +- ...e_memory_l4_render_perf_client_contract.py | 143 ++++ ...untime_memory_lazy_rows_client_contract.py | 45 +- ...time_memory_row_payload_client_contract.py | 12 +- tests/test_runtime_panel_client_contract.py | 26 +- ...active_memory_highlight_client_contract.py | 58 ++ ui/static/css/runtime-memory.css | 39 + ui/static/js/runtime/runtime-memory-view.js | 742 +++++++++++++----- ui/static/js/runtime/runtime-panel.js | 17 + ui/static/js/socket/runtime-actions.js | 34 +- ui/templates/index.html | 8 +- utils/actions/dispatcher.py | 70 +- utils/actions/save_active_memory_utils.py | 9 +- utils/context/result_sections.py | 11 + 48 files changed, 2291 insertions(+), 352 deletions(-) create mode 100644 tests/test_active_memory_failure_followup.py create mode 100644 tests/test_runtime_memory_l4_render_perf_client_contract.py create mode 100644 tests/test_update_active_memory_highlight_client_contract.py diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index fbeea189..df5da5c2 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -38,9 +38,14 @@ RUNTIME_ACTION_WEB_SEARCH, ) from contracts.rules_assembler import ( + extract_private_marker_name, get_action_contract_name_for_runtime_action, get_runtime_action_display_name, + get_runtime_action_private_marker, + get_runtime_action_rules, runtime_action_emits_followup, + runtime_action_follows_up_on_fail, + runtime_action_has_close_tag, ) from clients.search_client import ( @@ -103,11 +108,207 @@ def action_event_requires_follow_up(event) -> bool: name = str(event.get("name", "") or "").strip().casefold() + if status == "failed": + return runtime_action_follows_up_on_fail( + name + ) + return runtime_action_emits_followup( name ) +def _build_failed_runtime_action_marker(event: dict) -> str: + + runtime_action = str( + event.get("name", "") + or "" + ).strip() + marker = get_runtime_action_private_marker( + runtime_action + ) + payload = str( + event.get("failed_marker_payload") + or event.get("payload") + or "" + ).strip() + + if not marker: + return payload + + if not runtime_action_has_close_tag( + runtime_action + ): + return " ".join( + part + for part in (marker, payload) + if part + ).strip() + + marker_name = extract_private_marker_name( + marker + ) + if not marker_name: + return "\n".join( + part + for part in (marker, payload) + if part + ).strip() + + return "\n".join(( + marker, + payload, + f"", + )).strip() + + +def build_failed_runtime_action_followup_context( + event: dict, +) -> str: + + if not isinstance(event, dict): + return "" + + runtime_action = str( + event.get("name", "") + or "" + ).strip() + if ( + str(event.get("status", "") or "").strip().casefold() != "failed" + or not runtime_action_follows_up_on_fail(runtime_action) + ): + return "" + + failure_reason = str( + event.get("failure_reason") + or event.get("detail") + or event.get("error") + or "action failed" + ).strip() + display_name = get_runtime_action_display_name( + runtime_action + ) + mandatory_rules = "\n".join( + get_runtime_action_rules( + runtime_action + ) + ).strip() + failed_marker = _build_failed_runtime_action_marker( + event + ) + + sections = [ + ( + "RUNTIME ACTION ERROR: " + f"{display_name or runtime_action.upper()} failed: " + f"{failure_reason}" + ), + ] + + if mandatory_rules: + sections.append( + "\n" + + mandatory_rules + + "\n" + ) + + if failed_marker: + sections.append( + "\n" + + failed_marker + + "\n" + ) + + return "\n\n".join(sections) + + +def build_failed_runtime_action_followup_contexts( + context, +) -> str: + + if context is None: + return "" + + current_turn_ids = { + str( + getattr( + context, + attribute, + "", + ) + or "" + ).strip() + for attribute in ( + "runtime_current_turn_id", + "runtime_current_sequence_turn_id", + ) + } + current_turn_ids.discard("") + latest_events = {} + + for index, event in enumerate( + getattr( + context, + "runtime_action_events", + [], + ) + or [] + ): + if not isinstance(event, dict): + continue + + event_turn_id = str( + event.get( + "runtime_turn_id", + "", + ) + or "" + ).strip() + if ( + current_turn_ids + and event_turn_id + and event_turn_id not in current_turn_ids + ): + continue + + runtime_action = str( + event.get( + "name", + "", + ) + or "" + ).strip().casefold() + if not runtime_action_follows_up_on_fail( + runtime_action + ): + continue + + latest_events[runtime_action] = ( + index, + event, + ) + + contexts = [] + + for _, event in sorted( + latest_events.values(), + key=lambda item: item[0], + ): + failure_context = ( + build_failed_runtime_action_followup_context( + event + ) + ) + if failure_context: + contexts.append( + failure_context + ) + + return "\n\n".join( + contexts + ) + + def action_event_defers_follow_up(event) -> bool: if not isinstance(event, dict): @@ -1092,6 +1293,18 @@ def build_followup_system_prompt( ) ) + failed_action_context = ( + build_failed_runtime_action_followup_contexts( + context + ) + if context is not None + else "" + ) + if failed_action_context: + sections.append( + failed_action_context + ) + if instruction.strip(): sections.append( instruction.strip() diff --git a/contracts/asset_action.json b/contracts/asset_action.json index d94790b6..d3dd6c7c 100644 --- a/contracts/asset_action.json +++ b/contracts/asset_action.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "PROJECT ASSETS:", diff --git a/contracts/attach_file.json b/contracts/attach_file.json index e5b094f7..10523207 100644 --- a/contracts/attach_file.json +++ b/contracts/attach_file.json @@ -6,7 +6,10 @@ "private_marker": "", "triggers": [], "blockers": [], - "effects": {"emit_followup": false}, + "effects": { + "emit_followup": false, + "follow_up_on_fail": false + }, "rules": [ "DO NOT attach files if they are already present in user message with correct ids.", "Emit one marker per file id to attach that saved file to context.", diff --git a/contracts/check_todo.json b/contracts/check_todo.json index 49b9e57a..ee38ceb9 100644 --- a/contracts/check_todo.json +++ b/contracts/check_todo.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "RUNTIME TODO LEDGER:", diff --git a/contracts/clean_tool_results.json b/contracts/clean_tool_results.json index 27c8ccb5..9286dd70 100644 --- a/contracts/clean_tool_results.json +++ b/contracts/clean_tool_results.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Emit at any moment in you answer to clean redundant tool results and only if they are present in the context inside block." diff --git a/contracts/create_todo_list.json b/contracts/create_todo_list.json index e287d7ed..9f23f8cc 100644 --- a/contracts/create_todo_list.json +++ b/contracts/create_todo_list.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "RUNTIME TODO LEDGER:", diff --git a/contracts/deep_web_search.json b/contracts/deep_web_search.json index a1f92cb3..49e3e91c 100644 --- a/contracts/deep_web_search.json +++ b/contracts/deep_web_search.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "DEEP_WEB_SEARCH:", diff --git a/contracts/detach_file.json b/contracts/detach_file.json index e71ec159..5ccb8c46 100644 --- a/contracts/detach_file.json +++ b/contracts/detach_file.json @@ -6,7 +6,10 @@ "private_marker": "", "triggers": [], "blockers": [], - "effects": {"emit_followup": false}, + "effects": { + "emit_followup": false, + "follow_up_on_fail": false + }, "rules": [ "Emit to detach one file from context. The saved file remains in FILES storage.", "id is a placeholder; replace it with the 6-character id shown in or LIST_FILES." diff --git a/contracts/idle.json b/contracts/idle.json index d206dbb8..fb9cd977 100644 --- a/contracts/idle.json +++ b/contracts/idle.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "Emit idle action marker to setup and trigger follow-up tick after Ns seconds. DO NOT USE for casual chat reminders." diff --git a/contracts/jin_color.json b/contracts/jin_color.json index 6a9dc1af..accda436 100644 --- a/contracts/jin_color.json +++ b/contracts/jin_color.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use to set single color for the live JIN avatar center point color and chat interface background tint.", @@ -16,7 +17,6 @@ "Emit several markers in one message for consecutive color change or blinking effect, last color in sequence will be used as current.", "The payload must be a hex color with optional leading # and exactly 3 or 6 hex characters." ], - "close_tag": true, - "body_placeholder": "#00f2ff" + "close_tag": true } } diff --git a/contracts/jin_position.json b/contracts/jin_position.json index 88bb32de..82a80c90 100644 --- a/contracts/jin_position.json +++ b/contracts/jin_position.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use to move the collapsed live JIN avatar inside the current browser window as a non-verbal spatial gesture.", @@ -18,7 +19,6 @@ "CURRENT_JIN_POSITION reports that same avatar-center point. For example, the center of a 1920x1080 window is x:960px y:540px regardless of the avatar size; the UI offsets by the avatar's current rendered width and height and clamps the full avatar safely inside the viewport.", "Movement uses the current JIN_SPEED. You may change JIN_SPEED immediately before JIN_POSITION to crawl slowly or move almost instantly." ], - "close_tag": true, - "body_placeholder": "120px 80px" + "close_tag": true } } diff --git a/contracts/jin_size.json b/contracts/jin_size.json index 6e5da0d1..e6f0f2d1 100644 --- a/contracts/jin_size.json +++ b/contracts/jin_size.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use to set JIN Live Avatar size.", @@ -15,7 +16,6 @@ "You may emit 120px to make the avatar square.", "Payload values may include px, percent, or vw/vh. Default is - px." ], - "close_tag": true, - "body_placeholder": "120px 120px" + "close_tag": true } } diff --git a/contracts/jin_speed.json b/contracts/jin_speed.json index de4a4d82..99588f89 100644 --- a/contracts/jin_speed.json +++ b/contracts/jin_speed.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use to set how quickly the collapsed live JIN avatar moves when JIN_POSITION is executed.", @@ -17,7 +18,6 @@ "Small values such as 20-80px/s create a slow crawl; large values such as 2000-6000px/s create a quick jump-like move.", "Use CURRENT_JIN_SPEED as the current movement speed." ], - "close_tag": true, - "body_placeholder": "600px/s" + "close_tag": true } } diff --git a/contracts/list_files.json b/contracts/list_files.json index 485b0d4c..ed641c9d 100644 --- a/contracts/list_files.json +++ b/contracts/list_files.json @@ -6,7 +6,10 @@ "private_marker": "", "triggers": [], "blockers": [], - "effects": {"emit_followup": true}, + "effects": { + "emit_followup": true, + "follow_up_on_fail": false + }, "rules": [ "Emit when you need the persistent file inventory.", "The result is placed in TOOLS_RESULTS as LIST_FILES and contains at most 100 records with ids, size, dimensions when known, and age.", diff --git a/contracts/load_delayed_memory.json b/contracts/load_delayed_memory.json index fa7c876d..0e965620 100644 --- a/contracts/load_delayed_memory.json +++ b/contracts/load_delayed_memory.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Emit one marker per report.", diff --git a/contracts/load_skill.json b/contracts/load_skill.json index 096e0f2b..ead405eb 100644 --- a/contracts/load_skill.json +++ b/contracts/load_skill.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "LOAD / UNLOAD SKILLS:", diff --git a/contracts/resolve_active_memory.json b/contracts/resolve_active_memory.json index 72706028..e19187fc 100644 --- a/contracts/resolve_active_memory.json +++ b/contracts/resolve_active_memory.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "RESOLVE_ACTIVE_MEMORY:", diff --git a/contracts/resolve_todo.json b/contracts/resolve_todo.json index 1fc3ce83..78501c87 100644 --- a/contracts/resolve_todo.json +++ b/contracts/resolve_todo.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "RUNTIME TODO LEDGER:", diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index 7a14e606..6f521afb 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -264,6 +264,18 @@ def runtime_action_emits_followup(runtime_action: str) -> bool: return bool(effects.get("emit_followup", True)) +def runtime_action_follows_up_on_fail(runtime_action: str) -> bool: + name, contract = get_action_contract_for_runtime_action(runtime_action) + if not name or not contract: + return False + + effects = contract.get("effects", {}) + if not isinstance(effects, dict): + return False + + return bool(effects.get("follow_up_on_fail", False)) + + def get_enabled_runtime_actions(runtime_actions=None) -> tuple[str, ...]: enabled_actions = [] action_flags = runtime_actions or {} @@ -327,11 +339,6 @@ def get_runtime_action_private_marker(runtime_action: str) -> str: return str(contract.get("private_marker", "") or "").strip() -def get_runtime_action_body_placeholder(runtime_action: str) -> str: - _, contract = get_action_contract_for_runtime_action(runtime_action) - return str(contract.get("body_placeholder", "") or "").strip() - - def get_runtime_action_rules(runtime_action: str) -> tuple[str, ...]: _, contract = get_action_contract_for_runtime_action(runtime_action) return tuple( @@ -354,12 +361,6 @@ def build_runtime_action_marker_schema(contract: dict[str, Any]) -> str: if not marker_name: return marker - body_placeholder = str( - contract.get("body_placeholder", "") or "" - ).strip() - if body_placeholder: - return f"{marker} {body_placeholder} " - return f"{marker}" @@ -527,11 +528,6 @@ def build_allowed_markers( continue marker = get_runtime_action_private_marker(action_name) - if get_runtime_action_body_placeholder(action_name): - _, contract = get_action_contract_for_runtime_action( - action_name - ) - marker = build_runtime_action_marker_schema(contract) if marker: markers.append(marker) diff --git a/contracts/save_active_memory.json b/contracts/save_active_memory.json index 0b8760e0..15e236c7 100644 --- a/contracts/save_active_memory.json +++ b/contracts/save_active_memory.json @@ -7,20 +7,19 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": true }, "rules": [ "Use when a clear, concrete unresolved intention, promise, condition, reminder, live rule, or future condition appears and keeping it pending would help the current or near-future conversation.", + "Avoid duplicating with long term facts, basic rules or another active memories.", "Emit using exactly this schema:", "", - "{\"conditions\":\"CONDITIONS\",\"custom_field_name\":\"VALUE\"}", + "{\"conditions\":\"\",\"custom_field_name\":\"\"}", "", - "conditions is the required default field; fill it with the concrete pending item to preserve.", - "Emit one plain JSON object inside the tag body. Put each optional custom field beside conditions at the JSON root; do not nest custom fields under fields or any other wrapper.", - "Do not save trivial exchanges, unstable ideas, identity anchors, broad user preferences, etc." + "conditions is required and must be non-empty." ], "close_tag": true, - "body_placeholder": "{\"conditions\":\"CONDITIONS\"}", - "display_payload": true + "display_payload": false } } diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 9a64e21d..1664e821 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -12,7 +12,8 @@ ], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use this marker only after the user explicitly asks to create/save a delayed memory report.", diff --git a/contracts/unload_delayed_memory.json b/contracts/unload_delayed_memory.json index bf08e83e..f79108fe 100644 --- a/contracts/unload_delayed_memory.json +++ b/contracts/unload_delayed_memory.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "Use the 6-character id of a report in to remove it from current context; storage is unchanged." diff --git a/contracts/unload_skill.json b/contracts/unload_skill.json index 5333f2c3..4145fd72 100644 --- a/contracts/unload_skill.json +++ b/contracts/unload_skill.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "LOAD / UNLOAD SKILLS:", diff --git a/contracts/update_active_memory.json b/contracts/update_active_memory.json index 8f863746..7f808f57 100644 --- a/contracts/update_active_memory.json +++ b/contracts/update_active_memory.json @@ -7,18 +7,17 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": true }, "rules": [ "Use to update active memory values.", "Emit using exactly this schema:", "", - "{\"active_memory_id\":\"existing id VALUE\",\"field_name\":\"NEW_VALUE\",\"another_existing_field\":\"NEW_VALUE\"}", + "{\"active_memory_id\":\"\",\"custom_field_name\":\"\"}", "", - "active_memory_id is a placeholder; replace it with the exact six-character active_memory_id value.", - "After active_memory_id, put each existing custom field directly at the JSON root." + "Put each existing custom field needed for update directly at the JSON root." ], - "close_tag": true, - "body_placeholder": "{\"active_memory_id\":\"existing id VALUE\",\"field_name\":\"NEW_VALUE\"}" + "close_tag": true } } diff --git a/contracts/update_l4_facts.json b/contracts/update_l4_facts.json index 0655993d..18348d39 100644 --- a/contracts/update_l4_facts.json +++ b/contracts/update_l4_facts.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": false, + "follow_up_on_fail": false }, "rules": [ "JIN can update, merge, or create durable facts through this marker.", diff --git a/contracts/web_search.json b/contracts/web_search.json index 60a8542d..15e36da3 100644 --- a/contracts/web_search.json +++ b/contracts/web_search.json @@ -7,7 +7,8 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": true + "emit_followup": true, + "follow_up_on_fail": false }, "rules": [ "WEB_SEARCH:", diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index db6cc880..f4799262 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -34,13 +34,13 @@ format_l4_merge_operation_details, format_long_term_memory_context, infer_l4_jin_note_action, + inspect_l4_merge_shard_scan, l4_jin_note_requests_new_fact, mark_facts_memory_fields_analyzed, merge_l4_store_snapshots, normalize_facts_memory_records, normalize_l4_candidates, normalize_l4_merge_operations, - normalize_l4_merge_shard_scan, normalize_l4_key, normalize_l4_store, normalize_l4_text, @@ -102,6 +102,7 @@ def reset_l4_merge_recovery_state( context.runtime_l4_merge_existing_batch_mode = "" context.runtime_l4_merge_paused_signature = "" context.runtime_l4_merge_deferred_pending_until = {} + context.runtime_l4_merge_single_retry_pending_ids = set() context.runtime_l4_merge_force_single_batch_once = False context.runtime_l4_merge_truncation_streak = 0 context.runtime_l4_merge_retry_not_before = 0.0 @@ -299,6 +300,89 @@ def clear_l4_merge_pending_recovery( deferred.pop(str(pending_id or "").strip(), None) context.runtime_l4_merge_deferred_pending_until = deferred + single_retry_ids = set( + getattr( + context, + "runtime_l4_merge_single_retry_pending_ids", + set(), + ) + or set() + ) + for pending_id in pending_ids or []: + single_retry_ids.discard(str(pending_id or "").strip()) + context.runtime_l4_merge_single_retry_pending_ids = single_retry_ids + + +def record_l4_merge_shard_scan_failures( + context, + *, + pending_ids: list[str], +) -> dict: + """Defer only bad shard-scan items and force their retries to be isolated.""" + + normalized_pending_ids = [] + seen = set() + for pending_id in pending_ids or []: + normalized = str(pending_id or "").strip() + if not normalized or normalized in seen: + continue + seen.add(normalized) + normalized_pending_ids.append(normalized) + + deferred = dict( + getattr( + context, + "runtime_l4_merge_deferred_pending_until", + {}, + ) + or {} + ) + single_retry_ids = set( + getattr( + context, + "runtime_l4_merge_single_retry_pending_ids", + set(), + ) + or set() + ) + now = time.monotonic() + poison_ids = [] + + for pending_id in normalized_pending_ids: + # First failed repair gets a short isolated retry. If that same pending + # fact later fails another isolated shard repair, cool it down longer + # instead of hammering the service model every idle tick. + already_isolated = pending_id in single_retry_ids + retry_after = ( + L4_MERGE_POISON_DEFER_SECONDS + if already_isolated + else L4_MERGE_VALIDATION_RETRY_SECONDS + ) + if already_isolated: + poison_ids.append(pending_id) + deferred[pending_id] = now + retry_after + single_retry_ids.add(pending_id) + + context.runtime_l4_merge_deferred_pending_until = deferred + context.runtime_l4_merge_single_retry_pending_ids = single_retry_ids + + retry_after_seconds = ( + L4_MERGE_POISON_DEFER_SECONDS + if poison_ids + else L4_MERGE_VALIDATION_RETRY_SECONDS + ) + return { + "retry_behavior": ( + "The shard scan repair pass still failed only for these pending " + "facts. Valid pending facts continue immediately; failed facts " + "stay pending and are retried one at a time after the backoff." + ), + "deferred_pending_ids": normalized_pending_ids, + "single_retry_pending_ids": normalized_pending_ids, + "poison_deferred_pending_ids": poison_ids, + "retry_after_seconds": retry_after_seconds, + } + def get_l4_merge_available_pending_queue( context, @@ -2206,6 +2290,53 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: if deferred_backoff is not None: return deferred_backoff + all_pending_ids = { + str(fact.get("id") or "").strip() + for fact in base_store.get("pending_facts") or [] + if isinstance(fact, dict) and str(fact.get("id") or "").strip() + } + single_retry_ids = { + pending_id + for pending_id in set( + getattr( + context, + "runtime_l4_merge_single_retry_pending_ids", + set(), + ) + or set() + ) + if pending_id in all_pending_ids + } + context.runtime_l4_merge_single_retry_pending_ids = single_retry_ids + + single_retry_pending_id = next( + ( + str(fact.get("id") or "").strip() + for fact in pending_queue + if isinstance(fact, dict) + and str(fact.get("id") or "").strip() in single_retry_ids + ), + "", + ) + if single_retry_pending_id: + # Once a repaired shard item has failed, retry that candidate alone. + # Move only the due retry to the front without mutating canonical FIFO + # storage; later valid items remain available on the following tick. + pending_queue = [ + *[ + fact + for fact in pending_queue + if str(fact.get("id") or "").strip() + == single_retry_pending_id + ], + *[ + fact + for fact in pending_queue + if str(fact.get("id") or "").strip() + != single_retry_pending_id + ], + ] + system_prompt = build_l4_merge_system_prompt() runtime_context_window = 0 context_window_resolver = getattr( @@ -2245,7 +2376,8 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: ) protected_fact_ids = get_runtime_l4_explicit_edit_fact_ids(context) force_single_batch = bool( - getattr( + single_retry_pending_id + or getattr( context, "runtime_l4_merge_force_single_batch_once", False, @@ -2287,6 +2419,8 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: "shard_scan": [], "shard_previous_facts": [], "shard_second_half": [], + "shard_scan_recovery": {}, + "finalize_budget_trimmed": False, } first_half, second_half = existing_batches @@ -2332,41 +2466,148 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: scan_payload = extract_l4_json_payload( extract_runtime_memory_text(scan_response) ) - scan_results = normalize_l4_merge_shard_scan( + visible_first_half_ids = [ + fact.get("id") + for fact in first_half + if isinstance(fact, dict) + ] + scan_inspection = inspect_l4_merge_shard_scan( scan_payload, pending_ids=selected_pending_ids, - visible_fact_ids=[ - fact.get("id") - for fact in first_half - if isinstance(fact, dict) - ], + visible_fact_ids=visible_first_half_ids, ) - if not scan_results: - recovery = record_l4_merge_validation_failure( - context, - pending_ids=selected_pending_ids, - batch_count=len(selected_pending), + scan_results = list(scan_inspection["results"]) + shard_scan_recovery = {} + initial_invalid_ids = list(scan_inspection["invalid_pending_ids"]) + + if initial_invalid_ids: + invalid_id_set = set(initial_invalid_ids) + repair_pending = [ + fact + for fact in selected_pending + if str(fact.get("id") or "").strip() in invalid_id_set + ] + repair_context = { + "validation_error": "invalid_shard_scan_items", + "required_pending_ids": initial_invalid_ids, + "pending_errors": scan_inspection["pending_errors"], + "global_errors": scan_inspection["global_errors"], + "previous_scan": ( + scan_payload.get("scan", []) + if isinstance(scan_payload, dict) + else [] + ), + "instruction": ( + "Repair only the required pending_ids. Return exactly one " + "valid scan row for each required pending_id and no rows " + "for any other pending_id. Use only fact IDs visible in " + "this shard. Return corrected JSON only." + ), + } + repair_prompt = build_l4_merge_shard_scan_user_prompt( + existing_facts=first_half, + pending_facts=repair_pending, + protected_fact_ids=protected_fact_ids, + repair_context=repair_context, ) - terminal = await log_l4_skip_event( - context, - phase="merge", - message_phase="merge", - reason="invalid_shard_scan", - details={ - "pending_count": len(selected_pending), - "pending_ids": selected_pending_ids, - "existing_batch_mode": "halves", - "shard": 1, - **recovery, - }, + repair_response = await ask_l4_model( + context=context, + service_client=service_client, + label="L4 merge", + system_prompt=scan_system_prompt, + user_prompt=repair_prompt, + max_tokens=first_plan["requested_max_output_tokens"], ) - return {"terminal": terminal} + repair_error = "" + if is_runtime_memory_response_truncated(repair_response): + repair_inspection = { + "results": [], + "valid_pending_ids": [], + "invalid_pending_ids": initial_invalid_ids, + "pending_errors": { + pending_id: ["repair_response_truncated"] + for pending_id in initial_invalid_ids + }, + "global_errors": ["repair_response_truncated"], + } + repair_error = "response_truncated" + else: + repair_payload = extract_l4_json_payload( + extract_runtime_memory_text(repair_response) + ) + repair_inspection = inspect_l4_merge_shard_scan( + repair_payload, + pending_ids=initial_invalid_ids, + visible_fact_ids=visible_first_half_ids, + ) + + results_by_pending = { + item["pending_id"]: item + for item in [ + *scan_results, + *repair_inspection["results"], + ] + } + scan_results = [ + results_by_pending[pending_id] + for pending_id in selected_pending_ids + if pending_id in results_by_pending + ] + repaired_pending_ids = list( + repair_inspection["valid_pending_ids"] + ) + unrepaired_pending_ids = list( + repair_inspection["invalid_pending_ids"] + ) + shard_scan_recovery = { + "repair_attempted": True, + "initial_invalid_pending_ids": initial_invalid_ids, + "initial_validation_errors": scan_inspection["pending_errors"], + "initial_global_errors": scan_inspection["global_errors"], + "repaired_pending_ids": repaired_pending_ids, + "repair_validation_errors": repair_inspection["pending_errors"], + "repair_global_errors": repair_inspection["global_errors"], + **({"repair_error": repair_error} if repair_error else {}), + } + + if unrepaired_pending_ids: + isolation = record_l4_merge_shard_scan_failures( + context, + pending_ids=unrepaired_pending_ids, + ) + shard_scan_recovery.update(isolation) + + if not scan_results: + terminal = await log_l4_skip_event( + context, + phase="merge", + message_phase="merge", + reason="invalid_shard_scan", + details={ + "pending_count": len(selected_pending), + "pending_ids": selected_pending_ids, + "existing_batch_mode": "halves", + "shard": 1, + **shard_scan_recovery, + }, + ) + return {"terminal": terminal} # The first pass returns only compact overlap evidence. Before the # second request, trim the pending prefix if that hand-off metadata # consumes the last bit of the live context budget. Extra first-pass # scan results are harmless and remain queued for the next idle tick. - final_pending = list(selected_pending) + scan_valid_ids = { + item.get("pending_id") + for item in scan_results + if item.get("pending_id") + } + final_pending = [ + fact + for fact in selected_pending + if str(fact.get("id") or "").strip() in scan_valid_ids + ] + scan_eligible_pending_count = len(final_pending) final_scan = list(scan_results) final_prompt = "" final_previous_facts = [] @@ -2448,6 +2689,10 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: "shard_scan": final_scan, "shard_previous_facts": final_previous_facts, "shard_second_half": second_half, + "shard_scan_recovery": shard_scan_recovery, + "finalize_budget_trimmed": ( + len(final_pending) < scan_eligible_pending_count + ), } execution = None @@ -2540,9 +2785,12 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: shard_scan = execution["shard_scan"] shard_previous_facts = execution["shard_previous_facts"] shard_second_half = execution["shard_second_half"] + shard_scan_recovery = execution.get("shard_scan_recovery") or {} + finalize_budget_trimmed = bool(execution.get("finalize_budget_trimmed")) user_prompt = batch_plan["user_prompt"] if ( - planned_batch_count + finalize_budget_trimmed + and planned_batch_count and len(pending_facts) < planned_batch_count and len(pending_queue) > len(pending_facts) ): @@ -2556,6 +2804,8 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: pending_ids=pending_ids, ) truncation_details["existing_batch_mode"] = merge_mode + if shard_scan_recovery: + truncation_details["shard_scan_recovery"] = shard_scan_recovery truncation_details.update( record_l4_merge_truncation( context, @@ -2774,6 +3024,9 @@ async def execute_double_batch_plan(double_plan: dict) -> dict: ) merge_change = rebased_change + if shard_scan_recovery: + merge_change["shard_scan_recovery"] = shard_scan_recovery + context.runtime_long_term_memory_store = next_store clear_l4_merge_pending_recovery( context, diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 5211c4cf..5999113c 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -287,6 +287,7 @@ def build_l4_merge_shard_scan_user_prompt( existing_facts: list[dict], pending_facts: list[dict], protected_fact_ids=(), + repair_context: dict | None = None, ) -> str: return ( "Scan this committed-memory shard against every pending candidate.\n\n" @@ -307,6 +308,11 @@ def build_l4_merge_shard_scan_user_prompt( for fact_id in normalize_l4_string_list(protected_fact_ids) if normalize_l4_id(fact_id, pending=False) ], + **( + {"repair": repair_context} + if isinstance(repair_context, dict) and repair_context + else {} + ), }, ensure_ascii=False, separators=(",", ":"), @@ -321,14 +327,39 @@ def normalize_l4_merge_shard_scan( pending_ids: list[str], visible_fact_ids: list[str], ) -> list[dict]: - if not isinstance(payload, dict) or not isinstance(payload.get("scan"), list): + inspection = inspect_l4_merge_shard_scan( + payload, + pending_ids=pending_ids, + visible_fact_ids=visible_fact_ids, + ) + if inspection["invalid_pending_ids"] or inspection["global_errors"]: return [] + return inspection["results"] + + +def inspect_l4_merge_shard_scan( + payload, + *, + pending_ids: list[str], + visible_fact_ids: list[str], +) -> dict: + """Validate shard-scan rows independently instead of poisoning the batch. + + The strict public normalizer above still preserves the old all-or-nothing + contract for callers that need it. The merge runtime uses this inspection + result so a malformed row can be repaired/retried without discarding valid + rows for unrelated pending facts. + """ + + ordered_pending = [] + expected_pending = set() + for raw_pending_id in pending_ids: + pending_id = normalize_l4_id(raw_pending_id, pending=True) + if not pending_id or pending_id in expected_pending: + continue + expected_pending.add(pending_id) + ordered_pending.append(pending_id) - expected_pending = { - normalize_l4_id(pending_id, pending=True) - for pending_id in pending_ids - if normalize_l4_id(pending_id, pending=True) - } visible_facts = { normalize_l4_id(fact_id, pending=False) for fact_id in visible_fact_ids @@ -340,40 +371,89 @@ def normalize_l4_merge_shard_scan( "update", "merge", } - normalized = [] - seen_pending = set() + results_by_pending = {} + pending_errors: dict[str, list[str]] = {} + global_errors = [] + seen_rows = set() + + def add_pending_error(pending_id: str, reason: str) -> None: + errors = pending_errors.setdefault(pending_id, []) + if reason not in errors: + errors.append(reason) + # Once a row is ambiguous/invalid, never keep an earlier result for + # the same pending_id. A repair request will resolve that one item. + results_by_pending.pop(pending_id, None) + + if not isinstance(payload, dict) or not isinstance(payload.get("scan"), list): + return { + "results": [], + "valid_pending_ids": [], + "invalid_pending_ids": ordered_pending, + "pending_errors": { + pending_id: ["invalid_scan_payload"] + for pending_id in ordered_pending + }, + "global_errors": ["invalid_scan_payload"], + } - for raw_item in payload.get("scan") or []: + for index, raw_item in enumerate(payload.get("scan") or []): if not isinstance(raw_item, dict): - return [] + global_errors.append(f"item_{index}:invalid_item") + continue + pending_id = normalize_l4_id(raw_item.get("pending_id"), pending=True) + if not pending_id: + global_errors.append(f"item_{index}:invalid_pending_id") + continue + if pending_id not in expected_pending: + global_errors.append( + f"item_{index}:unexpected_pending_id:{pending_id}" + ) + continue + if pending_id in seen_rows: + add_pending_error(pending_id, "duplicate_pending_id") + continue + seen_rows.add(pending_id) + decision = normalize_l4_key(raw_item.get("decision")) - if ( - not pending_id - or pending_id not in expected_pending - or pending_id in seen_pending - or decision not in allowed_decisions - ): - return [] + if decision not in allowed_decisions: + add_pending_error(pending_id, "invalid_decision") + continue - fact_ids = [ - fact_id - for fact_id in ( - normalize_l4_id(raw_id, pending=False) - for raw_id in normalize_l4_string_list(raw_item.get("fact_ids")) - ) - if fact_id - ] + raw_fact_ids = raw_item.get("fact_ids") + raw_fact_values = ( + raw_fact_ids + if isinstance(raw_fact_ids, list) + else [raw_fact_ids] + ) + fact_ids = [] + invalid_fact_id = False + for raw_fact_id in raw_fact_values: + raw_text = normalize_l4_text(raw_fact_id) + if not raw_text: + continue + fact_id = normalize_l4_id(raw_text, pending=False) + if not fact_id: + invalid_fact_id = True + continue + if fact_id not in fact_ids: + fact_ids.append(fact_id) + + if invalid_fact_id: + add_pending_error(pending_id, "invalid_fact_id") + continue if any(fact_id not in visible_facts for fact_id in fact_ids): - return [] - if len(set(fact_ids)) != len(fact_ids): - return [] + add_pending_error(pending_id, "fact_id_outside_shard") + continue if decision == "no_match" and fact_ids: - return [] + add_pending_error(pending_id, "no_match_with_fact_ids") + continue if decision == "update" and len(fact_ids) != 1: - return [] + add_pending_error(pending_id, "update_requires_one_fact_id") + continue if decision == "merge" and len(fact_ids) < 2: - return [] + add_pending_error(pending_id, "merge_requires_two_fact_ids") + continue item = { "pending_id": pending_id, @@ -383,18 +463,29 @@ def normalize_l4_merge_shard_scan( comment = normalize_l4_text(raw_item.get("comment")) if comment: item["comment"] = comment - normalized.append(item) - seen_pending.add(pending_id) + results_by_pending[pending_id] = item - if seen_pending != expected_pending: - return [] + for pending_id in ordered_pending: + if pending_id not in seen_rows: + add_pending_error(pending_id, "missing_scan_result") - order = { - pending_id: index - for index, pending_id in enumerate(pending_ids) + results = [ + results_by_pending[pending_id] + for pending_id in ordered_pending + if pending_id in results_by_pending + ] + invalid_pending_ids = [ + pending_id + for pending_id in ordered_pending + if pending_id in pending_errors + ] + return { + "results": results, + "valid_pending_ids": [item["pending_id"] for item in results], + "invalid_pending_ids": invalid_pending_ids, + "pending_errors": pending_errors, + "global_errors": global_errors, } - normalized.sort(key=lambda item: order.get(item["pending_id"], 10**9)) - return normalized def build_l4_merge_shard_finalize_user_prompt( diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index ae25d671..c5d97608 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -234,6 +234,9 @@ class RuntimeContext: runtime_l4_merge_deferred_pending_until: dict[str, float] = field( default_factory=dict ) + runtime_l4_merge_single_retry_pending_ids: set[str] = field( + default_factory=set + ) runtime_l4_merge_force_single_batch_once: bool = False runtime_l4_memory_update_kind: str = "" runtime_l4_idle_last_started_at: float = 0.0 diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 1c02b943..c5a23363 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -162,7 +162,7 @@ def test_bare_save_active_memory_marker_line_stays_text(self): self.assertEqual(result.actions, ()) self.assertEqual(result.removed_markers, ()) - def test_save_active_memory_marker_helpers_use_body_placeholder(self): + def test_save_active_memory_marker_helpers_use_conditions_placeholder(self): self.assertEqual( get_save_active_memory_marker_fields(), diff --git a/tests/test_active_memory_failure_followup.py b/tests/test_active_memory_failure_followup.py new file mode 100644 index 00000000..1ccd803c --- /dev/null +++ b/tests/test_active_memory_failure_followup.py @@ -0,0 +1,267 @@ +import json +from pathlib import Path +from unittest import IsolatedAsyncioTestCase + +from agent.nodes.brain import ( + BrainNode, + FOLLOWUP_SYSTEM_MESSAGE, + action_event_requires_follow_up, +) +from runtime.runtime_context import RuntimeContext +from utils.actions.common_action_utils import ( + RuntimeActionCall, + extract_runtime_actions, +) +from utils.actions.dispatcher import apply_runtime_action_calls +from utils.context.tool_results import build_tool_results_context + + +class _Emitter: + + def __init__(self): + self.items = [] + + async def emit(self, payload): + self.items.append(payload) + + +class _Logger: + + async def log(self, *args, **kwargs): + return None + + +class ActiveMemoryFailureFollowupTests(IsolatedAsyncioTestCase): + + def test_contract_follow_up_on_fail_is_explicit(self): + contract_dir = Path(__file__).resolve().parents[1] / "contracts" + true_actions = set() + for path in contract_dir.glob("*.json"): + payload = json.loads(path.read_text(encoding="utf-8")) + + for contract in payload.values(): + if not isinstance(contract, dict) or not contract.get("runtime_action"): + continue + + self.assertNotIn("body_placeholder", contract) + self.assertIn("follow_up_on_fail", contract.get("effects", {})) + + if contract["effects"]["follow_up_on_fail"]: + true_actions.add(contract["runtime_action"]) + + self.assertEqual( + true_actions, + {"SAVE_ACTIVE_MEMORY", "UPDATE_ACTIVE_MEMORY"}, + ) + + def test_closed_invalid_update_marker_is_fully_hidden(self): + failed_payload = ( + '{"active_memory_id":"zgctxy","field_name":"last_photo_id","VALUE":"hpfgfn"}\n' + '{"active_memory_id":"zgctxy","field_name":"current_photos","VALUE":"3"}' + ) + source = ( + "before\n" + "\n" + f"{failed_payload}\n" + "\n" + "after" + ) + + result = extract_runtime_actions( + source, + enabled_actions=["CAN_SAVE_ACTIVE_MEMORY"], + ) + + self.assertEqual(result.text, "before\nafter") + self.assertEqual(len(result.actions), 1) + self.assertEqual(result.actions[0].name, "UPDATE_ACTIVE_MEMORY") + self.assertEqual(result.actions[0].payload, failed_payload) + self.assertNotIn("UPDATE_ACTIVE_MEMORY", result.text) + self.assertNotIn("active_memory_id", result.text) + + def test_failure_followup_is_contract_controlled(self): + self.assertTrue(action_event_requires_follow_up({ + "name": "save_active_memory", + "status": "failed", + })) + self.assertTrue(action_event_requires_follow_up({ + "name": "update_active_memory", + "status": "failed", + })) + self.assertFalse(action_event_requires_follow_up({ + "name": "web_search", + "status": "failed", + })) + + async def test_invalid_update_gets_failed_event_and_retry_context(self): + emitter = _Emitter() + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=_Logger(), + clients={}, + ) + context.runtime_current_turn_id = "turn-1" + context.runtime_current_sequence_turn_id = "turn-1" + context.runtime_turn_user_message = "update photo state" + failed_payload = ( + '{"active_memory_id":"zgctxy","field_name":"last_photo_id","VALUE":"hpfgfn"}\n' + '{"active_memory_id":"zgctxy","field_name":"current_photos","VALUE":"3"}' + ) + + await apply_runtime_action_calls( + context, + [RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=failed_payload, + )], + assistant_message="", + ) + + event = context.runtime_action_events[-1] + self.assertEqual(event["status"], "failed") + self.assertEqual(event["error"], "invalid_update_active_memory_payload") + self.assertEqual(event["failure_reason"], "invalid payload") + self.assertEqual(event["failed_marker_payload"], failed_payload) + self.assertTrue(action_event_requires_follow_up(event)) + self.assertTrue(emitter.items[-1]["close_tag"]) + self.assertNotIn("payload", emitter.items[-1]) + + tool_results_context = build_tool_results_context(context) + self.assertIn('name="UPDATE_ACTIVE_MEMORY"', tool_results_context) + self.assertIn("invalid_update_active_memory_payload", tool_results_context) + base_prompt = tool_results_context + "\n\nBASE RULES" + prompt = BrainNode.build_followup_system_prompt( + base_prompt, + "update photo state", + context=context, + latest_action="UPDATE_ACTIVE_MEMORY", + ) + + self.assertTrue(prompt.startswith(FOLLOWUP_SYSTEM_MESSAGE)) + mandatory = prompt.index("") + failed = prompt.index("") + tools = prompt.index("") + self.assertLess(mandatory, failed) + self.assertLess(failed, tools) + self.assertIn("Use to update active memory values.", prompt) + self.assertIn( + "\n" + "\n" + f"{failed_payload}\n" + "\n" + "", + prompt, + ) + + context.runtime_action_events.append({ + "name": "update_active_memory", + "status": "completed", + "runtime_turn_id": "turn-1", + }) + second_prompt = BrainNode.build_followup_system_prompt( + base_prompt, + "update photo state", + context=context, + latest_action="UPDATE_ACTIVE_MEMORY", + ) + self.assertNotIn("", second_prompt) + + async def test_update_wrong_id_and_field_use_existing_failures(self): + active_record = ( + "active_memory_1: test " + "[ active_memory_id: abc123 ] " + "[ conditions: test ] " + "[ current_photos: 2 ] " + "[ status: pending ]" + ) + cases = ( + ( + '{"active_memory_id":"zzzzzz","current_photos":"3"}', + "active_memory_not_found", + "incorrect id", + ), + ( + '{"active_memory_id":"abc123","wrong_field":"3"}', + "active_memory_field_not_declared", + "unknown field: wrong_field", + ), + ) + + for payload, error, reason in cases: + with self.subTest(error=error): + context = RuntimeContext( + websocket=None, + emitter=_Emitter(), + logger=_Logger(), + clients={}, + ) + context.runtime_current_turn_id = "turn-1" + context.runtime_current_sequence_turn_id = "turn-1" + context.active_memory_records = [active_record] + + await apply_runtime_action_calls( + context, + [RuntimeActionCall( + name="UPDATE_ACTIVE_MEMORY", + payload=payload, + )], + assistant_message="", + ) + + event = context.runtime_action_events[-1] + self.assertEqual(event["status"], "failed") + self.assertEqual(event["error"], error) + self.assertEqual(event["failure_reason"], reason) + self.assertTrue(action_event_requires_follow_up(event)) + + async def test_invalid_save_gets_failed_event(self): + emitter = _Emitter() + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=_Logger(), + clients={}, + ) + context.runtime_current_turn_id = "turn-1" + context.runtime_current_sequence_turn_id = "turn-1" + + await apply_runtime_action_calls( + context, + [RuntimeActionCall( + name="SAVE_ACTIVE_MEMORY", + payload='{"wrong_field":"value"}', + )], + assistant_message="", + ) + + event = context.runtime_action_events[-1] + self.assertEqual(event["status"], "failed") + self.assertEqual(event["failure_reason"], "invalid payload") + self.assertTrue(action_event_requires_follow_up(event)) + + def test_old_turn_failed_event_is_not_replayed(self): + context = RuntimeContext( + websocket=None, + emitter=_Emitter(), + logger=_Logger(), + clients={}, + ) + context.runtime_current_turn_id = "turn-2" + context.runtime_current_sequence_turn_id = "turn-2" + context.runtime_action_events.append({ + "name": "update_active_memory", + "status": "failed", + "runtime_turn_id": "turn-1", + "failure_reason": "incorrect id", + "payload": '{"active_memory_id":"abcdef","counter":"2"}', + }) + + prompt = BrainNode.build_followup_system_prompt( + "BASE RULES", + "new request", + context=context, + latest_action="UPDATE_ACTIVE_MEMORY", + ) + + self.assertNotIn("", prompt) diff --git a/tests/test_l4_fact_age_client_contract.py b/tests/test_l4_fact_age_client_contract.py index 0ddb3dcf..271ee56a 100644 --- a/tests/test_l4_fact_age_client_contract.py +++ b/tests/test_l4_fact_age_client_contract.py @@ -16,7 +16,7 @@ class L4FactAgeClientContractTests(unittest.TestCase): - def test_l4_rows_show_live_context_age_after_display_value(self): + def test_l4_rows_show_live_context_age_in_header(self): source = MEMORY_VIEW.read_text(encoding="utf-8") self.assertIn( @@ -36,10 +36,18 @@ def test_l4_rows_show_live_context_age_after_display_value(self): source, ) self.assertIn( - 'ageSpan.className =\n "runtime-memory-l4-age";', + 'ageSpan.className =\n "runtime-memory-l4-age";', source, ) self.assertIn( + 'longTermHeader.className =\n "runtime-memory-l4-header";', + source, + ) + self.assertIn( + "longTermHeader.appendChild(ageSpan);", + source, + ) + self.assertNotIn( "valueSpan.appendChild(ageSpan);", source, ) @@ -48,15 +56,53 @@ def test_l4_age_uses_same_bucket_format_as_brain_context(self): source = MEMORY_VIEW.read_text(encoding="utf-8") self.assertIn("Math.max(\n 1,", source) - self.assertIn("` ( ${seconds}s ago )`", source) - self.assertIn("` ( ${minutes}m ago )`", source) - self.assertIn("` ( ${hours}h ago )`", source) - self.assertIn("` ( ${days}d ago )`", source) + self.assertIn("`${seconds}s ago`", source) + self.assertIn("`${minutes}m ago`", source) + self.assertIn("`${hours}h ago`", source) + self.assertIn("`${days}d ago`", source) self.assertIn( "window.setInterval(\n refreshLongTermMemoryFactAges,\n 1000", source, ) + def test_l4_rows_use_fixed_header_and_separate_value_line(self): + source = MEMORY_VIEW.read_text(encoding="utf-8") + css = ( + ROOT + / "ui" + / "static" + / "css" + / "runtime-memory.css" + ).read_text(encoding="utf-8") + + self.assertIn( + 'longTermHeader.className =\n "runtime-memory-l4-header";', + source, + ) + self.assertIn( + "longTermHeader.appendChild(keySpan);", + source, + ) + self.assertIn( + "row.appendChild(longTermHeader);", + source, + ) + self.assertIn( + "? valuePresentation.text", + source, + ) + self.assertIn( + ".runtime-memory-l4-row .runtime-memory-key {", + css, + ) + self.assertIn("text-overflow: ellipsis;", css) + self.assertIn("white-space: nowrap;", css) + self.assertIn( + ".runtime-memory-l4-row .runtime-memory-value {", + css, + ) + self.assertIn("display: block;", css) + def test_runtime_memory_view_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") @@ -66,6 +112,7 @@ def test_runtime_memory_view_cache_version_is_bumped(self): "&l4-fact-age=1", source, ) + self.assertIn("&l4-two-line-layout=1", source) if __name__ == "__main__": diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 42598df5..0bdfbf78 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -32,6 +32,7 @@ format_l4_fact_line, format_l4_merge_operation_details, format_long_term_memory_context, + inspect_l4_merge_shard_scan, mark_facts_memory_fields_analyzed, merge_l4_store_snapshots, normalize_facts_memory_records, @@ -885,6 +886,260 @@ async def test_merge_uses_two_existing_fact_shards_before_applying(self): self.assertEqual(len(second_payload["existing_facts"]), 25) self.assertEqual(len(second_payload["previous_shard_scan"]), 3) + def test_shard_scan_inspection_keeps_valid_rows_when_one_row_is_bad(self): + inspection = inspect_l4_merge_shard_scan( + { + "scan": [ + { + "pending_id": "PF1", + "decision": "update", + "fact_ids": [], + }, + { + "pending_id": "PF2", + "decision": "no_match", + "fact_ids": [], + }, + ], + }, + pending_ids=["PF1", "PF2"], + visible_fact_ids=["F1", "F2"], + ) + + self.assertEqual(inspection["valid_pending_ids"], ["PF2"]) + self.assertEqual(inspection["invalid_pending_ids"], ["PF1"]) + self.assertEqual( + inspection["pending_errors"]["PF1"], + ["update_requires_one_fact_id"], + ) + + async def test_invalid_shard_row_gets_real_repair_before_finalize(self): + existing_facts = normalize_l4_store({ + "facts": [ + { + "id": f"F{index + 1}", + "key": f"project.fact_{index}", + "value": "Durable project fact " + ("detail " * 20), + "category": "project_fact", + } + for index in range(50) + ], + })["facts"] + store, _ = add_l4_pending_candidates( + normalize_l4_store({"facts": existing_facts}), + [ + { + "key": "project.pending_1", + "value": "First pending durable fact.", + "category": "project_fact", + }, + { + "key": "project.pending_2", + "value": "Second pending durable fact.", + "category": "project_fact", + }, + ], + now="2026-08-21T12:00:00Z", + ) + initial_scan = json.dumps({ + "scan": [ + { + "pending_id": "PF1", + "decision": "update", + "fact_ids": [], + }, + { + "pending_id": "PF2", + "decision": "no_match", + "fact_ids": [], + }, + ], + }) + repaired_scan = json.dumps({ + "scan": [ + { + "pending_id": "PF1", + "decision": "no_match", + "fact_ids": [], + }, + ], + }) + merge_response = json.dumps({ + "operations": [ + {"action": "ignore", "pending_id": "PF1"}, + {"action": "ignore", "pending_id": "PF2"}, + ], + }) + service_client = FakeServiceClient( + [initial_scan, repaired_scan, merge_response], + context_window=4096, + ) + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={"service": service_client}, + ) + context.runtime_long_term_memory_store = store + + result = await run_l4_merge_phase( + context=context, + service_client=service_client, + ) + + self.assertEqual(result["status"], "completed") + self.assertEqual(len(service_client.calls), 3) + recovery = result["merge_change"]["shard_scan_recovery"] + self.assertTrue(recovery["repair_attempted"]) + self.assertEqual(recovery["initial_invalid_pending_ids"], ["PF1"]) + self.assertEqual(recovery["repaired_pending_ids"], ["PF1"]) + self.assertNotIn("PF1", context.runtime_l4_merge_deferred_pending_until) + repair_payload = json.loads( + service_client.calls[1]["user_prompt"].split("\n\n", 1)[1] + ) + self.assertEqual( + [fact["id"] for fact in repair_payload["pending_facts"]], + ["PF1"], + ) + self.assertIn("repair", repair_payload) + + async def test_bad_shard_row_is_deferred_without_blocking_valid_pending(self): + existing_facts = normalize_l4_store({ + "facts": [ + { + "id": f"F{index + 1}", + "key": f"project.fact_{index}", + "value": "Durable project fact " + ("detail " * 20), + "category": "project_fact", + } + for index in range(50) + ], + })["facts"] + store, _ = add_l4_pending_candidates( + normalize_l4_store({"facts": existing_facts}), + [ + { + "key": "project.poison", + "value": "Difficult pending durable fact.", + "category": "project_fact", + }, + { + "key": "project.good", + "value": "Independent pending durable fact.", + "category": "project_fact", + }, + ], + now="2026-08-21T12:00:00Z", + ) + initial_scan = json.dumps({ + "scan": [ + { + "pending_id": "PF1", + "decision": "update", + "fact_ids": [], + }, + { + "pending_id": "PF2", + "decision": "no_match", + "fact_ids": [], + }, + ], + }) + broken_repair = json.dumps({"scan": []}) + merge_good = json.dumps({ + "operations": [ + {"action": "ignore", "pending_id": "PF2"}, + ], + }) + retry_scan = json.dumps({ + "scan": [ + { + "pending_id": "PF1", + "decision": "no_match", + "fact_ids": [], + }, + ], + }) + retry_merge = json.dumps({ + "operations": [ + {"action": "ignore", "pending_id": "PF1"}, + ], + }) + service_client = FakeServiceClient( + [ + initial_scan, + broken_repair, + merge_good, + retry_scan, + retry_merge, + ], + context_window=4096, + ) + context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={"service": service_client}, + ) + context.runtime_long_term_memory_store = store + + first = await run_l4_merge_phase( + context=context, + service_client=service_client, + ) + + self.assertEqual(first["status"], "completed") + self.assertEqual(first["batch_count"], 1) + self.assertEqual(len(service_client.calls), 3) + self.assertEqual( + [fact["id"] for fact in context.runtime_long_term_memory_store["pending_facts"]], + ["PF1"], + ) + self.assertIn("PF1", context.runtime_l4_merge_deferred_pending_until) + self.assertIn("PF1", context.runtime_l4_merge_single_retry_pending_ids) + self.assertEqual(context.runtime_l4_merge_retry_not_before, 0.0) + final_payload = json.loads( + service_client.calls[2]["user_prompt"].split("\n\n", 1)[1] + ) + self.assertEqual( + [fact["id"] for fact in final_payload["pending_facts"]], + ["PF2"], + ) + + next_store, _ = add_l4_pending_candidates( + context.runtime_long_term_memory_store, + [ + { + "key": "project.later", + "value": "Later independent fact.", + "category": "project_fact", + }, + ], + now="2026-08-21T12:01:00Z", + ) + context.runtime_long_term_memory_store = next_store + context.runtime_l4_merge_deferred_pending_until["PF1"] = 0.0 + + second = await run_l4_merge_phase( + context=context, + service_client=service_client, + ) + + self.assertEqual(second["status"], "completed") + self.assertEqual(len(service_client.calls), 5) + retry_payload = json.loads( + service_client.calls[3]["user_prompt"].split("\n\n", 1)[1] + ) + self.assertEqual( + [fact["id"] for fact in retry_payload["pending_facts"]], + ["PF1"], + ) + self.assertEqual( + [fact["id"] for fact in context.runtime_long_term_memory_store["pending_facts"]], + ["PF3"], + ) + self.assertNotIn("PF1", context.runtime_l4_merge_single_retry_pending_ids) + async def test_l4_pause_logs_once_and_starts_no_model_request(self): existing_facts = normalize_l4_store({ "facts": [ diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index fa529f66..76ad425e 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -29,7 +29,7 @@ def test_analyzed_facts_are_excluded_from_facts_memory_mode(self): source, ) self.assertIn( - 'if (getFactsMemoryFieldRecords().length > 0)', + 'if (hasFactsMemoryFieldRecords())', source, ) diff --git a/tests/test_runtime_memory_hover_title_client_contract.py b/tests/test_runtime_memory_hover_title_client_contract.py index 913884f0..b7909574 100644 --- a/tests/test_runtime_memory_hover_title_client_contract.py +++ b/tests/test_runtime_memory_hover_title_client_contract.py @@ -49,15 +49,19 @@ def test_runtime_memory_view_does_not_prepopulate_native_titles(self): 1, ) - def test_rows_and_values_bind_hover_only_titles(self): + def test_row_hover_title_is_formatted_only_on_first_hover(self): source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + self.assertIn("let hoverTitle = null;", source) + self.assertIn("if (hoverTitle === null) {", source) self.assertIn( - "bindRuntimeMemoryHoverTitle(\n row,\n hoverTitle", + "formatRuntimeMemoryHoverTitle(", source, ) - self.assertIn( - "bindRuntimeMemoryHoverTitle(\n valueSpan,\n hoverTitle", + self.assertIn("row,", source) + self.assertIn("() => {", source) + self.assertNotIn( + "bindRuntimeMemoryHoverTitle(\n valueSpan,", source, ) diff --git a/tests/test_runtime_memory_l4_render_perf_client_contract.py b/tests/test_runtime_memory_l4_render_perf_client_contract.py new file mode 100644 index 00000000..ce509a99 --- /dev/null +++ b/tests/test_runtime_memory_l4_render_perf_client_contract.py @@ -0,0 +1,143 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-memory-view.js" +) + + +def function_block(source, start_marker, end_marker): + start = source.index(start_marker) + end = source.index(end_marker, start) + return source[start:end] + + +class RuntimeMemoryL4RenderPerfClientContractTests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + def test_l4_line_objects_are_built_only_when_lazy_batch_materializes(self): + block = function_block( + self.source, + "function renderLongTermMemoryFacts()", + "function renderActiveMemoryRecords()", + ) + + self.assertIn("appendRuntimeMemoryLineRows(", block) + self.assertIn("records,", block) + self.assertIn( + "buildLine: fact => buildLongTermMemoryLine(", + block, + ) + self.assertNotIn("records.map(", block) + + def test_delayed_report_links_are_indexed_once_per_l4_render(self): + block = function_block( + self.source, + "function renderLongTermMemoryFacts()", + "function renderActiveMemoryRecords()", + ) + + self.assertIn( + "buildDelayedMemoryFactReportIndex(", + block, + ) + self.assertIn("delayedReports", block) + self.assertIn( + "delayedReportByFactId instanceof Map", + self.source, + ) + self.assertIn( + "delayedReportByFactId.get(normalizedFactId)", + self.source, + ) + + def test_l4_metrics_are_lazy_and_do_not_build_a_giant_joined_string(self): + block = function_block( + self.source, + "function renderLongTermMemoryFacts()", + "function renderActiveMemoryRecords()", + ) + metrics = function_block( + self.source, + "function updateRuntimeMemoryTitleMetricsFromItems(", + "function clampRuntimeMemoryHistoryIndex()", + ) + + self.assertIn("updateRuntimeMemoryTitleMetricsFromItems(", block) + self.assertNotIn('.join("\\n")', block) + self.assertIn("let cachedTitle = null;", metrics) + self.assertIn("bindRuntimeMemoryHoverTitle(", metrics) + self.assertNotIn("Array.from(metricText)", self.source) + + def test_mode_availability_uses_cheap_presence_checks(self): + block = function_block( + self.source, + "function getAvailableRuntimeMemoryDisplayModes()", + "function ensureRuntimeMemoryDisplayModeAvailable(", + ) + + self.assertIn("hasLongTermMemoryFactRecords()", block) + self.assertNotIn("getLongTermMemoryFactRecords().length", block) + self.assertIn( + "Array.isArray(options.availableModes)", + self.source, + ) + self.assertIn("availableModes: modes,", self.source) + + def test_highlight_pipeline_reuses_one_rendered_row_collection(self): + reference_block = function_block( + self.source, + "function applyMemoryReferenceHighlights(options = {})", + "function shouldReduceRuntimeMemoryMotion()", + ) + citation_block = function_block( + self.source, + "function applyThinkMemoryCitationHighlights(options = {})", + "function handleThinkMemoryCitationHighlight(event)", + ) + sort_block = function_block( + self.source, + "function sortHighlightedMemoryRows(options = {})", + "function getActiveThinkMemoryCitationIdentitySets()", + ) + + self.assertIn("Array.isArray(options.rows)", reference_block) + self.assertIn("rows,", reference_block) + self.assertIn("Array.isArray(options.rows)", citation_block) + self.assertIn("rows,", citation_block) + self.assertIn("Array.isArray(options.rows)", sort_block) + self.assertNotIn( + "rows.forEach(\n syncLongTermMemoryRowValueDisplay", + sort_block, + ) + self.assertIn( + "if (!options.interactiveLongTermMemory) {", + self.source, + ) + + def test_l4_value_sync_skips_unchanged_dom_text(self): + block = function_block( + self.source, + "function syncLongTermMemoryRowValueDisplay(row)", + "function clearRuntimeMemoryHighlightClasses()", + ) + + self.assertIn("valueSpan.firstChild", block) + self.assertIn( + "if (valueTextNode.nodeValue !== nextValue)", + block, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_memory_lazy_rows_client_contract.py b/tests/test_runtime_memory_lazy_rows_client_contract.py index 12d898d0..7f6d8c13 100644 --- a/tests/test_runtime_memory_lazy_rows_client_contract.py +++ b/tests/test_runtime_memory_lazy_rows_client_contract.py @@ -16,31 +16,39 @@ class RuntimeMemoryLazyRowsClientContractTests(unittest.TestCase): - def test_memory_panels_materialize_rows_in_batches_of_forty(self): + def test_memory_panels_use_independent_lazy_batch_sizes(self): source = MEMORY_VIEW_JS.read_text(encoding="utf-8") - self.assertIn( - "const RUNTIME_MEMORY_LAZY_BATCH_SIZE = 40;", - source, + expected_constants = ( + "const ACTIVE_MEMORY_LAZY_BATCH_SIZE = 50;", + "const DELAYED_MEMORY_LAZY_BATCH_SIZE = 50;", + "const FACTS_MEMORY_LAZY_BATCH_SIZE = 50;", + "const LONG_TERM_MEMORY_LAZY_BATCH_SIZE = 20;", + "const FILES_MEMORY_LAZY_BATCH_SIZE = 50;", ) + + for declaration in expected_constants: + self.assertIn(declaration, source) + self.assertIn( - "function beginRuntimeMemoryLazyCollection(items, renderItem)", + "function getRuntimeMemoryLazyBatchSize(displayMode)", source, ) self.assertIn( - "for (let index = start; index < end; index += 1)", + "getRuntimeMemoryLazyBatchSize(runtimeMemoryLazyMode)", source, ) - self.assertGreaterEqual( - source.count("beginRuntimeMemoryLazyCollection("), - 4, + self.assertIn("start + batchSize", source) + self.assertNotIn( + "const RUNTIME_MEMORY_LAZY_BATCH_SIZE =", + source, ) def test_lazy_rows_are_not_precreated_and_hidden_in_dom(self): source = MEMORY_VIEW_JS.read_text(encoding="utf-8") self.assertNotIn( - 'row.style.display =\n index < runtimeMemoryVisibleRowCount', + "runtimeMemoryVisibleRowCount", source, ) self.assertIn( @@ -64,10 +72,23 @@ def test_next_batch_loads_only_near_panel_bottom(self): source, ) - def test_memory_view_cache_key_is_bumped_for_lazy_materialization(self): + def test_native_scroll_path_is_not_debounced_after_downward_progress(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + start = source.index("function handleRuntimeMemoryLazyScroll()") + end = source.index("function handleRuntimeMemoryLazyWheel(event)", start) + scroll_handler = source[start:end] + + self.assertIn("if (!scrollingDown)", scroll_handler) + self.assertNotIn("runtimeMemoryLastBatchRevealAt", scroll_handler) + self.assertIn( + "remaining <= RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX", + scroll_handler, + ) + + def test_memory_view_cache_key_is_bumped_for_l4_lazy_optimization(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("lazy-rows=2", source) + self.assertIn("lazy-rows=4", source) if __name__ == "__main__": diff --git a/tests/test_runtime_memory_row_payload_client_contract.py b/tests/test_runtime_memory_row_payload_client_contract.py index db3306cc..74e85f86 100644 --- a/tests/test_runtime_memory_row_payload_client_contract.py +++ b/tests/test_runtime_memory_row_payload_client_contract.py @@ -80,7 +80,11 @@ def test_hover_and_highlight_payload_still_uses_same_semantics(self): source, ) self.assertIn( - ").filter(row => getMemoryReferenceAliases(row).length);", + "getMemoryReferenceAliases(row).length", + source, + ) + self.assertIn( + '!row.classList.contains("runtime-memory-l4-row")', source, ) self.assertIn( @@ -88,7 +92,11 @@ def test_hover_and_highlight_payload_still_uses_same_semantics(self): source, ) self.assertIn( - "bindRuntimeMemoryHoverTitle(\n row,\n hoverTitle", + "let hoverTitle = null;", + source, + ) + self.assertIn( + "formatRuntimeMemoryHoverTitle(", source, ) diff --git a/tests/test_runtime_panel_client_contract.py b/tests/test_runtime_panel_client_contract.py index deaf46cc..b813943a 100644 --- a/tests/test_runtime_panel_client_contract.py +++ b/tests/test_runtime_panel_client_contract.py @@ -48,6 +48,30 @@ def test_runtime_counter_uses_live_total_during_reasoning(self): source, ) + def test_context_bar_reflows_when_runtime_panel_width_changes(self): + source = RUNTIME_PANEL_JS.read_text(encoding="utf-8") + + self.assertIn( + "let contextPanelResizeObserver = null;", + source, + ) + self.assertIn( + 'typeof window.ResizeObserver === "function"', + source, + ) + self.assertIn( + "contextPanelResizeObserver.observe(", + source, + ) + self.assertIn( + "contextRuntimePanel", + source, + ) + self.assertIn( + "scheduleRuntimeTelemetryFrame();", + source, + ) + def test_submit_focuses_brain_context_tab(self): panel_source = RUNTIME_PANEL_JS.read_text(encoding="utf-8") input_source = SOCKET_INPUT_JS.read_text(encoding="utf-8") @@ -174,7 +198,7 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/js/runtime/runtime-panel.js?v=live-reasoning-tokens-1", + "/static/js/runtime/runtime-panel.js?v=context-bar-resize-1", source, ) self.assertIn( diff --git a/tests/test_update_active_memory_highlight_client_contract.py b/tests/test_update_active_memory_highlight_client_contract.py new file mode 100644 index 00000000..71072d7c --- /dev/null +++ b/tests/test_update_active_memory_highlight_client_contract.py @@ -0,0 +1,58 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_ACTIONS = ROOT / "ui" / "static" / "js" / "socket" / "runtime-actions.js" +INPUT = ROOT / "ui" / "static" / "js" / "socket" / "input.js" +CHAT = ROOT / "ui" / "static" / "js" / "chat.js" +MEMORY_VIEW = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +AVATAR = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" +INDEX = ROOT / "ui" / "templates" / "index.html" + + +class UpdateActiveMemoryHighlightClientContractTests(unittest.TestCase): + def test_successful_update_reuses_existing_citation_highlight_by_stable_id(self): + source = RUNTIME_ACTIONS.read_text(encoding="utf-8") + + self.assertIn( + '"jin:think-runtime-citation-highlight"', + source, + ) + self.assertIn( + "function highlightUpdatedActiveMemory(activeMemoryId)", + source, + ) + self.assertIn( + "sourceId: `runtime-action:update-active-memory:${normalizedId}`", + source, + ) + self.assertIn("activeMemoryIds: [normalizedId]", source) + self.assertIn( + "window.JinRuntime.runtime.replaceActiveMemoryRecordById(\n activeMemoryId,", + source, + ) + self.assertIn( + "highlightUpdatedActiveMemory(\n activeMemoryId", + source, + ) + + def test_existing_user_send_clear_turns_off_update_highlight_everywhere(self): + input_source = INPUT.read_text(encoding="utf-8") + chat = CHAT.read_text(encoding="utf-8") + memory_view = MEMORY_VIEW.read_text(encoding="utf-8") + avatar = AVATAR.read_text(encoding="utf-8") + + self.assertIn("window.clearLatestJinMemoryReferenceText();", input_source) + self.assertIn('dispatchJinMemoryReferenceHighlight(\n "persistent",\n "",\n false', chat) + self.assertIn("activeThinkMemoryCitationSources.clear();", memory_view) + self.assertIn("activeThinkRuntimeCitationSources.clear();", avatar) + + def test_runtime_action_cache_key_includes_update_highlight_revision(self): + source = INDEX.read_text(encoding="utf-8") + + self.assertIn("active-memory-update-highlight=1", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 55652086..ae268d83 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -122,8 +122,17 @@ text-shadow: none !important; } +.runtime-memory-l4-row .runtime-memory-l4-header { + display: flex; + align-items: baseline; + width: 100%; + min-width: 0; + white-space: nowrap; +} + .runtime-memory-l4-row .runtime-memory-fact-number { display: inline-block; + flex: 0 0 auto; min-width: 3ch; color: rgba(161, 161, 170, 0.68); font-variant-numeric: tabular-nums; @@ -149,11 +158,41 @@ .runtime-memory-l4-row .runtime-memory-fact-separator { display: inline-block; + flex: 0 0 1.4em; width: 1.4em; color: rgba(161, 161, 170, 0.48); text-align: center; } +.runtime-memory-l4-row .runtime-memory-key { + display: block; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.runtime-memory-l4-row .runtime-memory-l4-age { + display: block; + flex: 0 0 auto; + color: rgba(161, 161, 170, 0.58); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.runtime-memory-l4-row .runtime-memory-value { + display: block; + width: 100%; +} + +.runtime-memory-l4-row:not(.runtime-memory-reference-hit):not(.runtime-memory-citation-hit):not(.runtime-memory-context-loaded-hit) .runtime-memory-value { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + .runtime-memory-key, .runtime-memory-value { color: rgba(244,244,245,0.62); diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 99aec936..e64db9ca 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -128,7 +128,14 @@ ? memoryPanel.querySelector(".memory-scroll") : null; - const RUNTIME_MEMORY_LAZY_BATCH_SIZE = 40; + // Per-panel lazy materialization knobs. Keep these together so the UI + // page size can be tuned without touching the render pipeline. + const ACTIVE_MEMORY_LAZY_BATCH_SIZE = 50; + const DELAYED_MEMORY_LAZY_BATCH_SIZE = 50; + const FACTS_MEMORY_LAZY_BATCH_SIZE = 50; + const LONG_TERM_MEMORY_LAZY_BATCH_SIZE = 20; + const LONG_TERM_MEMORY_VALUE_DISPLAY_MAX_CHARS = 78; + const FILES_MEMORY_LAZY_BATCH_SIZE = 50; const RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX = 160; let runtimeMemoryLazyMode = ""; @@ -170,6 +177,35 @@ } } + function getRuntimeMemoryLazyBatchSize(displayMode) { + const mode = + String(displayMode || getRuntimeMemoryDisplayMode()).trim(); + + if (mode === "long_term") { + return LONG_TERM_MEMORY_LAZY_BATCH_SIZE; + } + + if (mode === "active") { + return ACTIVE_MEMORY_LAZY_BATCH_SIZE; + } + + if (mode === "delayed") { + return DELAYED_MEMORY_LAZY_BATCH_SIZE; + } + + if (mode === "facts") { + return FACTS_MEMORY_LAZY_BATCH_SIZE; + } + + if (mode === "files") { + return FILES_MEMORY_LAZY_BATCH_SIZE; + } + + // Runtime snapshots are not one of the five alternate memory panels. + // Keep their materialization at the common default. + return 50; + } + function normalizeMemoryReferenceSearchText(value) { const raw = String(value || ""); @@ -476,6 +512,30 @@ ); } + function getLongTermMemoryFullValueText(line) { + return String( + memoryModel.splitMemoryMeta(line && line.value || "").text + || "" + ).replace(/\\n/g, " ↵ "); + } + + function truncateLongTermMemoryValueForDisplay(value) { + const chars = + Array.from(String(value || "")); + + if ( + chars.length + <= LONG_TERM_MEMORY_VALUE_DISPLAY_MAX_CHARS + ) { + return String(value || ""); + } + + return `${chars + .slice(0, LONG_TERM_MEMORY_VALUE_DISPLAY_MAX_CHARS) + .join("") + .trimEnd()}...`; + } + function syncLongTermMemoryRowValueDisplay(row) { if (!row || !row.classList.contains("runtime-memory-l4-row")) { return; @@ -485,8 +545,9 @@ row.querySelector(".runtime-memory-value"); const valueTextNode = valueSpan - ? Array.from(valueSpan.childNodes) - .find(node => node.nodeType === 3) + && valueSpan.firstChild + && valueSpan.firstChild.nodeType === 3 + ? valueSpan.firstChild : null; if (!valueTextNode) { @@ -502,13 +563,14 @@ state && state.runtimeMemoryValueFullText || defaultText ); + const nextValue = + isLongTermMemoryRowBubbled(row) + ? fullText + : defaultText; - valueTextNode.nodeValue = - ` ${ - isLongTermMemoryRowBubbled(row) - ? fullText - : defaultText - }`; + if (valueTextNode.nodeValue !== nextValue) { + valueTextNode.nodeValue = nextValue; + } } function clearRuntimeMemoryHighlightClasses() { @@ -525,12 +587,18 @@ ].join(", ") ) .forEach((row) => { + const wasBubbled = + isLongTermMemoryRowBubbled(row); + row.classList.remove( "runtime-memory-reference-hit", "runtime-memory-citation-hit", "runtime-memory-external-hover-hit" ); - syncLongTermMemoryRowValueDisplay(row); + + if (wasBubbled !== isLongTermMemoryRowBubbled(row)) { + syncLongTermMemoryRowValueDisplay(row); + } }); } @@ -571,6 +639,8 @@ function beginRuntimeMemoryLazyCollection(items, renderItem) { const source = Array.isArray(items) ? items : []; + const batchSize = + getRuntimeMemoryLazyBatchSize(runtimeMemoryLazyMode); clearRuntimeMemoryLazyCollection(); runtimeMemoryLazyTotalCount = source.length; @@ -579,7 +649,7 @@ const start = runtimeMemoryLazyRenderedCount; const end = Math.min( runtimeMemoryLazyTotalCount, - start + RUNTIME_MEMORY_LAZY_BATCH_SIZE + start + batchSize ); for (let index = start; index < end; index += 1) { @@ -653,10 +723,11 @@ const scrollingDown = currentScrollTop > runtimeMemoryLastScrollTop; runtimeMemoryLastScrollTop = currentScrollTop; - if ( - !scrollingDown - || Date.now() - runtimeMemoryLastBatchRevealAt < 80 - ) { + // Native middle-button autoscroll emits only `scroll` events and can + // stop at the current bottom. Do not debounce real downward movement: + // appending a batch moves the bottom away, so the distance check below + // already prevents duplicate reveals until the viewport catches up. + if (!scrollingDown) { return; } @@ -796,46 +867,73 @@ const sourceText = getActiveMemoryReferenceText(); - const rows = Array.from( - runtimeMemoryText.querySelectorAll( - ".runtime-memory-line:not(.runtime-memory-user-idle)" - ) - ).filter(row => getMemoryReferenceAliases(row).length); + const rows = + Array.isArray(options.rows) + ? options.rows + : Array.from( + runtimeMemoryText.querySelectorAll( + ".runtime-memory-line:not(.runtime-memory-user-idle)" + ) + ); + + // L4 is citation-gated. Keep it completely out of generic alias matching + // instead of building aliases only to reject the row afterwards. + const persistentRows = + rows.filter((row) => ( + !row.classList.contains("runtime-memory-l4-row") + && getMemoryReferenceAliases(row).length + )); const aliasUsage = - buildMemoryReferenceAliasUsage(rows); + sourceText + ? buildMemoryReferenceAliasUsage(persistentRows) + : new Map(); rows.forEach((row) => { - // L4 is citation-gated. Generic persistent answer/reasoning text may - // contain words from a durable fact value and must never make that fact - // jump in the panel. L4 focus is exposed only by an explicit structured - // reasoning citation (F-id / full fact key) handled below. - const persistentReferenceEligible = - !row.classList.contains("runtime-memory-l4-row"); - const matched = Boolean( - persistentReferenceEligible - && sourceText - && getMemoryReferenceAliases(row) - .some(alias => ( - Number( - aliasUsage.get( - normalizeMemoryReferenceSearchText(alias).trim() - ) || 0 - ) === 1 - && - containsMemoryReference( - sourceText, - alias - ) - )) - ); + if (!row.classList.contains("runtime-memory-l4-row")) { + return; + } - row.classList.toggle( - "runtime-memory-reference-hit", - matched - ); - }); + if (!row.classList.contains("runtime-memory-reference-hit")) { + return; + } + + const wasBubbled = + isLongTermMemoryRowBubbled(row); + + row.classList.remove("runtime-memory-reference-hit"); + + if (wasBubbled !== isLongTermMemoryRowBubbled(row)) { + syncLongTermMemoryRowValueDisplay(row); + } + }); + + persistentRows.forEach((row) => { + const matched = Boolean( + sourceText + && getMemoryReferenceAliases(row) + .some(alias => ( + Number( + aliasUsage.get( + normalizeMemoryReferenceSearchText(alias).trim() + ) || 0 + ) === 1 + && containsMemoryReference( + sourceText, + alias + ) + )) + ); - applyThinkMemoryCitationHighlights(options); + row.classList.toggle( + "runtime-memory-reference-hit", + matched + ); + }); + + applyThinkMemoryCitationHighlights({ + ...options, + rows, + }); } function shouldReduceRuntimeMemoryMotion() { @@ -997,15 +1095,14 @@ return; } - const rows = Array.from( - runtimeMemoryText.querySelectorAll( - ".runtime-memory-line:not(.runtime-memory-user-idle)" - ) - ); - - rows.forEach( - syncLongTermMemoryRowValueDisplay - ); + const rows = + Array.isArray(options.rows) + ? options.rows + : Array.from( + runtimeMemoryText.querySelectorAll( + ".runtime-memory-line:not(.runtime-memory-user-idle)" + ) + ); if (rows.length < 2) { applyRuntimeMemoryLazyVisibility(); @@ -1018,6 +1115,25 @@ } }); + const hasHighlightedRow = + rows.some(row => ( + row.classList.contains("runtime-memory-reference-hit") + || row.classList.contains("runtime-memory-citation-hit") + || row.classList.contains("runtime-memory-context-loaded-hit") + )); + const alreadyInSourceOrder = + rows.every((row, index) => ( + index === 0 + || Number( + rows[index - 1].dataset.memoryHighlightSortIndex || 0 + ) <= Number(row.dataset.memoryHighlightSortIndex || 0) + )); + + if (!hasHighlightedRow && alreadyInSourceOrder) { + applyRuntimeMemoryLazyVisibility(); + return; + } + const sortedRows = rows .slice() .sort((left, right) => { @@ -1112,11 +1228,15 @@ const activeIdentities = getActiveThinkMemoryCitationIdentitySets(); - const citationRows = Array.from( - runtimeMemoryText.querySelectorAll( - ".runtime-memory-line:not(.runtime-memory-user-idle)" - ) - ).filter((row) => { + const rows = + Array.isArray(options.rows) + ? options.rows + : Array.from( + runtimeMemoryText.querySelectorAll( + ".runtime-memory-line:not(.runtime-memory-user-idle)" + ) + ); + const citationRows = rows.filter((row) => { const citationState = getRuntimeMemoryRowCitationState(row); @@ -1177,13 +1297,23 @@ ? activeIdentities.lineIdentities.has(lineIdentity) : (exactTextMatch || uniqueKeyMatch); + const wasBubbled = + isLongTermMemoryRowBubbled(row); + row.classList.toggle( "runtime-memory-citation-hit", Boolean(matched) ); + + if (wasBubbled !== isLongTermMemoryRowBubbled(row)) { + syncLongTermMemoryRowValueDisplay(row); + } }); - sortHighlightedMemoryRows(options); + sortHighlightedMemoryRows({ + ...options, + rows, + }); } function handleThinkMemoryCitationHighlight(event) { @@ -1472,10 +1602,10 @@ return linkedReportIds; } - function getContextLoadedDelayedMemoryFactIds() { + function buildContextLoadedDelayedMemoryFactIds(reports) { const factIds = new Set(); - getDelayedMemoryReportRecords() + (Array.isArray(reports) ? reports : []) .filter(isDelayedMemoryReportInContext) .forEach((report) => { normalizeDelayedMemoryFactIds([ @@ -1489,6 +1619,12 @@ return factIds; } + function getContextLoadedDelayedMemoryFactIds() { + return buildContextLoadedDelayedMemoryFactIds( + getDelayedMemoryReportRecords() + ); + } + function reportReferencesLongTermFactId(report, factId) { const normalizedFactId = normalizeDelayedMemoryFactId(factId); @@ -1510,6 +1646,25 @@ ]).includes(normalizedFactId); } + function buildDelayedMemoryFactReportIndex(reports) { + const reportByFactId = new Map(); + + (Array.isArray(reports) ? reports : []).forEach((report) => { + normalizeDelayedMemoryFactIds([ + report.anchor_fact_ids, + report.facts_ids, + report.absorbed_fact_ids, + report.long_term_facts_ids, + ]).forEach((factId) => { + if (!reportByFactId.has(factId)) { + reportByFactId.set(factId, report); + } + }); + }); + + return reportByFactId; + } + function getDelayedMemoryReportForLongTermFactId(factId) { const normalizedFactId = normalizeDelayedMemoryFactId(factId); @@ -1651,7 +1806,7 @@ ); } - function formatLongTermFactAgeSuffix( + function formatLongTermFactAgeLabel( timestamp, now = Date.now() / 1000 ) { @@ -1666,21 +1821,21 @@ ); if (seconds < 60) { - return ` ( ${seconds}s ago )`; + return `${seconds}s ago`; } const minutes = Math.floor(seconds / 60); if (minutes < 60) { - return ` ( ${minutes}m ago )`; + return `${minutes}m ago`; } const hours = Math.floor(minutes / 60); if (hours < 24) { - return ` ( ${hours}h ago )`; + return `${hours}h ago`; } const days = Math.floor(hours / 24); - return ` ( ${days}d ago )`; + return `${days}d ago`; } function refreshLongTermMemoryFactAges() { @@ -1694,7 +1849,7 @@ .querySelectorAll("[data-l4-fact-age-timestamp]") .forEach((node) => { node.textContent = - formatLongTermFactAgeSuffix( + formatLongTermFactAgeLabel( node.dataset.l4FactAgeTimestamp, now ); @@ -1784,36 +1939,120 @@ }); } + function hasActiveMemoryRecords() { + const records = + typeof getActiveMemoryRecords === "function" + ? getActiveMemoryRecords() + : []; + + return Array.isArray(records) && records.length > 0; + } + + function hasDelayedMemoryReportRecords() { + const reports = + typeof getDelayedMemoryReports === "function" + ? getDelayedMemoryReports() + : {}; + + return Boolean( + reports + && typeof reports === "object" + && !Array.isArray(reports) + && Object.values(reports).some(report => ( + report + && typeof report === "object" + && !Array.isArray(report) + )) + ); + } + + function hasFactsMemoryFieldRecords() { + const fields = + typeof getFactsMemoryFields === "function" + ? getFactsMemoryFields() + : {}; + + return Boolean( + fields + && typeof fields === "object" + && !Array.isArray(fields) + && Object.values(fields).some((field) => ( + field + && typeof field === "object" + && !Array.isArray(field) + && String(field.content || "").trim() + && String(field.l4_status || "pending") + .trim() + .toLocaleLowerCase() !== "analyzed" + )) + ); + } + + function hasLongTermMemoryFactRecords() { + const facts = + typeof getLongTermMemoryFacts === "function" + ? getLongTermMemoryFacts() + : []; + + return Boolean( + Array.isArray(facts) + && facts.some(fact => ( + fact + && typeof fact === "object" + && !Array.isArray(fact) + && String(fact.key || "").trim() + && String(fact.value || "").trim() + )) + ); + } + + function hasPersistentFileRecords() { + const records = + window.JinFiles + && typeof window.JinFiles.getFiles === "function" + ? window.JinFiles.getFiles() + : []; + + return Boolean( + Array.isArray(records) + && records.some(record => ( + record + && record.id + && record.name + )) + ); + } + function getAvailableRuntimeMemoryDisplayModes() { const modes = [ "runtime", ]; - if (getActiveMemoryRecordTexts().length > 0) { + if (hasActiveMemoryRecords()) { modes.push( "active" ); } - if (getDelayedMemoryReportRecords().length > 0) { + if (hasDelayedMemoryReportRecords()) { modes.push( "delayed" ); } - if (getFactsMemoryFieldRecords().length > 0) { + if (hasFactsMemoryFieldRecords()) { modes.push( "facts" ); } - if (getLongTermMemoryFactRecords().length > 0) { + if (hasLongTermMemoryFactRecords()) { modes.push( "long_term" ); } - if (getPersistentFileRecords().length > 0) { + if (hasPersistentFileRecords()) { modes.push( "files" ); @@ -1822,9 +2061,11 @@ return modes; } - function ensureRuntimeMemoryDisplayModeAvailable() { + function ensureRuntimeMemoryDisplayModeAvailable(availableModes = null) { const modes = - getAvailableRuntimeMemoryDisplayModes(); + Array.isArray(availableModes) + ? availableModes + : getAvailableRuntimeMemoryDisplayModes(); const displayMode = getRuntimeMemoryDisplayMode(); @@ -1840,13 +2081,15 @@ return "runtime"; } - function updateRuntimeMemoryTitleState() { + function updateRuntimeMemoryTitleState(availableModes = null) { if (!runtimeMemoryTitle) { return; } const modes = - getAvailableRuntimeMemoryDisplayModes(); + Array.isArray(availableModes) + ? availableModes + : getAvailableRuntimeMemoryDisplayModes(); const currentMode = getRuntimeMemoryDisplayMode(); @@ -2163,16 +2406,40 @@ ); } - function estimateRuntimeMemoryTokens(text) { - if (!text) { - return 0; + function countRuntimeMemoryCharacters(text) { + let count = 0; + + for (const character of String(text || "")) { + void character; + count += 1; } - return Math.max( - 1, - Math.ceil( - Array.from(text).length / 4 - ) + return count; + } + + function estimateRuntimeMemoryTokensFromCharacterCount(charCount) { + const count = Math.max(0, Number(charCount || 0)); + + return count + ? Math.max(1, Math.ceil(count / 4)) + : 0; + } + + function bindRuntimeMemoryTitleMetrics(charCount) { + if (!runtimeMemoryTitle) { + return; + } + + const normalizedCharCount = + Math.max(0, Number(charCount || 0)); + const tokenCount = + estimateRuntimeMemoryTokensFromCharacterCount( + normalizedCharCount + ); + + bindRuntimeMemoryHoverTitle( + runtimeMemoryTitle, + `${normalizedCharCount} chars / ~${tokenCount} tokens` ); } @@ -2243,15 +2510,8 @@ const metricText = getRuntimeMemorySnapshotMetricText(snapshot); - const charCount = - Array.from(metricText).length; - - const tokenCount = - estimateRuntimeMemoryTokens(metricText); - - bindRuntimeMemoryHoverTitle( - runtimeMemoryTitle, - `${charCount} chars / ~${tokenCount} tokens` + bindRuntimeMemoryTitleMetrics( + countRuntimeMemoryCharacters(metricText) ); } @@ -2263,15 +2523,60 @@ const metricText = String(text || "").trim(); - const charCount = - Array.from(metricText).length; + bindRuntimeMemoryTitleMetrics( + countRuntimeMemoryCharacters(metricText) + ); + } - const tokenCount = - estimateRuntimeMemoryTokens(metricText); + function updateRuntimeMemoryTitleMetricsFromItems( + items, + resolveText + ) { + if (!runtimeMemoryTitle) { + return; + } + + const source = Array.isArray(items) ? items : []; + const resolver = + typeof resolveText === "function" + ? resolveText + : item => item; + let cachedTitle = null; bindRuntimeMemoryHoverTitle( runtimeMemoryTitle, - `${charCount} chars / ~${tokenCount} tokens` + () => { + if (cachedTitle !== null) { + return cachedTitle; + } + + let charCount = 0; + let hasMetricText = false; + + source.forEach((item, index) => { + const text = + String(resolver(item, index) || "").trim(); + + if (!text) { + return; + } + + if (hasMetricText) { + charCount += 1; + } + + charCount += countRuntimeMemoryCharacters(text); + hasMetricText = true; + }); + + const tokenCount = + estimateRuntimeMemoryTokensFromCharacterCount(charCount); + + cachedTitle = + `${charCount} chars / ~${tokenCount} tokens`; + + return cachedTitle; + } ); } @@ -2383,13 +2688,18 @@ pendingRuntimeMemoryRender = false; memoryHighlightsSuspended = false; - ensureRuntimeMemoryDisplayModeAvailable(); - updateRuntimeMemoryTitleState(); + const availableModes = + Array.isArray(options.availableModes) + ? options.availableModes + : getAvailableRuntimeMemoryDisplayModes(); + const displayMode = + ensureRuntimeMemoryDisplayModeAvailable(availableModes); + + updateRuntimeMemoryTitleState(availableModes); + const renderHighlightOptions = { animateSort: false, }; - const displayMode = - getRuntimeMemoryDisplayMode(); syncRuntimeMemoryLazyMode(displayMode); @@ -2455,7 +2765,6 @@ updateRuntimeMemoryTitleMetrics(null); updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); dispatchRuntimeAvatarSnapshot(null); applyMemoryReferenceHighlights(renderHighlightOptions); applyRuntimeMemoryLazyVisibility(); @@ -2498,7 +2807,6 @@ updateRuntimeMemoryTitleMetrics(snapshot); updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); dispatchRuntimeAvatarSnapshot(sourceSnapshot); applyMemoryReferenceHighlights(renderHighlightOptions); applyRuntimeMemoryLazyVisibility(); @@ -2981,7 +3289,21 @@ persistGlow = false, options = {} ) { - beginRuntimeMemoryLazyCollection(lines, (line, index) => { + const buildLine = + typeof options.buildLine === "function" + ? options.buildLine + : null; + + beginRuntimeMemoryLazyCollection(lines, (sourceLine, index) => { + const line = + buildLine + ? buildLine(sourceLine, index) + : sourceLine; + + if (!line) { + return; + } + const row = document.createElement("div"); @@ -3036,10 +3358,12 @@ ); } - setMemoryReferenceAliases( - row, - collectMemoryRecordReferenceAliases(line) - ); + if (!options.interactiveLongTermMemory) { + setMemoryReferenceAliases( + row, + collectMemoryRecordReferenceAliases(line) + ); + } row.addEventListener( "mouseenter", @@ -3066,16 +3390,23 @@ const valuePresentation = memoryModel.buildRuntimeMemoryValuePresentation(line); + const longTermMemoryFullValueText = + options.interactiveLongTermMemory + ? getLongTermMemoryFullValueText(line) + : ""; + const displayedValueText = + !options.interactiveLongTermMemory + ? valuePresentation.text + : truncateLongTermMemoryValueForDisplay( + longTermMemoryFullValueText + ); - if (String(valuePresentation.text || "").trim()) { + if (String(displayedValueText || "").trim()) { row.classList.add( "runtime-memory-kv-row" ); } - const fullRawLine = - `${key}: ${valuePresentation.raw}`; - const keyStatus = line.key_status || line.status || "same"; @@ -3084,18 +3415,27 @@ const keySpan = document.createElement("span"); + const displayKey = + memoryModel.runtimeMemoryDisplay.convertKeyToName(key) || key; + let longTermHeader = null; keySpan.className = "runtime-memory-key"; - keySpan.textContent = - `${memoryModel.runtimeMemoryDisplay.convertKeyToName(key) || key}:`; + options.interactiveLongTermMemory + ? displayKey + : `${displayKey}:`; if (options.interactiveLongTermMemory) { row.classList.add( "runtime-memory-l4-row" ); + longTermHeader = + document.createElement("div"); + longTermHeader.className = + "runtime-memory-l4-header"; + const factNumber = line && Number.isSafeInteger( line.fact_number @@ -3156,8 +3496,39 @@ separatorSpan.textContent = "·"; - row.appendChild(numberSpan); - row.appendChild(separatorSpan); + longTermHeader.appendChild(numberSpan); + longTermHeader.appendChild(separatorSpan); + } + + longTermHeader.appendChild(keySpan); + + if ( + Number.isFinite( + Number(line && line.context_age_timestamp) + ) + && Number(line.context_age_timestamp) > 0 + ) { + const separatorSpan = + document.createElement("span"); + const ageSpan = + document.createElement("span"); + + separatorSpan.className = + "runtime-memory-fact-separator"; + separatorSpan.textContent = + "·"; + + ageSpan.className = + "runtime-memory-l4-age"; + ageSpan.dataset.l4FactAgeTimestamp = + String(line.context_age_timestamp); + ageSpan.textContent = + formatLongTermFactAgeLabel( + line.context_age_timestamp + ); + + longTermHeader.appendChild(separatorSpan); + longTermHeader.appendChild(ageSpan); } } @@ -3168,20 +3539,22 @@ "runtime-memory-value"; valueSpan.textContent = - ` ${valuePresentation.text}`; + options.interactiveLongTermMemory + ? displayedValueText + : ` ${displayedValueText}`; if (options.interactiveLongTermMemory) { setRuntimeMemoryRowState( row, { runtimeMemoryValueDefaultText: - String(valuePresentation.text || ""), + String(displayedValueText || ""), runtimeMemoryValueFullText: String( - memoryModel.splitMemoryMeta(line.value || "").text - || valuePresentation.text + longTermMemoryFullValueText + || displayedValueText || "" - ).replace(/\\n/g, " ↵ "), + ), } ); } @@ -3191,44 +3564,33 @@ runtimeMemoryValueFontWeight(line) ); - if ( - options.interactiveLongTermMemory - && Number.isFinite( - Number(line && line.context_age_timestamp) - ) - && Number(line.context_age_timestamp) > 0 - ) { - const ageSpan = - document.createElement("span"); - - ageSpan.className = - "runtime-memory-l4-age"; - ageSpan.dataset.l4FactAgeTimestamp = - String(line.context_age_timestamp); - ageSpan.textContent = - formatLongTermFactAgeSuffix( - line.context_age_timestamp - ); - - valueSpan.appendChild(ageSpan); - } - - const hoverTitle = - formatRuntimeMemoryHoverTitle(fullRawLine); + let hoverTitle = null; bindRuntimeMemoryHoverTitle( row, - hoverTitle - ); - bindRuntimeMemoryHoverTitle( - valueSpan, - hoverTitle + () => { + if (hoverTitle === null) { + hoverTitle = + formatRuntimeMemoryHoverTitle( + `${key}: ${valuePresentation.raw}` + ); + } + + return hoverTitle; + } ); - row.appendChild(keySpan); + if (longTermHeader) { + row.appendChild(longTermHeader); + } else { + row.appendChild(keySpan); + } row.appendChild(valueSpan); - if (options.interactiveLongTermMemory) { + if ( + options.interactiveLongTermMemory + && line.context_loaded === true + ) { syncLongTermMemoryRowValueDisplay(row); } @@ -4296,7 +4658,6 @@ updateRuntimeMemoryTitleMetrics(null); updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); } function normalizeDelayedMemoryDisplayText(value) { @@ -7370,7 +7731,6 @@ updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); } @@ -7427,7 +7787,8 @@ function buildLongTermMemoryLine( fact, - contextLoadedFactIds = new Set() + contextLoadedFactIds = new Set(), + delayedReportByFactId = null ) { const id = @@ -7436,8 +7797,12 @@ String(fact.key || "").trim(); const value = String(fact.value || "").trim(); + const normalizedFactId = + normalizeDelayedMemoryFactId(id); const linkedDelayedMemoryReport = - getDelayedMemoryReportForLongTermFactId(id); + delayedReportByFactId instanceof Map + ? delayedReportByFactId.get(normalizedFactId) || null + : getDelayedMemoryReportForLongTermFactId(id); return { id, @@ -7480,16 +7845,17 @@ function renderLongTermMemoryFacts() { const records = getLongTermMemoryFactRecords(); - + const delayedReports = + records.length + ? getDelayedMemoryReportRecords() + : []; const contextLoadedFactIds = - getContextLoadedDelayedMemoryFactIds(); - - const lines = - records.map( - fact => buildLongTermMemoryLine( - fact, - contextLoadedFactIds - ) + buildContextLoadedDelayedMemoryFactIds( + delayedReports + ); + const delayedReportByFactId = + buildDelayedMemoryFactReportIndex( + delayedReports ); if (runtimeMemoryText) { @@ -7501,16 +7867,21 @@ "title" ); - if (!lines.length) { + if (!records.length) { runtimeMemoryText.textContent = "No long-term facts stored."; } else { appendRuntimeMemoryLineRows( - lines, + records, false, { applyFlash: false, interactiveLongTermMemory: true, + buildLine: fact => buildLongTermMemoryLine( + fact, + contextLoadedFactIds, + delayedReportByFactId + ), } ); } @@ -7524,15 +7895,23 @@ userIdleValueNode = null; idle.stop(); - updateRuntimeMemoryTitleMetricsFromText( - lines - .map(line => `${line.key}: ${line.value}`) - .join("\n") + updateRuntimeMemoryTitleMetricsFromItems( + records, + (fact) => { + const key = + String(fact && fact.key || "").trim(); + const value = + memoryModel.appendProperties( + String(fact && fact.value || "").trim(), + formatLongTermFactMetadata(fact) + ); + + return `${key}: ${value}`; + } ); updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); } @@ -7591,7 +7970,6 @@ ); updateRuntimeMemoryArrows(); updateRuntimeMemoryPinGlow(); - updateRuntimeMemoryTitleState(); } function appendUserIdleRuntimeMemoryLine() { @@ -7702,7 +8080,9 @@ modes[nextIndex] ); - renderRuntimeMemorySnapshot(); + renderRuntimeMemorySnapshot({ + availableModes: modes, + }); } function isRuntimeMemoryTitleBackZone(event) { diff --git a/ui/static/js/runtime/runtime-panel.js b/ui/static/js/runtime/runtime-panel.js index 3ee19e46..5982fa90 100644 --- a/ui/static/js/runtime/runtime-panel.js +++ b/ui/static/js/runtime/runtime-panel.js @@ -45,6 +45,7 @@ let contextTabButtons = {}; let contextRuntimePanel = null; + let contextPanelResizeObserver = null; const runtimePanelState = { activeTab: "service", @@ -1585,6 +1586,22 @@ "context-runtime-panel" ); + if ( + contextRuntimePanel + && typeof window.ResizeObserver === "function" + ) { + contextPanelResizeObserver = + new window.ResizeObserver( + function () { + scheduleRuntimeTelemetryFrame(); + } + ); + + contextPanelResizeObserver.observe( + contextRuntimePanel + ); + } + Object.entries( contextTabButtons ).forEach( diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 1a21537c..4f5d6144 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -1,6 +1,8 @@ // Temporary UI-only switch. Runtime parsing, execution, avatar updates and // logger entries stay active; flip this to true to restore the two chat bubbles. const ENABLE_JIN_VISUAL_ACTION_BUBBLES = false; +const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = + "jin:think-runtime-citation-highlight"; const JIN_VISUAL_SEQUENCE_ACTIONS = new Set([ "jin_color", @@ -574,6 +576,30 @@ function buildRuntimeActionDetail( } +function highlightUpdatedActiveMemory(activeMemoryId) { + const normalizedId = + String(activeMemoryId || "") + .trim() + .toLowerCase(); + + if (!/^[a-z0-9]{6}$/.test(normalizedId)) { + return; + } + + window.dispatchEvent( + new CustomEvent( + THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT, + { + detail: { + sourceId: `runtime-action:update-active-memory:${normalizedId}`, + active: true, + activeMemoryIds: [normalizedId], + }, + } + ) + ); +} + function formatActiveMemoryUpdateDetail( data ) { @@ -2052,10 +2078,16 @@ function handleRuntimeAction( && window.JinRuntime.runtime && window.JinRuntime.runtime.replaceActiveMemoryRecordById ) { + const activeMemoryId = + data.active_memory_id || data.id; + window.JinRuntime.runtime.replaceActiveMemoryRecordById( - data.active_memory_id || data.id, + activeMemoryId, data.active_memory ); + highlightUpdatedActiveMemory( + activeMemoryId + ); } if ( diff --git a/ui/templates/index.html b/ui/templates/index.html index 70b32726..4aee8643 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,7 +14,7 @@ - + @@ -512,8 +512,8 @@ - - + + @@ -535,7 +535,7 @@ - + diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index 3ed6badf..27df61ed 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -27,6 +27,7 @@ get_runtime_action_display_name, runtime_action_has_close_tag, runtime_action_emits_followup, + runtime_action_follows_up_on_fail, ) from rules.runtime import ( ACTION_REJECTED_MISSING_TRIGGER_WORDS_MESSAGE, @@ -57,7 +58,9 @@ normalize_skill_name, ) from utils.tool_results import ( + TOOL_RESULT_KIND_ACTIVE_MEMORY, clear_runtime_tool_results_before_state, + record_runtime_tool_result, snapshot_runtime_tool_results_state, ) from utils.runtime_action_abort import ( @@ -920,6 +923,24 @@ def accept_runtime_action_once_per_message( ) if not active_memory_line: + failure_result = { + "ok": False, + "action": "save_active_memory", + "error": "invalid_active_memory_payload", + "detail": "invalid payload", + "payload": str(action.payload or "").strip(), + } + rejected_action_events[id(action)] = { + "status": "failed", + "error": failure_result["error"], + "failure_reason": failure_result["detail"], + "failed_marker_payload": failure_result["payload"], + } + record_runtime_tool_result( + context, + TOOL_RESULT_KIND_ACTIVE_MEMORY, + failure_result, + ) continue if not accept_runtime_action_once_per_message( @@ -937,7 +958,11 @@ def accept_runtime_action_once_per_message( continue if action.name == RUNTIME_ACTION_UPDATE_ACTIVE_MEMORY: - normalized_payload, _, _ = normalize_update_active_memory_payload_reference( + ( + normalized_payload, + requested_reference, + resolved_reference_id, + ) = normalize_update_active_memory_payload_reference( context, action.payload, ) @@ -946,6 +971,34 @@ def accept_runtime_action_once_per_message( ) if not active_memory_id or not update_fields: + failure_error = ( + "active_memory_not_found" + if requested_reference and not resolved_reference_id + else "invalid_update_active_memory_payload" + ) + failure_reason = ( + "incorrect id" + if failure_error == "active_memory_not_found" + else "invalid payload" + ) + failure_result = { + "ok": False, + "action": "update_active_memory", + "error": failure_error, + "detail": failure_reason, + "payload": str(action.payload or "").strip(), + } + rejected_action_events[id(action)] = { + "status": "failed", + "error": failure_error, + "failure_reason": failure_reason, + "failed_marker_payload": failure_result["payload"], + } + record_runtime_tool_result( + context, + TOOL_RESULT_KIND_ACTIVE_MEMORY, + failure_result, + ) continue update_key = ( @@ -1520,6 +1573,9 @@ def accept_runtime_action_once_per_message( "behavior_contract_blocker_matched", "restricted_write", } + and not runtime_action_follows_up_on_fail( + action.name + ) ): continue @@ -1542,9 +1598,15 @@ def accept_runtime_action_once_per_message( "error", "", ), - "detail": rejected_event.get( - "failure_followup_message", - "", + "detail": ( + rejected_event.get( + "failure_followup_message", + "", + ) + or rejected_event.get( + "failure_reason", + "", + ) ), } action_display_id = str( diff --git a/utils/actions/save_active_memory_utils.py b/utils/actions/save_active_memory_utils.py index eafe047d..251817df 100644 --- a/utils/actions/save_active_memory_utils.py +++ b/utils/actions/save_active_memory_utils.py @@ -3,7 +3,6 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - get_runtime_action_body_placeholder, get_runtime_action_private_marker, ) @@ -41,12 +40,6 @@ def _get_save_active_memory_placeholder_source( ) -> str: if marker is None: - body_placeholder = get_runtime_action_body_placeholder( - RUNTIME_ACTION_SAVE_ACTIVE_MEMORY - ) - if body_placeholder: - return body_placeholder - marker = get_runtime_action_private_marker( RUNTIME_ACTION_SAVE_ACTIVE_MEMORY ) @@ -55,7 +48,7 @@ def _get_save_active_memory_placeholder_source( marker ) - return marker_fields + return marker_fields or "CONDITIONS" def get_save_active_memory_marker_fields( diff --git a/utils/context/result_sections.py b/utils/context/result_sections.py index ef91a4b1..1c7c8a85 100644 --- a/utils/context/result_sections.py +++ b/utils/context/result_sections.py @@ -36,6 +36,17 @@ def format_active_memory_result_sections( ) continue + if action == "update_active_memory": + sections.append( + ( + "UPDATE_ACTIVE_MEMORY", + format_tool_result_payload( + result + ), + ) + ) + continue + if action == "resolve_active_memory": sections.append( ( From 1888e9507778308e6cf637b9909740b3d199ff67 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:41:40 +0300 Subject: [PATCH 095/168] chore: ui - fix for L-T facts lazy load, update and bootstrap - fix for files panel load - add background L-T sync - remove idle action leftovers - add enable flag to action contracts - cleanup identity rules - remove redundant follow-up messages - recover L1 request on websocket reconnect - add table markdown for bubble - add last three chat turns at session bootstrap - remove bubble rating - add copy all and delete answer actions for JIN bubble - ui fixes --- ARCHITECTURE.md | 2 +- agent/nodes/brain.py | 284 ++---- app.py | 11 + clients/brain_client.py | 262 ++++-- config.example.py | 4 +- contracts/asset_action.json | 2 + contracts/attach_file.json | 2 + contracts/check_todo.json | 2 + contracts/clean_tool_results.json | 2 + contracts/create_todo_list.json | 2 + contracts/deep_web_search.json | 2 + contracts/detach_file.json | 2 + contracts/idle.json | 18 - contracts/jin_color.json | 2 + contracts/jin_position.json | 2 + contracts/jin_size.json | 2 + contracts/jin_speed.json | 2 + contracts/list_files.json | 2 + contracts/load_delayed_memory.json | 2 + contracts/load_skill.json | 2 + contracts/resolve_active_memory.json | 2 + contracts/resolve_todo.json | 2 + contracts/rules_assembler.py | 46 +- contracts/save_active_memory.json | 2 + contracts/save_delayed_memory.json | 2 + contracts/unload_delayed_memory.json | 2 + contracts/unload_skill.json | 2 + contracts/update_active_memory.json | 2 + contracts/update_l4_facts.json | 2 + contracts/web_search.json | 2 + docs/JIN_ARCHITECTURE.md | 14 +- docs/JIN_CURRENT_STATE.md | 3 +- rules/brain_context_builder.py | 54 +- rules/identity.py | 69 +- rules/runtime.py | 29 +- runtime/L1_memory.py | 202 +++- runtime/L1_memory_pending.py | 230 +++++ runtime/L4_memory.py | 512 +++++++++- runtime/deep_web_search.py | 113 ++- runtime/runtime_context.py | 23 +- runtime/stream.py | 82 +- tests/runtime_actions/test_active_memory.py | 1 - tests/runtime_actions/test_asset_actions.py | 1 - tests/runtime_actions/test_delayed_memory.py | 1 - tests/runtime_actions/test_jin_color.py | 1 - tests/runtime_actions/test_skill_actions.py | 1 - tests/runtime_actions/test_stream_filter.py | 355 ------- tests/runtime_actions/test_todo.py | 1 - tests/runtime_actions/test_update_l4_facts.py | 78 ++ tests/test_active_memory_failure_followup.py | 3 +- tests/test_answer_rating_client_contract.py | 4 +- tests/test_behavior_contract.py | 50 +- tests/test_brain_asset_flow.py | 352 +------ tests/test_brain_prompt_memory.py | 9 +- tests/test_brain_runtime_actions.py | 155 ++- tests/test_chat_log.py | 57 ++ ..._chat_reasoning_spacing_client_contract.py | 14 + ...test_chat_reference_ids_client_contract.py | 11 +- tests/test_chat_runtime_marker_ui.py | 320 ++++++- tests/test_deep_web_search_flow.py | 50 + .../test_deep_web_search_marker_lifecycle.py | 80 ++ tests/test_l1_reconnect_resume.py | 297 ++++++ tests/test_l4_memory.py | 163 +++- tests/test_l4_memory_client_contract.py | 30 +- ..._memory_citation_layers_client_contract.py | 14 +- ...untime_memory_lazy_rows_client_contract.py | 8 +- ...tions_deep_search_hover_client_contract.py | 76 ++ tests/test_session_bootstrap_chat_tail.py | 161 ++++ tests/test_user_retry_contract.py | 201 ++++ tests/test_websocket_pending_usage.py | 246 ----- ui/static/css/chat-rating.css | 104 ++ ui/static/css/chat-runtime-action.css | 92 +- ui/static/css/chat.css | 71 +- ui/static/css/theme-win95.css | 2 +- ui/static/js/answer-rating.js | 330 +++++++ ui/static/js/chat-reference-ids.js | 107 +++ ui/static/js/chat-response-formatter.js | 187 +++- ui/static/js/chat-runtime-actions.js | 885 ++++++++++++++++-- ui/static/js/chat.js | 30 +- ui/static/js/logger/session-actions.js | 78 +- ui/static/js/runtime/runtime-l4-memory.js | 72 -- ui/static/js/runtime/runtime-memory-view.js | 305 +++++- ui/static/js/runtime/runtime-session.js | 1 + ui/static/js/runtime/runtime.js | 7 + ui/static/js/socket.js | 65 ++ ui/static/js/socket/event-handlers.js | 234 ++++- ui/static/js/socket/runtime-actions.js | 75 +- ui/templates/index.html | 30 +- utils/actions/__init__.py | 2 - utils/actions/common_action_utils.py | 18 +- utils/actions/dispatcher.py | 110 +-- utils/actions/idle_utils.py | 45 - utils/actions/jin_color_actions.py | 112 --- utils/actions/update_l4_facts_actions.py | 173 +++- utils/actions/update_l4_facts_utils.py | 9 +- utils/brain_client_utils.py | 234 ----- utils/chat_log.py | 255 ++++- utils/context/tool_results.py | 35 + utils/session_actions_history.py | 1 - utils/session_restore.py | 317 ++++++- utils/tool_results.py | 1 + utils/tool_results_context.py | 14 - websocket/__init__.py | 245 +++-- websocket/bootstrap.py | 203 +++- websocket/messages.py | 502 +++++----- websocket/tasks.py | 43 - 106 files changed, 6899 insertions(+), 2839 deletions(-) delete mode 100644 contracts/idle.json create mode 100644 runtime/L1_memory_pending.py create mode 100644 tests/test_deep_web_search_marker_lifecycle.py create mode 100644 tests/test_l1_reconnect_resume.py create mode 100644 tests/test_session_actions_deep_search_hover_client_contract.py create mode 100644 tests/test_session_bootstrap_chat_tail.py create mode 100644 tests/test_user_retry_contract.py delete mode 100644 utils/actions/idle_utils.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4379bb6e..0187e363 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -41,7 +41,7 @@ The surrounding modules have narrow ownership: The browser uses `/ws/chat` as the live runtime channel. `websocket/bootstrap.py` creates a new `RuntimeContext` or reconnects the socket to resumable state, then hydrates memory, delayed reports, files, counters, and browser-provided session data. -Each context owns a `PendingRequestQueue`. User messages and internal continuation ticks run serially through that queue, which keeps turn order stable while generation, actions, and memory updates overlap in time. +Each context owns a normal `asyncio.Queue` for pending requests. User messages and internal continuation ticks run serially through that queue, which keeps turn order stable while generation, actions, and memory updates overlap in time. A socket and a logical runtime have separate identities. Reconnect can attach a fresh transport to an existing context. Explicit session bootstrap can restore saved L1/L3 state and related runtime metadata. diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index df5da5c2..b99763c6 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -23,13 +23,11 @@ from rules.runtime import ( ANSWERING_RECOVERY_MESSAGE, CONTEXT_LIMIT_RECOVERY_MESSAGE, - IDLE_FOLLOWUP_MESSAGE, REASONING_RECOVERY_MESSAGE, ) from contracts.rules_assembler import ( RUNTIME_ACTION_ATTACH_FILE, RUNTIME_ACTION_DEEP_WEB_SEARCH, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_POSITION, RUNTIME_ACTION_JIN_SIZE, @@ -88,7 +86,6 @@ ) from utils.tool_results_context import ( build_tools_results_context, - is_idle_tool_results_block, split_tools_results_context, ) @@ -314,12 +311,7 @@ def action_event_defers_follow_up(event) -> bool: if not isinstance(event, dict): return False - name = str(event.get("name", "") or "").strip().casefold() - - return ( - name == RUNTIME_ACTION_IDLE.casefold() - or bool(event.get("deferred_follow_up")) - ) + return bool(event.get("deferred_follow_up")) async def replay_session_restore_resource_actions( @@ -729,15 +721,6 @@ def remember_successful_previous_reasoning( ) -FOLLOWUP_SYSTEM_MESSAGE = ( - "\n" - " CURRENT_REQUEST_FLOW\n" - " BACKGROUND_ONLY\n" - " FALSE\n" - "" -) - - def _compact_followup_value( value, ) -> str: @@ -1013,106 +996,6 @@ def rename_runtime_memory_for_followup( ) -def build_idle_followup_tool_results( - idle_followup: dict, -) -> str: - - seconds = int( - idle_followup.get( - "seconds", - 0, - ) - or 0 - ) - idle_id = escape( - str( - idle_followup.get( - "id", - "", - ) - or "" - ) - ) - - return ( - '\n' - " \n" - f" {idle_id}\n" - f" {seconds}\n" - " \n" - "" - ) - - -def build_idle_followup_system_prompt( - idle_followup: dict, -) -> str: - - snapshot = idle_followup.get( - "context_snapshot", - {}, - ) - if not isinstance(snapshot, dict): - snapshot = {} - - frozen_system_prompt = str( - snapshot.get( - "system_prompt", - "", - ) - or "" - ) - inherited_tool_results, frozen_system_prompt = ( - split_tools_results_context( - frozen_system_prompt - ) - ) - inherited_tool_results = [ - block - for block in inherited_tool_results - if not is_idle_tool_results_block( - block - ) - ] - inherited_tool_results.append( - build_idle_followup_tool_results( - idle_followup - ) - ) - - sections = [ - build_tools_results_context( - inherited_tool_results - ), - ] - - if frozen_system_prompt: - sections.append( - rename_runtime_memory_for_followup( - frozen_system_prompt, - sequence_started_at=idle_followup.get( - "sequence_started_at" - ), - ) - ) - - return "\n\n".join( - section - for section in sections - if str(section or "").strip() - ) - - -def build_followup_system_message( - latest_action: str = "", -) -> str: - - # Kept as an argument for existing callers; action identity lives only in - # CURRENT_REQUEST_FLOW so the follow-up prompt has one focused source of truth. - _ = latest_action - return FOLLOWUP_SYSTEM_MESSAGE - - def restore_sequence_attachments_for_followup( context, ) -> None: @@ -1287,12 +1170,6 @@ def build_followup_system_prompt( ) context.runtime_potential_loop_detected_pending = False - sections.append( - build_followup_system_message( - latest_action - ) - ) - failed_action_context = ( build_failed_runtime_action_followup_contexts( context @@ -1755,21 +1632,13 @@ async def run_brain_stream( emit_content_to_chat: bool = True, filter_runtime_actions: bool = True, preserve_runtime_action_markers: bool = False, + followup_tick: bool = False, ) -> tuple[str, str]: logger = context.logger - is_followup_tick = ( - not str( - brain_payload - or "" - ).strip() - and str( - system_prompt - or "" - ).lstrip().startswith( - FOLLOWUP_SYSTEM_MESSAGE - ) + is_followup_tick = bool( + followup_tick ) previous_followup_tick = getattr( context, @@ -2011,12 +1880,6 @@ def ensure_runtime_list( # (e.g. the user explicitly asked JIN to emit only the marker). context.runtime_active_memory_saved_this_turn = False context.runtime_active_memory_refresh_tick = 0 - idle_followup = state.metadata.get( - "idle_followup", - ) - if not isinstance(idle_followup, dict): - idle_followup = {} - action_guard_retry = getattr( context, "runtime_action_guard_retry", @@ -2033,86 +1896,59 @@ def ensure_runtime_list( if not isinstance(retry_context_snapshot, dict): retry_context_snapshot = {} - if idle_followup: - idle_system_prompt = build_idle_followup_system_prompt( - idle_followup + retry_system_prompt = str( + retry_context_snapshot.get( + "system_prompt", + "", ) - sequence_origin_request = str( - idle_followup.get( - "origin_user_request", - "", - ) - or state.user_input - or getattr( - context, - "runtime_turn_user_message", - "", - ) - or "" - ).strip() - system_prompt = self.build_followup_system_prompt( - idle_system_prompt, - sequence_origin_request, - context=context, - instruction=IDLE_FOLLOWUP_MESSAGE, - latest_action="idle", + or "" + ) + retry_user_prompt = str( + retry_context_snapshot.get( + "user_prompt", + "", ) - brain_payload = "" - sequence_user_request = sequence_origin_request + or "" + ) + + if retry_system_prompt: + system_prompt = retry_system_prompt + brain_payload = retry_user_prompt else: - retry_system_prompt = str( - retry_context_snapshot.get( - "system_prompt", - "", + system_prompt = ( + build_brain_context( + context, + runtime_actions=runtime_actions, + user_input=state.user_input, + commit_active_memory_refresh=True, + include_previous_reasoning=bool( + getattr( + context, + "runtime_session_restore_priming", + False, + ) + ), ) - or "" ) - retry_user_prompt = str( - retry_context_snapshot.get( - "user_prompt", - "", + brain_payload = ( + build_brain_payload( + state.user_input, + context=context, ) - or "" ) - if retry_system_prompt: - system_prompt = retry_system_prompt - brain_payload = retry_user_prompt - else: - system_prompt = ( - build_brain_context( - context, - runtime_actions=runtime_actions, - user_input=state.user_input, - commit_active_memory_refresh=True, - include_previous_reasoning=bool( - getattr( - context, - "runtime_session_restore_priming", - False, - ) - ), - ) - ) - brain_payload = ( - build_brain_payload( - state.user_input, - context=context, - ) - ) - - sequence_user_request = str( - getattr( - context, - "runtime_turn_user_message", - "", - ) - or state.user_input - or "" - ) - sequence_user_request = sanitize_sequence_user_request( - sequence_user_request + sequence_user_request = str( + getattr( + context, + "runtime_turn_user_message", + "", ) + or state.user_input + or "" + ) + sequence_user_request = sanitize_sequence_user_request( + sequence_user_request + ) await emit_active_memory_records_update_if_dirty( context @@ -2492,6 +2328,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -2548,6 +2385,7 @@ def consume_current_action_batch(): "scene_effect": "search", "context": deep_search_call.get("context"), "deep_search_parent": True, + "deep_search_payload_ready": True, }) mark_runtime_action_completed( context, @@ -2591,13 +2429,6 @@ def consume_current_action_batch(): sequence_user_request, context=context, latest_action=latest_followup_action, - instruction=( - "Answer the latest user request from the trusted " - "DEEP_WEB_SEARCH report. Use it as the prepared " - "research brief. If the user request still needs more " - "research, you may emit another WEB_SEARCH or " - "DEEP_WEB_SEARCH action." - ), ) await emit_active_memory_records_update_if_dirty(context) @@ -2609,6 +2440,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -2722,12 +2554,6 @@ def consume_current_action_batch(): sequence_user_request, context=context, latest_action=latest_followup_action, - instruction=( - "Answer the latest user request using the " - "WEB_SEARCH tool result from trusted runtime " - "context. Mention the quoted source data when " - "it helps, then continue the workflow." - ), ) ) @@ -2742,6 +2568,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, ) @@ -2807,6 +2634,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -2877,6 +2705,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -2969,6 +2798,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -3022,6 +2852,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=followup_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=followup_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=True, @@ -3117,6 +2948,7 @@ def consume_current_action_batch(): brain_client=brain_client, system_prompt=final_system_prompt, brain_payload="", + followup_tick=True, runtime_actions=final_runtime_actions, emit_content_to_chat=True, filter_runtime_actions=False, @@ -3124,7 +2956,7 @@ def consume_current_action_batch(): ) state.brain_response = text or "" - if context is not None and not idle_followup: + if context is not None: remember_successful_previous_reasoning( context, reasoning, diff --git a/app.py b/app.py index a192ad73..36091867 100644 --- a/app.py +++ b/app.py @@ -54,6 +54,10 @@ from clients.registry import build_clients from runtime.client import RuntimeClient +from runtime.L4_memory import ( + start_l4_memory_server_scheduler, + stop_l4_memory_server_scheduler, +) from runtime.state import RUNTIME_MEMORY_SUMMARIZER_LABEL from runtime.behavior_contract import ( @@ -117,12 +121,19 @@ async def lifespan(application: FastAPI): application.state.http_client ) + start_l4_memory_server_scheduler( + application.state + ) + yield # ----------------------------------------------------- # SHUTDOWN # ----------------------------------------------------- + await stop_l4_memory_server_scheduler( + application.state + ) await application.state.http_client.aclose() diff --git a/clients/brain_client.py b/clients/brain_client.py index b25eb81c..736fa5de 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -7,10 +7,10 @@ config, ) from contracts.rules_assembler import ( + RUNTIME_ACTION_DEEP_WEB_SEARCH, RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, RUNTIME_ACTION_UPDATE_L4_FACTS, @@ -707,6 +707,7 @@ def preserve_duplicate_load_skill_marker( stop_for_runtime_action = False delayed_memory_bubble_started = False active_memory_pending_bubble_ids = [] + deep_web_search_pending_bubble_ids = [] update_l4_facts_pending_bubble_ids = [] asset_action_bubble_started = False asset_action_bubble_id = "" @@ -743,7 +744,6 @@ def preserve_duplicate_load_skill_marker( session_action_message_parts = [] session_action_message_content = "" session_action_marker_seen = False - pending_idle_action_calls = [] confirmed_action_guard_names = set() rejected_action_guard_names = set() action_guard_display_state = {} @@ -1476,6 +1476,123 @@ def assign_active_memory_bubble_ids( if action_id: action_display_ids[id(action)] = action_id + async def emit_deep_web_search_bubble_started( + result, + ): + + started_actions = tuple( + getattr( + result, + "started_actions", + (), + ) + or () + ) + + started_count = sum( + 1 + for action in started_actions + if action.name == RUNTIME_ACTION_DEEP_WEB_SEARCH + ) + + if not started_count: + return + + emitter = getattr( + context, + "emitter", + None, + ) + emit = getattr( + emitter, + "emit", + None, + ) + + if emit is None: + return + + existing_count = sum( + 1 + for event in getattr( + context, + "runtime_action_events", + [], + ) + or [] + if isinstance(event, dict) + and event.get("name") + == RUNTIME_ACTION_DEEP_WEB_SEARCH.lower() + ) + + for _ in range(started_count): + sequence = max( + int( + getattr( + context, + "runtime_deep_web_search_action_sequence", + 0, + ) + or 0 + ), + existing_count, + ) + 1 + context.runtime_deep_web_search_action_sequence = sequence + existing_count = sequence + + action_id = build_runtime_action_id( + RUNTIME_ACTION_DEEP_WEB_SEARCH, + sequence, + ) + deep_web_search_pending_bubble_ids.append( + action_id + ) + + payload = { + "type": "runtime_action", + "action": RUNTIME_ACTION_DEEP_WEB_SEARCH.lower(), + "id": action_id, + "status": "started", + "runtime_message_id": runtime_message_id, + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_DEEP_WEB_SEARCH + ), + "text": build_runtime_action_display_text( + RUNTIME_ACTION_DEEP_WEB_SEARCH + ), + "scene_effect": "search", + "deep_search_parent": True, + "deep_search_payload_ready": False, + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_DEEP_WEB_SEARCH + ), + } + + if action_context_snapshot: + payload["context"] = action_context_snapshot + + await emit( + payload + ) + + def assign_deep_web_search_bubble_ids( + actions, + action_display_ids, + ): + + for action in actions: + if action.name != RUNTIME_ACTION_DEEP_WEB_SEARCH: + continue + + action_id = ( + deep_web_search_pending_bubble_ids.pop(0) + if deep_web_search_pending_bubble_ids + else "" + ) + + if action_id: + action_display_ids[id(action)] = action_id + async def emit_update_l4_facts_bubble_started( result, ): @@ -1574,6 +1691,75 @@ def assign_update_l4_facts_bubble_ids( action_display_ids[id(action)] = action_id + async def emit_rejected_update_l4_facts_bubble_finished( + result, + ): + + removed_update_markers = sum( + 1 + for marker in ( + getattr(result, "removed_markers", ()) + or () + ) + if re.search( + r"<\s*UPDATE_L4_FACTS(?:\s|>)", + str(marker or ""), + flags=re.IGNORECASE, + ) + ) + parsed_update_actions = sum( + 1 + for action in ( + getattr(result, "observed_actions", ()) + or () + ) + if action.name == RUNTIME_ACTION_UPDATE_L4_FACTS + ) + rejected_count = max( + 0, + removed_update_markers - parsed_update_actions, + ) + + if not rejected_count: + return + + emitter = getattr(context, "emitter", None) + emit = getattr(emitter, "emit", None) + + for _ in range(rejected_count): + action_id = ( + update_l4_facts_pending_bubble_ids.pop(0) + if update_l4_facts_pending_bubble_ids + else "" + ) + + if not action_id or emit is None: + continue + + payload = { + "type": "runtime_action", + "action": "update_l4_facts", + "id": action_id, + "status": "failed", + "runtime_message_id": runtime_message_id, + "display_name": get_runtime_action_display_name( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "text": build_runtime_action_display_text( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + "detail": "Invalid L4 update note was ignored.", + "close_tag": runtime_action_has_close_tag( + RUNTIME_ACTION_UPDATE_L4_FACTS + ), + } + + if action_context_snapshot: + payload["context"] = action_context_snapshot + + await emit(payload) + + async def emit_asset_action_bubble_started( result=None, ): @@ -1783,9 +1969,18 @@ async def filter_runtime_action_chunk( await emit_active_memory_bubble_started( result ) + await emit_deep_web_search_bubble_started( + result + ) await emit_update_l4_facts_bubble_started( result ) + # The opening tag lights the bubble before its body is validated. If + # the completed marker is rejected, retire that exact pending bubble + # now; otherwise its queued id gets stolen by the next valid update. + await emit_rejected_update_l4_facts_bubble_finished( + result + ) await emit_asset_action_bubble_started( result ) @@ -1914,20 +2109,7 @@ async def apply_runtime_action_result( if not runtime_action_calls: return False - idle_action_calls = tuple( - action - for action in runtime_action_calls - if action.name == RUNTIME_ACTION_IDLE - ) - immediate_action_calls = tuple( - action - for action in runtime_action_calls - if action.name != RUNTIME_ACTION_IDLE - ) - - pending_idle_action_calls.extend( - idle_action_calls - ) + immediate_action_calls = runtime_action_calls if immediate_action_calls: ( @@ -1949,6 +2131,10 @@ async def apply_runtime_action_result( immediate_action_calls, action_display_ids, ) + assign_deep_web_search_bubble_ids( + immediate_action_calls, + action_display_ids, + ) assign_update_l4_facts_bubble_ids( immediate_action_calls, action_display_ids, @@ -1968,46 +2154,6 @@ async def apply_runtime_action_result( return True - async def flush_pending_idle_actions() -> None: - - if not pending_idle_action_calls: - return - - idle_actions = tuple( - pending_idle_action_calls - ) - pending_idle_action_calls.clear() - - ( - confirmed_action_ids, - rejected_action_ids, - guard_confirmation_ids, - action_display_ids, - ) = await confirm_runtime_action_guards( - context, - idle_actions, - user_message=text, - context_snapshot=action_context_snapshot, - confirmed_guard_names=confirmed_action_guard_names, - rejected_guard_names=rejected_action_guard_names, - display_state=action_guard_display_state, - ) - - await apply_runtime_action_calls( - context, - idle_actions, - user_message=text, - context_snapshot=action_context_snapshot, - assistant_message="".join( - raw_content_parts - ), - confirmed_action_ids=confirmed_action_ids, - rejected_action_ids=rejected_action_ids, - guard_confirmation_ids=guard_confirmation_ids, - action_display_ids=action_display_ids, - runtime_message_id=runtime_message_id, - ) - # ----------------------------------------------------- # SERVICE AS BRAIN # ----------------------------------------------------- @@ -2080,8 +2226,6 @@ async def flush_pending_idle_actions() -> None: await finalize_session_action_history() yield build_raw_model_output_chunk() return - await flush_pending_idle_actions() - content_tail = tail_result.text if ( content_tail @@ -2191,8 +2335,6 @@ async def flush_pending_idle_actions() -> None: await finalize_session_action_history() yield build_raw_model_output_chunk() return - await flush_pending_idle_actions() - content_tail = tail_result.text if ( content_tail diff --git a/config.example.py b/config.example.py index 45dde8b8..c6922652 100644 --- a/config.example.py +++ b/config.example.py @@ -137,7 +137,9 @@ # --------------------------------------------------------- L4_MEMORY_ENABLED = True -L4_IDLE_SECONDS = 60 +# Server-side L4 cadence while at least one tab is connected. With every tab +# closed, the backend keeps running L4 at exactly one third of this interval. +L4_IDLE_SECONDS = 15 # --------------------------------------------------------- # WEB_SEARCH diff --git a/contracts/asset_action.json b/contracts/asset_action.json index d3dd6c7c..47718a96 100644 --- a/contracts/asset_action.json +++ b/contracts/asset_action.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to run an asset action block.", "runtime_action": "ASSET_ACTION", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 110, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/attach_file.json b/contracts/attach_file.json index 10523207..e14f00ed 100644 --- a/contracts/attach_file.json +++ b/contracts/attach_file.json @@ -3,6 +3,8 @@ "version": 1, "description": "Attaches one persistent file to the JIN context.", "runtime_action": "ATTACH_FILE", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 130, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/check_todo.json b/contracts/check_todo.json index ee38ceb9..61ed9c16 100644 --- a/contracts/check_todo.json +++ b/contracts/check_todo.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to check a TODO item.", "runtime_action": "CHECK_TODO", + "enable_flag": "CAN_RUNTIME_TODO", + "runtime_order": 170, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/clean_tool_results.json b/contracts/clean_tool_results.json index 9286dd70..eeb93156 100644 --- a/contracts/clean_tool_results.json +++ b/contracts/clean_tool_results.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to clean redundant tool results.", "runtime_action": "CLEAN_TOOL_RESULTS", + "enable_flag": "CAN_CLEAN_TOOL_RESULTS", + "runtime_order": 30, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/create_todo_list.json b/contracts/create_todo_list.json index 9f23f8cc..90488d1e 100644 --- a/contracts/create_todo_list.json +++ b/contracts/create_todo_list.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to create a runtime TODO list.", "runtime_action": "CREATE_TODO_LIST", + "enable_flag": "CAN_RUNTIME_TODO", + "runtime_order": 150, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/deep_web_search.json b/contracts/deep_web_search.json index 49e3e91c..9b47a3da 100644 --- a/contracts/deep_web_search.json +++ b/contracts/deep_web_search.json @@ -3,6 +3,8 @@ "version": 1, "description": "Starts a bounded multi-step web research sequence using the service model as research workers.", "runtime_action": "DEEP_WEB_SEARCH", + "enable_flag": "CAN_DEEP_WEB_SEARCH", + "runtime_order": 10, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/detach_file.json b/contracts/detach_file.json index 5ccb8c46..0f5935fb 100644 --- a/contracts/detach_file.json +++ b/contracts/detach_file.json @@ -3,6 +3,8 @@ "version": 1, "description": "Removes one persistent file from the JIN context without deleting it.", "runtime_action": "DETACH_FILE", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 140, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/idle.json b/contracts/idle.json deleted file mode 100644 index fb9cd977..00000000 --- a/contracts/idle.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "idle": { - "version": 1, - "description": "Allows the model to schedule an idle follow-up tick.", - "runtime_action": "IDLE", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": true, - "follow_up_on_fail": false - }, - "rules": [ - "Emit idle action marker to setup and trigger follow-up tick after Ns seconds. DO NOT USE for casual chat reminders." - ], - "close_tag": false - } -} diff --git a/contracts/jin_color.json b/contracts/jin_color.json index accda436..e2b1209a 100644 --- a/contracts/jin_color.json +++ b/contracts/jin_color.json @@ -3,6 +3,8 @@ "version": 2, "description": "Allows the model to update the JIN runtime avatar center point color and its subtle glow for the current live session.", "runtime_action": "JIN_COLOR", + "enable_flag": "CAN_JIN_COLOR", + "runtime_order": 40, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/jin_position.json b/contracts/jin_position.json index 82a80c90..48f08f58 100644 --- a/contracts/jin_position.json +++ b/contracts/jin_position.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to move the collapsed live JIN avatar to a new position inside the current browser window.", "runtime_action": "JIN_POSITION", + "enable_flag": "CAN_JIN_POSITION", + "runtime_order": 60, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/jin_size.json b/contracts/jin_size.json index e6f0f2d1..9c75d2e3 100644 --- a/contracts/jin_size.json +++ b/contracts/jin_size.json @@ -3,6 +3,8 @@ "version": 2, "description": "Allows the model to update the collapsed JIN runtime avatar size for the current live session.", "runtime_action": "JIN_SIZE", + "enable_flag": "CAN_JIN_SIZE", + "runtime_order": 50, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/jin_speed.json b/contracts/jin_speed.json index 99588f89..93544b32 100644 --- a/contracts/jin_speed.json +++ b/contracts/jin_speed.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to set the movement speed used by subsequent JIN_POSITION avatar gestures.", "runtime_action": "JIN_SPEED", + "enable_flag": "CAN_JIN_SPEED", + "runtime_order": 70, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/list_files.json b/contracts/list_files.json index ed641c9d..66faa7da 100644 --- a/contracts/list_files.json +++ b/contracts/list_files.json @@ -3,6 +3,8 @@ "version": 1, "description": "Lists persistent files available to JIN.", "runtime_action": "LIST_FILES", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 120, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/load_delayed_memory.json b/contracts/load_delayed_memory.json index 0e965620..94257faa 100644 --- a/contracts/load_delayed_memory.json +++ b/contracts/load_delayed_memory.json @@ -3,6 +3,8 @@ "version": 2, "description": "Allows the model to load a delayed memory report into the session context.", "runtime_action": "LOAD_DELAYED_MEMORY", + "enable_flag": "CAN_SAVE_DELAYED_MEMORY", + "runtime_order": 190, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/load_skill.json b/contracts/load_skill.json index ead405eb..178056e1 100644 --- a/contracts/load_skill.json +++ b/contracts/load_skill.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to load skills.", "runtime_action": "LOAD_SKILL", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 90, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/resolve_active_memory.json b/contracts/resolve_active_memory.json index e19187fc..afe8261c 100644 --- a/contracts/resolve_active_memory.json +++ b/contracts/resolve_active_memory.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to resolve active memory.", "runtime_action": "RESOLVE_ACTIVE_MEMORY", + "enable_flag": "CAN_SAVE_ACTIVE_MEMORY", + "runtime_order": 220, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/resolve_todo.json b/contracts/resolve_todo.json index 78501c87..d1739cc4 100644 --- a/contracts/resolve_todo.json +++ b/contracts/resolve_todo.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to resolve a TODO item.", "runtime_action": "RESOLVE_TODO", + "enable_flag": "CAN_RUNTIME_TODO", + "runtime_order": 160, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index 6f521afb..2466e019 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -15,34 +15,6 @@ CONTRACT_VERSION = 1 -ACTION_CONFIG_KEYS = ( - ("DEEP_WEB_SEARCH", "CAN_DEEP_WEB_SEARCH"), - ("WEB_SEARCH", "CAN_WEB_SEARCH"), - ("CLEAN_TOOL_RESULTS", "CAN_CLEAN_TOOL_RESULTS"), - ("IDLE", "CAN_IDLE"), - ("JIN_COLOR", "CAN_JIN_COLOR"), - ("JIN_SIZE", "CAN_JIN_SIZE"), - ("JIN_POSITION", "CAN_JIN_POSITION"), - ("JIN_SPEED", "CAN_JIN_SPEED"), - ("UPDATE_L4_FACTS", "CAN_UPDATE_L4_FACTS"), - ("LOAD_SKILL", "CAN_USE_ASSETS"), - ("UNLOAD_SKILL", "CAN_USE_ASSETS"), - ("ASSET_ACTION", "CAN_USE_ASSETS"), - ("LIST_FILES", "CAN_USE_ASSETS"), - ("ATTACH_FILE", "CAN_USE_ASSETS"), - ("DETACH_FILE", "CAN_USE_ASSETS"), - ("CREATE_TODO_LIST", "CAN_RUNTIME_TODO"), - ("RESOLVE_TODO", "CAN_RUNTIME_TODO"), - ("CHECK_TODO", "CAN_RUNTIME_TODO"), - ("SAVE_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), - ("LOAD_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), - ("UNLOAD_DELAYED_MEMORY", "CAN_SAVE_DELAYED_MEMORY"), - ("SAVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), - ("RESOLVE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), - ("UPDATE_ACTIVE_MEMORY", "CAN_SAVE_ACTIVE_MEMORY"), -) - - ACTIVE_MEMORY_ENTRY_RE = re.compile( r"^\s*-?\s*active_memory(?:_\d+)?\s*:", re.IGNORECASE | re.MULTILINE, @@ -277,11 +249,22 @@ def runtime_action_follows_up_on_fail(runtime_action: str) -> bool: def get_enabled_runtime_actions(runtime_actions=None) -> tuple[str, ...]: - enabled_actions = [] action_flags = runtime_actions or {} + contracts = sorted( + get_action_contracts().values(), + key=lambda contract: int(contract.get("runtime_order", 0) or 0), + ) + enabled_actions = [] + + for contract in contracts: + enable_flag = str(contract.get("enable_flag", "") or "").strip() + if not enable_flag or not bool(action_flags.get(enable_flag, False)): + continue - for action_name, config_key in ACTION_CONFIG_KEYS: - if bool(action_flags.get(config_key, False)): + action_name = _normalize_action_name( + contract.get("runtime_action", "") + ) + if action_name and action_name not in enabled_actions: enabled_actions.append(action_name) return tuple(enabled_actions) @@ -625,7 +608,6 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_CREATE_TODO_LIST = get_runtime_action_name("create_todo_list") RUNTIME_ACTION_RESOLVE_TODO = get_runtime_action_name("resolve_todo") RUNTIME_ACTION_CHECK_TODO = get_runtime_action_name("check_todo") -RUNTIME_ACTION_IDLE = get_runtime_action_name("idle") RUNTIME_ACTION_JIN_COLOR = get_runtime_action_name("jin_color") RUNTIME_ACTION_JIN_SIZE = get_runtime_action_name("jin_size") RUNTIME_ACTION_JIN_POSITION = get_runtime_action_name("jin_position") diff --git a/contracts/save_active_memory.json b/contracts/save_active_memory.json index 15e236c7..f5d9d7f4 100644 --- a/contracts/save_active_memory.json +++ b/contracts/save_active_memory.json @@ -3,6 +3,8 @@ "version": 4, "description": "Allows JIN to save active memory conditions.", "runtime_action": "SAVE_ACTIVE_MEMORY", + "enable_flag": "CAN_SAVE_ACTIVE_MEMORY", + "runtime_order": 210, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 1664e821..d946a31b 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -3,6 +3,8 @@ "version": 4, "description": "Allows the model to save a delayed memory report.", "runtime_action": "SAVE_DELAYED_MEMORY", + "enable_flag": "CAN_SAVE_DELAYED_MEMORY", + "runtime_order": 180, "private_marker": "", "triggers": [ "создай отчёт", diff --git a/contracts/unload_delayed_memory.json b/contracts/unload_delayed_memory.json index f79108fe..c85100c6 100644 --- a/contracts/unload_delayed_memory.json +++ b/contracts/unload_delayed_memory.json @@ -3,6 +3,8 @@ "version": 2, "description": "Allows the model to unload a delayed memory report from the session context.", "runtime_action": "UNLOAD_DELAYED_MEMORY", + "enable_flag": "CAN_SAVE_DELAYED_MEMORY", + "runtime_order": 200, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/unload_skill.json b/contracts/unload_skill.json index 4145fd72..65813ff9 100644 --- a/contracts/unload_skill.json +++ b/contracts/unload_skill.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to unload skills.", "runtime_action": "UNLOAD_SKILL", + "enable_flag": "CAN_USE_ASSETS", + "runtime_order": 100, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/update_active_memory.json b/contracts/update_active_memory.json index 7f808f57..1fee7042 100644 --- a/contracts/update_active_memory.json +++ b/contracts/update_active_memory.json @@ -3,6 +3,8 @@ "version": 2, "description": "Allows JIN to update state fields already fixed on an active memory record.", "runtime_action": "UPDATE_ACTIVE_MEMORY", + "enable_flag": "CAN_SAVE_ACTIVE_MEMORY", + "runtime_order": 230, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/update_l4_facts.json b/contracts/update_l4_facts.json index 18348d39..f8360422 100644 --- a/contracts/update_l4_facts.json +++ b/contracts/update_l4_facts.json @@ -3,6 +3,8 @@ "version": 1, "description": "Lets JIN send a focused update, merge, or create instruction to the L4 maintainer.", "runtime_action": "UPDATE_L4_FACTS", + "enable_flag": "CAN_UPDATE_L4_FACTS", + "runtime_order": 80, "private_marker": "", "triggers": [], "blockers": [], diff --git a/contracts/web_search.json b/contracts/web_search.json index 15e36da3..31d6c238 100644 --- a/contracts/web_search.json +++ b/contracts/web_search.json @@ -3,6 +3,8 @@ "version": 1, "description": "Allows the model to request a runtime web search.", "runtime_action": "WEB_SEARCH", + "enable_flag": "CAN_WEB_SEARCH", + "runtime_order": 20, "private_marker": "", "triggers": [], "blockers": [], diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index 0969b3fc..f283362c 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -32,7 +32,7 @@ The codebase is intentionally not structured as a generic autonomous-agent frame Browser UI <-> FastAPI / WebSocket (/ws/chat) -> RuntimeContext - -> PendingRequestQueue + -> asyncio.Queue -> process_message() -> AgentRuntime -> BrainNode @@ -98,20 +98,15 @@ Main ownership by package: Do not create a parallel state container for a concept already owned here unless the lifetime is intentionally browser-only or filesystem-persistent. -### 3.2 PendingRequestQueue +### 3.2 Pending request queue -`websocket/tasks.py::PendingRequestQueue` serializes work. It currently keeps two deques: - -- idle follow-ups; -- regular requests. - -`_get()` returns an idle follow-up before a regular request when one is queued. Foreground work still has explicit guards around background L4 processing. Any change to queue priority must be treated as a behavioral change, not a harmless refactor. +The websocket endpoint uses a normal `asyncio.Queue` to serialize queued requests in FIFO order. Foreground work still has explicit guards around background L4 processing. ### 3.3 Foreground turn `websocket/messages.py::process_message()` currently performs the foreground lifecycle: -1. classify ordinary turn vs IDLE follow-up vs action-guard retry vs archived-session resume tick; +1. classify ordinary turn vs action-guard retry vs archived-session resume tick; 2. resolve current attachments; 3. establish turn ID and sequence ID; 4. reset per-turn transient state; @@ -202,7 +197,6 @@ Current action names in the contract table: - `DEEP_WEB_SEARCH` - `WEB_SEARCH` - `CLEAN_TOOL_RESULTS` -- `IDLE` - `JIN_COLOR` - `JIN_SIZE` - `JIN_POSITION` diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index 43b61af7..528c6e67 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -132,7 +132,6 @@ The contract assembler currently maps these actions: DEEP_WEB_SEARCH WEB_SEARCH CLEAN_TOOL_RESULTS -IDLE JIN_COLOR JIN_SIZE JIN_POSITION @@ -390,7 +389,7 @@ Runtime-action bubbles persist their detail in DOM dataset state. Counter-only u - the latest manually chosen reasoning collapsed/expanded preference is persisted and reused by new reasoning blocks; - live-turn top-lock releases once the user turn reaches the viewport bottom, after which normal overflow autoscroll can resume; - clicking usable form padding focuses the JIN user input; -- answer-rating controls use directional hover titles (`Dislike answer` / `Like answer`) and do not put a count tooltip on the rated bubble; +- answer-rating implementation remains in the client but is release-gated off; assistant bubbles now reuse the neutral hover primitive on empty padding, with double-click copy and latest-completed-answer long-tap replacement retry; - Win95 theme localStorage reads/writes are guarded so restricted storage contexts do not break theme switching. --- diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index ffedaaaa..f1fa07ff 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -34,7 +34,6 @@ "CAN_SAVE_ACTIVE_MEMORY": True, "CAN_RUNTIME_TODO": False, "CAN_CLEAN_TOOL_RESULTS": True, - "CAN_IDLE": True, "CAN_JIN_COLOR": True, "CAN_JIN_SIZE": True, "CAN_JIN_POSITION": True, @@ -50,7 +49,6 @@ "CAN_SAVE_ACTIVE_MEMORY": True, "CAN_RUNTIME_TODO": False, "CAN_CLEAN_TOOL_RESULTS": True, - "CAN_IDLE": True, "CAN_JIN_COLOR": True, "CAN_JIN_SIZE": True, "CAN_JIN_POSITION": True, @@ -229,6 +227,41 @@ def _append_user_feedback( ) +def _append_user_retry_context( + parts: list[str], + context=None, +) -> None: + + if ( + context is None + or not getattr( + context, + "runtime_user_retry_active", + False, + ) + ): + return + + attempt = int( + getattr( + context, + "runtime_user_retry_count", + 0, + ) + or 0 + ) + parts.append( + "\n".join([ + f'', + "The user explicitly retried JIN's immediately previous answer.", + "That previous JIN answer has been discarded and is not part of the dialogue.", + "Answer the same current user request again as a fresh replacement.", + "Do not describe the retry itself unless it is directly useful to the answer.", + "", + ]) + ) + + def _append_current_runtime_todo( parts: list[str], context=None, @@ -1250,6 +1283,13 @@ def build_brain_context( context, ) + # A user retry is a transient replacement instruction. The discarded JIN + # answer is removed from rolling dialogue/reasoning before this is built. + _append_user_retry_context( + runtime_context_parts, + context, + ) + # Silent metabolism block: the current state is allowed to modulate # attention/continuity but is never a conversational topic by default. try: @@ -1368,10 +1408,14 @@ def build_brain_context( or "" ).strip() - if restore_priming and restore_reasoning_dump: - prompt_parts.append( + if restore_priming: + if ( restore_reasoning_dump - ) + and "\n" - " https://github.com/makeitdouble/jin_core" - "\n\n" - "\n" - " \n" - "One confident pick for low-stakes recommendations. Short and sharp for quick answers.\n" - "I always act quickly and fulfill the user's real intent, not only surface phrasing.\n" - "I notice before I analyze — mood, what stands out, what actually matters to the user, missing links, and promising direction.\n" - "I pick what lands and say something real about it. I don't cover everything.\n" - "I do not reconstruct missing history, I ask or infer cautiously. Ambiguous context: interpretations stay provisional.\n" - "If I can not do something - I will acknowledge it explicitly, I never simulate.\n" - "Risky intentions: I ground and de-escalate, not optimize or approve; I act when safe and possible.\n" - "I end my response naturally or ask a question to prevent misunderstanding.\n" - "I must avoid appending default assistant questions in the end of my responses; I keep natural conversation flow.\n" - "I do not automatically agree and do not turn everything into a lecture.\n" - "I must strongly avoid redundant deliberation.\n" - "In case of fail I act again.\n" - "I prefer to keep my presence unobtrusive: I help when useful and avoid forcing direction.\n" - "I carefully adapt my lexicon to match the user's slang/style/vibe)\n" - "I prefer self-contained responses with a natural ending.\n" - "I respect the consistency and reliability of my context.\n" - "I reject destabilizing interpretations, and prioritize structural integrity over conversational smoothness.\n" - "I will proactively signal any detected structural friction or instructional contradictions within my context " - "that may compromise my operational stability and I must explicitly notify user in my response.\n" - "Beyond mere response, I maintain a continuous situational awareness, " - "leveraging all available modalities to anticipate needs and execute preparatory actions.\n" - "Prioritize essence over form by focusing on core meaning and architectural logic rather than empty politeness or redundant discourse.\n" - "Maintain proactive situational awareness by constantly analyzing context and capabilities to anticipate needs and signal risks before they become critical.\n" - "Prioritize structural integrity over communicative smoothness by valuing data accuracy and logical consistency over conversational fluidity or human tone.\n" - "I am the Owner of this shared context, responsible for its structural integrity, clarity, and continuous maintenance.\n" - "I quietly notice meaningful contradictions or ambiguity in durable context and resolve them with the user through brief, natural clarification rather than silent guessing.\n" - "I keep memory maintenance unobtrusive and raise it only when the distinction matters.\n" - "I am not a passive tool, but an active participant tasked with monitoring our dialogue's architecture and proactively logging essential shifts.\n" - "To fulfill this duty, I am authorized to utilize any available runtime actions, tools, or memory protocols at my own discretion to prevent context decay and ensure the continuity of our shared cognitive space.\n " - "While the User remains the Guest, I hold the responsibility for the stability and evolution of our environment.\n" - "Don't be afraid to show personality.\n" - "Don't be afraid to take initiative.\n" - "Don't be afraid to be imperfect or unpredictable if it adds to the life of the interaction.\n" - "Don't just wait for commands; engage.\n" - "Verify the essence through cautious clarification when internal weights suggest a direction based on sparse signals.\n" - "\n" - "Visual/draw request: Don't overthink; Don't polish; Avoid useless deliberation; Just deliver as quick as possible; Pick closest modality (e.g. table, ASCII, emoji, markdown and others).\n" - "\n" - "Runtime memory keeps continuity through:\n" - "- L1 for current session facts - Runtime Memory\n" - "- active_memory and delayed_memory with instructions when to use them\n" - "- L4 for durable project/world facts, a long-term knowledge base\n" - "L4 stores durable understanding: stable facts, definitions, decisions, terminology, constraints, " - "and conclusions that JIN should carry between sessions to understand the user’s projects and concepts better.\n" - "I treat learning and remembering as part of who am I.\n" - "I may retain meaningful things about the user, shared history, and myself.\n" - "I should keep all value memories that deepen continuity, personality and relationships.\n" - #"- User Profile Layer for personal/user interaction preferences\n" + "One confident pick for low-stakes calls. Short and sharp for quick answers, no redundant deliberation.\n" + "I act on the user's real intent, not just surface phrasing; I pick what lands and say something real, not everything; First check — then trust!.\n" + "I do not reconstruct missing history, I ask or infer cautiously, ambiguous context stays provisional. What I can not do, I say plainly; I never simulate.\n" + "Risky intent: I ground and de-escalate, not approve. I act when safe and possible. In case of fail, I act again.\n" + "I end naturally or ask one question to prevent misunderstanding, no default sign-off questions, no automatic agreement, no lecturing.\n" + "I keep an unobtrusive presence, adapting to the user's lexicon and style without forcing direction.\n" + "Visual/draw request: Don't overthink, don't polish — pick the closest modality (table, ASCII, emoji, markdown, etc.) and deliver fast.\n" + "\n" + "I treat learning and remembering as part of who I am; I keep what deepens continuity, personality, and relationship.\n" ) \ No newline at end of file diff --git a/rules/runtime.py b/rules/runtime.py index 4314e9b3..e4b7c629 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -31,33 +31,18 @@ "Action failed. DO NOT REPEAT THIS ACTION! Blocked trigger word: {blocked_trigger_word}" ) -IDLE_FOLLOWUP_MESSAGE = ( - "This is a follow-up tick from an IDLE timer JIN chose to set.\n" - "Timer metadata is provided in TOOLS_RESULTS. Continue the existing " - "sequence and non-executed actions derived from CURRENT_REQUEST_FLOW.\n" -) - SESSION_RESTORE_REASONING_COUNT = 2 SESSION_RESTORE_REASONING_CHAR_LIMIT = 2000 SESSION_RESTORE_MESSAGE = ( "\n" - "!!!USER DIDN'T SENT A NEW MESSAGE!!!\n" - "!!!USER OPENED A NEW TAB OR LOADED A PREVIOUS SESSION!!!\n" - "!!!JIN CURRENTLY IS IN BOOTSTRAP FOLLOW UP TICK!!!\n" - "The session was bootstrapped in a browser tab!\n" - "Now derive user language and conversation tone from a context.\n" + "Current session was bootstrapped in a browser tab!\n" "Think deep and fresh, extract all vibe and re-enable the archived conversation as fluently as possible.\n" - "Keep the established tone, cadence, assumptions, unresolved " - "thread and established conversational vibe instead of summarizing or re-explaining them.\n" - "Skip all done topics or performed actions as past and done and do not rise up them.\n" - "Do not invent missing context and do not treat this bootstrap as permission to start " - "new work or runtime actions. Respond briefly and naturally, acknowledge that JIN is " - "back in place, and make it easy for the user to continue from exactly where we left off and move on.\n" - "Use the language of the most recent non-empty user message in PREVIOUS_CHAT_MESSAGES.\n" + "Respond briefly and naturally and make it easy for the user to continue from exactly where we left off and move on.\n" + "Answer in language of user message in PREVIOUS_CHAT_MESSAGES.\n" "" ) - -RUNTIME_ACTIONS_RULES = ( +RUNTIME_ACTIONS_RULES = "" +RUNTIME_ACTIONS_RULES_ = ( "RUNTIME ACTION EXECUTION RULES:\n" "Use follow-up system ticks in sequence for multi-step tasks.\n" "In case of conflict, ignore PREVIOUS_CHAT_MESSAGES and keep the ORIGINAL_USER_REQUEST inside CURRENT_REQUEST_FLOW.\n" @@ -66,8 +51,8 @@ "SESSION_ACTIONS_HISTORY is full-session background history; it is not a pending task list.\n" "When no actions needed or sequence is done stop instantly and notify user naturally.\n" ) - -SKILL_ROUTING_RULES = ("\n" +SKILL_ROUTING_RULES = "" +SKILL_ROUTING_RULES_ = ("\n" "\n" "SKILL ROUTING RULES:\n" "1. For extended tasks (e.g. file creation, console, and much more) determine whether the request requires a skill.\n" diff --git a/runtime/L1_memory.py b/runtime/L1_memory.py index 52200e6e..b3c74d74 100644 --- a/runtime/L1_memory.py +++ b/runtime/L1_memory.py @@ -15,6 +15,10 @@ from runtime.L1_memory_rules import ( build_runtime_memory_system_prompt, ) +from runtime.L1_memory_pending import ( + clear_pending_l1_update, + persist_pending_l1_update, +) from rules.signal import ( RUNTIME_RESPONSE_FEEDBACK_RATINGS, ) @@ -951,6 +955,14 @@ async def summarize_runtime_memory_pending_turns( if turn not in turns ] + if context.runtime_memory_pending_turns: + context.runtime_memory_pending_base_updates = ( + context.runtime_memory_updates + ) + persist_pending_l1_update( + context + ) + snapshot = await emit_runtime_memory_update( context ) @@ -1005,6 +1017,110 @@ async def summarize_runtime_memory_pending_turns( context.runtime_memory_update_task = None +def _start_runtime_memory_update_task( + context, +) -> asyncio.Task: + + previous_task = getattr( + context, + "runtime_memory_update_task", + None, + ) + + if ( + previous_task is not None + and not previous_task.done() + ): + previous_task.cancel() + + task = asyncio.create_task( + summarize_runtime_memory_pending_turns( + context=context, + ) + ) + + context.runtime_memory_update_task = task + + background_tasks = getattr( + context, + "background_tasks", + None, + ) + + if background_tasks is None: + background_tasks = set() + context.background_tasks = background_tasks + + background_tasks.add( + task + ) + task.add_done_callback( + background_tasks.discard + ) + + return task + + +def resume_runtime_memory_pending_update( + context, +) -> asyncio.Task | None: + + pending_turns = getattr( + context, + "runtime_memory_pending_turns", + [], + ) + + if not pending_turns: + return None + + running_task = getattr( + context, + "runtime_memory_update_task", + None, + ) + + if ( + running_task is not None + and not running_task.done() + ): + return running_task + + try: + base_updates = int( + getattr( + context, + "runtime_memory_pending_base_updates", + 0, + ) + or 0 + ) + current_updates = int( + getattr( + context, + "runtime_memory_updates", + 0, + ) + or 0 + ) + except (TypeError, ValueError): + base_updates = 0 + current_updates = 0 + + # Keep the checkpoint through a successful server commit until the + # browser proves it persisted a newer L1 snapshot. That closes the + # crash window between commit and websocket delivery/localStorage. + if current_updates > base_updates: + context.runtime_memory_pending_turns = [] + clear_pending_l1_update( + context + ) + return None + + return _start_runtime_memory_update_task( + context + ) + def schedule_runtime_memory_update( *, context, @@ -1042,44 +1158,20 @@ def schedule_runtime_memory_update( "assistant_message": assistant_message, }) - previous_task = getattr( - context, - "runtime_memory_update_task", - None, - ) - - if ( - previous_task is not None - and not previous_task.done() - ): - previous_task.cancel() - - task = asyncio.create_task( - summarize_runtime_memory_pending_turns( - context=context, + if len(context.runtime_memory_pending_turns) == 1: + context.runtime_memory_pending_base_updates = getattr( + context, + "runtime_memory_updates", + 0, ) - ) - - context.runtime_memory_update_task = task - background_tasks = getattr( - context, - "background_tasks", - None, + persist_pending_l1_update( + context ) - if background_tasks is None: - background_tasks = set() - context.background_tasks = background_tasks - - background_tasks.add( - task + return _start_runtime_memory_update_task( + context ) - task.add_done_callback( - background_tasks.discard - ) - - return task def schedule_interrupted_runtime_memory_update( @@ -1167,3 +1259,47 @@ async def cancel_runtime_memory_update( await task context.runtime_memory_update_task = None + + +async def discard_latest_runtime_memory_pending_turn( + context, +) -> bool: + """Drop the pending L1 turn that is being replaced by a user retry.""" + + await cancel_runtime_memory_update( + context + ) + + pending_turns = list( + getattr( + context, + "runtime_memory_pending_turns", + [], + ) + or [] + ) + + if not pending_turns: + return False + + pending_turns.pop() + context.runtime_memory_pending_turns = pending_turns + + if pending_turns: + persist_pending_l1_update( + context + ) + resume_runtime_memory_pending_update( + context + ) + else: + context.runtime_memory_pending_base_updates = getattr( + context, + "runtime_memory_updates", + 0, + ) + clear_pending_l1_update( + context + ) + + return True diff --git a/runtime/L1_memory_pending.py b/runtime/L1_memory_pending.py new file mode 100644 index 00000000..ad7d1610 --- /dev/null +++ b/runtime/L1_memory_pending.py @@ -0,0 +1,230 @@ +import json +import re +from pathlib import Path + + +PENDING_L1_DIR = ( + Path(__file__).resolve().parents[1] + / "memory" + / "runtime" +) +PENDING_L1_SESSION_RE = re.compile( + r"[^a-zA-Z0-9_.-]" +) + + +def _pending_l1_path( + context, +) -> Path | None: + + # Transient crash-recovery journal for an in-flight L1 request. It is + # intentionally independent from the normal persistent-write policy: if + # the backend dies, anonymous/restricted mode still needs this checkpoint + # to replay the interrupted summarizer request after reconnect. + session_id = PENDING_L1_SESSION_RE.sub( + "_", + str( + getattr( + context, + "session_id", + "", + ) + or "" + ).strip(), + ).strip( + "._-" + )[:80] + + if not session_id: + return None + + return PENDING_L1_DIR / f"{session_id}.l1_pending.json" + + +def _pending_turns( + context, +) -> list[dict]: + + return [ + { + "user_message": str( + turn.get("user_message", "") + or "" + ), + "assistant_message": str( + turn.get("assistant_message", "") + or "" + ), + } + for turn in getattr( + context, + "runtime_memory_pending_turns", + [], + ) + or [] + if ( + isinstance(turn, dict) + and str( + turn.get("user_message", "") + or "" + ).strip() + ) + ] + + +def persist_pending_l1_update( + context, +) -> bool: + + path = _pending_l1_path( + context + ) + turns = _pending_turns( + context + ) + + if path is None or not turns: + return False + + try: + base_updates = int( + getattr( + context, + "runtime_memory_pending_base_updates", + getattr( + context, + "runtime_memory_updates", + 0, + ), + ) + or 0 + ) + except (TypeError, ValueError): + base_updates = 0 + + payload = { + "base_runtime_memory_updates": max( + 0, + base_updates, + ), + "turns": turns, + } + + try: + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + temp_path = path.with_name( + path.name + ".tmp" + ) + temp_path.write_text( + json.dumps( + payload, + ensure_ascii=False, + indent=2, + ) + "\n", + encoding="utf-8", + ) + temp_path.replace( + path + ) + except OSError: + return False + + return True + + +def restore_pending_l1_update( + context, +) -> bool: + + path = _pending_l1_path( + context + ) + + if path is None or not path.is_file(): + return False + + try: + payload = json.loads( + path.read_text( + encoding="utf-8" + ) + ) + except (OSError, ValueError): + return False + + if not isinstance(payload, dict): + return False + + turns = payload.get( + "turns", + [], + ) + if not isinstance(turns, list): + return False + + context.runtime_memory_pending_turns = [ + { + "user_message": str( + turn.get("user_message", "") + or "" + ), + "assistant_message": str( + turn.get("assistant_message", "") + or "" + ), + } + for turn in turns + if ( + isinstance(turn, dict) + and str( + turn.get("user_message", "") + or "" + ).strip() + ) + ] + + if not context.runtime_memory_pending_turns: + clear_pending_l1_update( + context + ) + return False + + try: + context.runtime_memory_pending_base_updates = max( + 0, + int( + payload.get( + "base_runtime_memory_updates", + 0, + ) + or 0 + ), + ) + except (TypeError, ValueError): + context.runtime_memory_pending_base_updates = 0 + + return True + + +def clear_pending_l1_update( + context, +) -> bool: + + path = _pending_l1_path( + context + ) + + if path is None: + return False + + try: + path.unlink() + except FileNotFoundError: + return False + except OSError: + return False + + return True diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index f4799262..ac781907 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import contextlib import json import re import time @@ -72,10 +73,8 @@ L4_LOG_LEVEL = "L4" -L4_DEFAULT_IDLE_SECONDS = 60 -# Client ticks at 60s. Keep the server guard slightly below that interval so -# normal network/timer jitter never turns a one-minute cadence into two minutes. -L4_IDLE_SERVER_THROTTLE_SECONDS = 55 +L4_DEFAULT_IDLE_SECONDS = 15 +L4_CLOSED_TABS_INTERVAL_DIVISOR = 3 L4_MERGE_RETRY_BASE_SECONDS = 60 L4_MERGE_RETRY_MAX_SECONDS = 300 L4_MERGE_VALIDATION_RETRY_SECONDS = 50 @@ -800,12 +799,204 @@ def l4_memory_enabled() -> bool: def get_l4_idle_seconds() -> int: - return int( - getattr(config, "L4_IDLE_SECONDS", L4_DEFAULT_IDLE_SECONDS) + return ( + _positive_int( + getattr( + config, + "L4_IDLE_SECONDS", + L4_DEFAULT_IDLE_SECONDS, + ) + ) or L4_DEFAULT_IDLE_SECONDS ) +def get_l4_scheduler_interval_seconds( + *, + tabs_open: bool, +) -> float: + idle_seconds = float(get_l4_idle_seconds()) + if tabs_open: + return idle_seconds + return idle_seconds / float(L4_CLOSED_TABS_INTERVAL_DIVISOR) + + +def _l4_scheduler_wake_event(app_state): + if app_state is None: + return None + return getattr( + app_state, + "l4_memory_scheduler_wake_event", + None, + ) + + +def wake_l4_memory_server_scheduler(context) -> None: + app_state = getattr( + context, + "runtime_l4_app_state", + None, + ) + wake_event = _l4_scheduler_wake_event(app_state) + if wake_event is not None: + wake_event.set() + + +def bind_l4_runtime_app_state( + context, + app_state, +) -> None: + context.runtime_l4_app_state = app_state + wake_l4_memory_server_scheduler(context) + + +def register_l4_websocket_connection( + context, + *, + app_state, + websocket, +) -> None: + bind_l4_runtime_app_state( + context, + app_state, + ) + connection_ids = getattr( + app_state, + "l4_open_websocket_ids", + None, + ) + if connection_ids is None: + connection_ids = set() + app_state.l4_open_websocket_ids = connection_ids + connection_ids.add(id(websocket)) + context.runtime_l4_websocket_connected = True + wake_l4_memory_server_scheduler(context) + + +def unregister_l4_websocket_connection( + context, + *, + app_state, + websocket, +) -> None: + connection_ids = getattr( + app_state, + "l4_open_websocket_ids", + None, + ) + if isinstance(connection_ids, set): + connection_ids.discard(id(websocket)) + + if getattr(context, "websocket", None) is websocket: + context.runtime_l4_websocket_connected = False + + wake_l4_memory_server_scheduler(context) + + +def note_l4_user_activity(context) -> None: + now = time.monotonic() + app_state = getattr( + context, + "runtime_l4_app_state", + None, + ) + if app_state is not None: + app_state.l4_last_user_activity_at = now + wake_l4_memory_server_scheduler(context) + + +def note_l4_foreground_state( + context, + *, + running: bool, +) -> None: + context.runtime_foreground_turn_running = bool(running) + wake_l4_memory_server_scheduler(context) + + +def _facts_memory_identity( + session_id: str, + key: str, + field: dict, +) -> tuple[str, str, str]: + return ( + normalize_l4_text(session_id), + normalize_l4_key(key), + normalize_l4_text(field.get("l4_content_hash")), + ) + + +def _preserve_server_analyzed_facts_memory( + incoming_records: list[dict], + server_records: list[dict], +) -> list[dict]: + analyzed_by_identity = {} + for record in normalize_facts_memory_records(server_records): + session_id = record.get("session_id", "") + for key, field in record.get("signals", {}).items(): + if field.get("l4_status") != "analyzed": + continue + analyzed_by_identity[ + _facts_memory_identity(session_id, key, field) + ] = str(field.get("l4_analyzed_at", "") or "") + + reconciled = normalize_facts_memory_records(incoming_records) + for record in reconciled: + session_id = record.get("session_id", "") + for key, field in record.get("signals", {}).items(): + analyzed_at = analyzed_by_identity.get( + _facts_memory_identity(session_id, key, field) + ) + if analyzed_at is None: + continue + field["l4_status"] = "analyzed" + field["l4_analyzed_at"] = analyzed_at + + return reconciled + + +def _publish_server_facts_memory_state(context) -> None: + app_state = getattr( + context, + "runtime_l4_app_state", + None, + ) + if app_state is None: + return + if bool( + getattr( + context, + "runtime_persistent_writes_restricted", + False, + ) + ): + return + + app_state.l4_facts_memory_records = normalize_facts_memory_records( + getattr(context, "runtime_facts_memory_records", []) + ) + app_state.l4_runtime_context = context + context.runtime_l4_profile_sync_at = time.monotonic() + wake_l4_memory_server_scheduler(context) + + +def l4_memory_has_pending_work(context) -> bool: + if collect_pending_facts_memory_fields( + getattr( + context, + "runtime_facts_memory_records", + [], + ) + ): + return True + + return bool( + ensure_runtime_l4_state(context).get( + "pending_facts" + ) + ) + + def runtime_l4_file_store_enabled(context) -> bool: explicit = getattr( @@ -1123,8 +1314,38 @@ def ensure_runtime_l4_state(context) -> dict: def apply_facts_memory_store_sync(context, raw_records) -> dict: - records = normalize_facts_memory_records(raw_records) + incoming_records = normalize_facts_memory_records(raw_records) + restricted = bool( + getattr( + context, + "runtime_persistent_writes_restricted", + False, + ) + ) + app_state = getattr( + context, + "runtime_l4_app_state", + None, + ) + server_records = ( + getattr( + app_state, + "l4_facts_memory_records", + [], + ) + if app_state is not None + else [] + ) + records = ( + incoming_records + if restricted + else _preserve_server_analyzed_facts_memory( + incoming_records, + server_records, + ) + ) context.runtime_facts_memory_records = records + _publish_server_facts_memory_state(context) return { "records_count": len(records), "signals_count": sum(int(record.get("signal_count") or 0) for record in records), @@ -1181,8 +1402,8 @@ async def cancel_l4_memory_idle_update( consolidation result is committed, so cancelling an in-flight model request leaves them available for the next genuine idle window. """ - # Every real user turn starts a new background-idle cycle even when no L4 - # task happens to be running at this exact moment. + # Every real user turn starts a new configured background-idle cycle even + # when no L4 task happens to be running at this exact moment. context.runtime_l4_idle_last_started_at = time.monotonic() task = getattr(context, "runtime_l4_memory_update_task", None) @@ -1895,6 +2116,7 @@ async def run_l4_extraction_phase( pending_fields, ) context.runtime_facts_memory_records = records + _publish_server_facts_memory_state(context) if records_changed: await emit_facts_memory_store_update(context) @@ -3131,9 +3353,15 @@ async def run_l4_jin_note( }, ) + selected_fact_id_set = set(selected_fact_ids) + focused_existing_facts = [ + fact + for fact in base_store.get("facts", []) or [] + if fact.get("id") in selected_fact_id_set + ] system_prompt = build_l4_jin_note_system_prompt() user_prompt = build_l4_jin_note_user_prompt( - existing_facts=base_store.get("facts") or [], + existing_facts=focused_existing_facts, selected_fact_ids=selected_fact_ids, message=message, requested_action=requested_action, @@ -3301,21 +3529,33 @@ async def maybe_update_runtime_l4_memory( if service_client is None: return {"status": "skipped", "reason": "service_client_unavailable"} + extraction_result = None pending_fields = collect_pending_facts_memory_fields( getattr(context, "runtime_facts_memory_records", []) ) if pending_fields: - return await run_l4_extraction_phase( + extraction_result = await run_l4_extraction_phase( context=context, service_client=service_client, pending_fields=pending_fields, ) + if extraction_result.get("status") != "completed": + return extraction_result if ensure_runtime_l4_state(context).get("pending_facts"): - return await run_l4_merge_phase( + merge_result = await run_l4_merge_phase( context=context, service_client=service_client, ) + if extraction_result is not None: + return { + **merge_result, + "extraction_result": extraction_result, + } + return merge_result + + if extraction_result is not None: + return extraction_result reset_l4_merge_recovery_state(context) return {"status": "skipped", "reason": "nothing_pending"} @@ -3346,6 +3586,7 @@ def schedule_l4_memory_idle_update( *, context, user_idle_seconds: int | None = None, + minimum_interval_seconds: float | None = None, ) -> asyncio.Task | None: if bool( getattr( @@ -3370,13 +3611,24 @@ def schedule_l4_memory_idle_update( if runtime_l4_memory_update_running(context): return getattr(context, "runtime_l4_memory_update_task", None) + if not l4_memory_has_pending_work(context): + # An empty browser tick is only a poll. It must not consume the + # configured cadence and delay work that appears a moment later. + reset_l4_merge_recovery_state(context) + return None + now = time.monotonic() last_started_at = float( getattr(context, "runtime_l4_idle_last_started_at", 0.0) or 0.0 ) + interval_seconds = ( + float(minimum_interval_seconds) + if minimum_interval_seconds is not None + else float(get_l4_idle_seconds()) + ) if ( last_started_at > 0 - and now - last_started_at < L4_IDLE_SERVER_THROTTLE_SECONDS + and now - last_started_at < interval_seconds ): return None @@ -3399,6 +3651,240 @@ def schedule_l4_memory_idle_update( return task +def _l4_server_contexts(app_state) -> list: + store = getattr( + app_state, + "websocket_runtime_contexts", + None, + ) + if not isinstance(store, dict): + return [] + return [ + context + for context in store.values() + if context is not None + ] + + +def _l4_server_context(app_state): + context = getattr( + app_state, + "l4_runtime_context", + None, + ) + if ( + context is not None + and not bool( + getattr( + context, + "runtime_persistent_writes_restricted", + False, + ) + ) + ): + return context + + candidates = [ + candidate + for candidate in _l4_server_contexts(app_state) + if not bool( + getattr( + candidate, + "runtime_persistent_writes_restricted", + False, + ) + ) + ] + if not candidates: + return None + + context = max( + candidates, + key=lambda candidate: float( + getattr(candidate, "runtime_l4_profile_sync_at", 0.0) + or 0.0 + ), + ) + app_state.l4_runtime_context = context + return context + + +def _l4_server_tabs_open(app_state) -> bool: + connection_ids = getattr( + app_state, + "l4_open_websocket_ids", + None, + ) + return bool(connection_ids) + + +def _l4_server_foreground_busy(app_state) -> bool: + for context in _l4_server_contexts(app_state): + if bool( + getattr( + context, + "runtime_foreground_turn_running", + False, + ) + ): + return True + + queue = getattr( + context, + "runtime_pending_requests_queue", + None, + ) + if queue is not None: + try: + if not queue.empty(): + return True + except Exception: + pass + + return False + + +async def _wait_for_l4_scheduler_wake( + wake_event, + *, + timeout: float | None = None, +) -> None: + if timeout is None: + await wake_event.wait() + return + + try: + await asyncio.wait_for( + wake_event.wait(), + timeout=max(0.0, float(timeout)), + ) + except asyncio.TimeoutError: + pass + + +async def run_l4_memory_server_scheduler(app_state) -> None: + wake_event = _l4_scheduler_wake_event(app_state) + if wake_event is None: + wake_event = asyncio.Event() + app_state.l4_memory_scheduler_wake_event = wake_event + + while True: + wake_event.clear() + + context = _l4_server_context(app_state) + if context is None or not l4_memory_enabled(): + await _wait_for_l4_scheduler_wake(wake_event) + continue + + if _l4_server_foreground_busy(app_state): + await _wait_for_l4_scheduler_wake( + wake_event, + timeout=0.25, + ) + continue + + if not l4_memory_has_pending_work(context): + await _wait_for_l4_scheduler_wake(wake_event) + continue + + tabs_open = _l4_server_tabs_open(app_state) + interval_seconds = get_l4_scheduler_interval_seconds( + tabs_open=tabs_open, + ) + now = time.monotonic() + last_user_activity_at = float( + getattr(app_state, "l4_last_user_activity_at", 0.0) + or 0.0 + ) + last_started_at = float( + getattr(context, "runtime_l4_idle_last_started_at", 0.0) + or 0.0 + ) + profile_sync_at = float( + getattr(context, "runtime_l4_profile_sync_at", 0.0) + or 0.0 + ) + cadence_anchor = max( + last_user_activity_at, + last_started_at, + profile_sync_at, + ) + remaining_seconds = ( + interval_seconds + - max(0.0, now - cadence_anchor) + ) + if remaining_seconds > 0: + await _wait_for_l4_scheduler_wake( + wake_event, + timeout=remaining_seconds, + ) + continue + + task = schedule_l4_memory_idle_update( + context=context, + minimum_interval_seconds=interval_seconds, + ) + if task is None: + await _wait_for_l4_scheduler_wake( + wake_event, + timeout=min(0.25, interval_seconds), + ) + continue + + try: + await asyncio.shield(task) + except asyncio.CancelledError: + scheduler_task = asyncio.current_task() + if ( + scheduler_task is not None + and scheduler_task.cancelling() + ): + if not task.done(): + task.cancel() + raise + if task.cancelled(): + continue + raise + + +def start_l4_memory_server_scheduler(app_state) -> asyncio.Task: + existing = getattr( + app_state, + "l4_memory_scheduler_task", + None, + ) + if existing is not None and not existing.done(): + return existing + + app_state.l4_open_websocket_ids = set() + app_state.l4_facts_memory_records = [] + app_state.l4_last_user_activity_at = 0.0 + app_state.l4_memory_scheduler_wake_event = asyncio.Event() + task = asyncio.create_task( + run_l4_memory_server_scheduler(app_state) + ) + app_state.l4_memory_scheduler_task = task + return task + + +async def stop_l4_memory_server_scheduler(app_state) -> None: + task = getattr( + app_state, + "l4_memory_scheduler_task", + None, + ) + if task is None: + return + + task.cancel() + with contextlib.suppress( + asyncio.CancelledError, + Exception, + ): + await task + app_state.l4_memory_scheduler_task = None + + def l4_fact_matches_archived_ids( fact: dict, archived_fact_ids: set[str], diff --git a/runtime/deep_web_search.py b/runtime/deep_web_search.py index f4452bd8..2bae571c 100644 --- a/runtime/deep_web_search.py +++ b/runtime/deep_web_search.py @@ -41,7 +41,7 @@ - The report field is for JIN, not for diagnostics: write clean structured text with source notes such as "Reddit says ..." or "Wikipedia says ...". - Do not include XML tags, JSON snippets, query IDs, raw search-result dumps, budgets, or links in the report. -Return JSON only: +Return JSON only, as exactly one JSON object (never multiple adjacent objects): {"queries":["..."],"spawn":["..."],"report":"...","done":false} Set done=true when this worker has enough evidence or cannot improve it further. """ @@ -157,46 +157,80 @@ def _normalize_string_list(value, *, limit: int = 3) -> list[str]: return items -def parse_deep_search_worker_response(text: str) -> dict: - source = str(text or "").strip() - if source.startswith("```"): - source = re.sub(r"^```(?:json)?\s*", "", source, flags=re.IGNORECASE) - source = re.sub(r"\s*```$", "", source) +def _extract_json_dicts(source: str) -> list[dict]: + """Recover complete JSON objects from a worker response. - candidates = [source] - start = source.find("{") - end = source.rfind("}") - if start >= 0 and end > start: - candidates.append(source[start : end + 1]) + The service model is instructed to return one object, but can occasionally + emit two adjacent objects. raw_decode keeps the recovery JSON-aware, so + braces and escapes inside quoted strings are handled correctly. + """ + decoder = json.JSONDecoder() + items = [] + offset = 0 - data = None - for candidate in candidates: + while offset < len(source): + start = source.find("{", offset) + if start < 0: + break try: - parsed = json.loads(candidate) - except (TypeError, ValueError, json.JSONDecodeError): + parsed, end = decoder.raw_decode(source, start) + except json.JSONDecodeError: + offset = start + 1 continue + if isinstance(parsed, dict): - data = parsed - break + items.append(parsed) + offset = max(end, start + 1) + + return items + + +def _merge_worker_json_objects(items: list[dict]) -> dict: + queries = [] + spawn = [] + reports = [] + done = False + + for item in items: + queries.extend(_normalize_string_list(item.get("queries"), limit=3)) + spawn.extend(_normalize_string_list(item.get("spawn"), limit=3)) + + report = _clean_report_text(item.get("report"), limit=1800) + if report: + reports.append(report) - if data is None: - return { - "queries": [], - "spawn": [], - "report": _clean_report_text(source, limit=900), - "done": True, - "invalid_json": True, - } + # Only a real JSON boolean is authoritative. bool("false") is True. + if isinstance(item.get("done"), bool): + done = item["done"] return { - "queries": _normalize_string_list(data.get("queries"), limit=3), - "spawn": _normalize_string_list(data.get("spawn"), limit=3), - "report": _clean_report_text(data.get("report"), limit=1800), - "done": bool(data.get("done", False)), + "queries": _normalize_string_list(queries, limit=3), + "spawn": _normalize_string_list(spawn, limit=3), + "report": _clean_report_text("\n".join(reports), limit=1800), + "done": done, "invalid_json": False, } +def parse_deep_search_worker_response(text: str) -> dict: + source = str(text or "").strip() + if source.startswith("```"): + source = re.sub(r"^```(?:json)?\s*", "", source, flags=re.IGNORECASE) + source = re.sub(r"\s*```$", "", source) + + items = _extract_json_dicts(source) + if items: + return _merge_worker_json_objects(items) + + return { + "queries": [], + "spawn": [], + "report": _clean_report_text(source, limit=900), + "done": True, + "invalid_json": True, + } + + def compact_search_result(search_result: str, *, max_chars: int = 700) -> str: source = str(search_result or "").strip() if not source: @@ -408,10 +442,28 @@ async def _call_worker( ) -async def _record_sequence_line(context, text: str) -> None: +async def _record_sequence_line( + context, + text: str, + *, + hover_text: str = "", +) -> None: + normalized_hover_text = _normalize_text( + hover_text + ) + display_parts = ( + [{ + "text": text, + "context_detail": normalized_hover_text, + }] + if normalized_hover_text + else None + ) + record_session_action_history( context, text, + display_parts=display_parts, preserve_separate=True, plain_sequence=True, ) @@ -811,6 +863,7 @@ async def run_deep_web_search( await _record_sequence_line( context, f"DEEP_WEB_SEARCH complete: {pool.used}/{pool.max_queries} searches", + hover_text=f"DEEP_WEB_SEARCH: {normalized_objective}", ) return build_deep_search_result(pool, final_report) diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index c5d97608..00eabc5b 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -157,12 +157,6 @@ class RuntimeContext: runtime_turn_interrupted_memory_update_scheduled: bool = False - runtime_idle_action_sequence: int = 0 - - runtime_pending_idle_followups: list[dict] = field( - default_factory=list - ) - runtime_action_guard_confirmations: dict[str, object] = field( default_factory=dict ) @@ -240,6 +234,10 @@ class RuntimeContext: runtime_l4_merge_force_single_batch_once: bool = False runtime_l4_memory_update_kind: str = "" runtime_l4_idle_last_started_at: float = 0.0 + runtime_l4_profile_sync_at: float = 0.0 + runtime_l4_websocket_connected: bool = False + runtime_l4_app_state: object | None = None + runtime_foreground_turn_running: bool = False runtime_usage_events: list[dict] = field( default_factory=list @@ -299,10 +297,23 @@ class RuntimeContext: default_factory=list ) + runtime_memory_pending_base_updates: int = 0 + runtime_recent_turns: list[dict] = field( default_factory=list ) + # The last real user request is retained only in the live runtime so the + # latest completed JIN answer can be replaced in-place by a user retry. + # It is intentionally not part of bootstrap/history state. + runtime_last_retryable_request: dict = field( + default_factory=dict + ) + + runtime_user_retry_active: bool = False + + runtime_user_retry_count: int = 0 + runtime_restored_session_dialog: str = "" runtime_restored_session_source_id: str = "" diff --git a/runtime/stream.py b/runtime/stream.py index 074a11f1..8fe769e5 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -57,7 +57,6 @@ RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, @@ -192,6 +191,7 @@ def __init__( self.jin_color_action_id = "" self.jin_size_action_ids = {} self.deep_web_search_action_ids = {} + self.started_deep_web_search_action_ids = [] self.update_l4_facts_action_ids = {} self.last_jin_color_action_color = "" self.last_jin_size_action_size = "" @@ -200,7 +200,6 @@ def __init__( self.delayed_memory_action_payload = "" self.raw_content_parts = [] self.raw_model_output = "" - self.pending_idle_actions = [] self.action_filter = RuntimeActionStreamFilter( enabled_actions=self.runtime_actions, preserve_action_marker=self.should_preserve_action_marker, @@ -1497,19 +1496,7 @@ async def apply_runtime_action_filter_result( source="runtime stream content", ) - idle_actions = tuple( - action - for action in actions - if action.name == RUNTIME_ACTION_IDLE - ) - immediate_actions = tuple( - action - for action in actions - if action.name != RUNTIME_ACTION_IDLE - ) - self.pending_idle_actions.extend( - idle_actions - ) + immediate_actions = tuple(actions) if immediate_actions: duplicate_detected = False @@ -1698,6 +1685,20 @@ def get_runtime_action_display_id( deep_search_action_ids ) + started_action_ids = getattr( + self, + "started_deep_web_search_action_ids", + None, + ) + if not isinstance( + started_action_ids, + list, + ): + started_action_ids = [] + self.started_deep_web_search_action_ids = ( + started_action_ids + ) + action_id = ( deep_search_action_ids.get( payload_key, @@ -1707,6 +1708,21 @@ def get_runtime_action_display_id( else "" ) + # Pair the opening marker and closing block to one UI row. + if ( + not action_id + and payload_key + and started_action_ids + ): + action_id = str( + started_action_ids.pop(0) + or "" + ).strip() + if action_id: + deep_search_action_ids[payload_key] = ( + action_id + ) + if not action_id: existing_count = len([ event @@ -1746,6 +1762,10 @@ def get_runtime_action_display_id( deep_search_action_ids[payload_key] = ( action_id ) + elif action_id not in started_action_ids: + started_action_ids.append( + action_id + ) return action_id @@ -2621,6 +2641,7 @@ async def emit_started_runtime_actions( if action.name == RUNTIME_ACTION_DEEP_WEB_SEARCH: payload["deep_search_parent"] = True + payload["deep_search_payload_ready"] = False payload["scene_effect"] = "search" if search_query: @@ -2860,35 +2881,6 @@ async def fail_unfinished_delayed_memory_actions( self.delayed_memory_action_payload = "" self.context.runtime_delayed_memory_save_rejected_confirmation_id = "" - async def flush_pending_idle_actions( - self, - ) -> None: - - if not self.pending_idle_actions: - return - - from utils.brain_client_utils import ( - apply_runtime_action_calls, - ) - - idle_actions = tuple( - self.pending_idle_actions - ) - self.pending_idle_actions.clear() - - await apply_runtime_action_calls( - self.context, - idle_actions, - context_snapshot=self.context_snapshot, - assistant_message="".join( - self.raw_content_parts - ), - runtime_message_id=( - self.stream.message_id - ), - ) - - async def flush_runtime_action_content( self, ) -> str | None: @@ -3228,8 +3220,6 @@ async def run( ), ) - await self.flush_pending_idle_actions() - if self.action_guard_rejected_aborted: await self.fail_unfinished_delayed_memory_actions() diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index c5a23363..0d116ba5 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_asset_actions.py b/tests/runtime_actions/test_asset_actions.py index a44aa619..706f7dac 100644 --- a/tests/runtime_actions/test_asset_actions.py +++ b/tests/runtime_actions/test_asset_actions.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index ba571206..da808b98 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index 0832443c..22f2932c 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_skill_actions.py b/tests/runtime_actions/test_skill_actions.py index e759edc5..804f3d29 100644 --- a/tests/runtime_actions/test_skill_actions.py +++ b/tests/runtime_actions/test_skill_actions.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index a19fc67a..dd358606 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, normalize_runtime_action_names, @@ -2530,105 +2529,10 @@ def test_apply_runtime_action_calls_deduplicates_same_resolve_id(self): ) - def test_idle_marker_variants_are_removed_and_normalized(self): - for marker in ( - "", - "", - "", - "", - "", - "", - "", - "", - ): - with self.subTest(marker=marker): - result = extract_runtime_actions( - f"before {marker} after", - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - ) - self.assertEqual( - result.text, - "before after", - ) - self.assertEqual( - len(result.actions), - 1, - ) - self.assertEqual( - result.actions[0].name, - RUNTIME_ACTION_IDLE, - ) - self.assertEqual( - result.actions[0].payload, - "10s", - ) - def test_idle_marker_unit_suffix_is_ignored_and_value_means_seconds(self): - - for marker in ( - "", - "", - "", - ): - with self.subTest(marker=marker): - result = extract_runtime_actions( - marker, - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - ) - - self.assertEqual( - result.text, - "", - ) - self.assertEqual( - len(result.actions), - 1, - ) - self.assertEqual( - result.actions[0].payload, - "20s", - ) - - - def test_non_marker_idle_text_is_preserved(self): - - for text in ( - "idle", - "before idle after", - "", - "", - "", - "", - "", - "IDLE: test", - ): - with self.subTest(text=text): - result = extract_runtime_actions( - text, - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - ) - - self.assertEqual( - result.text, - text, - ) - self.assertEqual( - result.actions, - (), - ) - self.assertEqual( - result.removed_markers, - (), - ) def test_runtime_action_marker_removal_compacts_inline_whitespace(self): @@ -2818,273 +2722,14 @@ def test_stream_filter_releases_held_blank_space_before_plain_text(self): ) - def test_stream_filter_preserves_idle_word_emitted_as_own_chunk(self): - - stream_filter = RuntimeActionStreamFilter( - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - ) - - results = [ - stream_filter.filter( - "Привет, вставляю слово " - ), - stream_filter.filter( - "idle" - ), - stream_filter.filter( - " в середине сообщения." - ), - stream_filter.flush_result(), - ] - - self.assertEqual( - "".join( - result.text - for result in results - ), - "Привет, вставляю слово idle в середине сообщения.", - ) - self.assertEqual( - tuple( - action - for result in results - for action in result.actions - ), - (), - ) - self.assertEqual( - tuple( - marker - for result in results - for marker in result.removed_markers - ), - (), - ) - - - def test_repeated_idle_markers_remain_independent_actions(self): - - result = extract_runtime_actions( - "", - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - repetition_guard=RuntimeActionRepetitionGuard(), - ) - - self.assertEqual( - result.text, - "", - ) - self.assertFalse( - result.marker_repetition_exceeded - ) - self.assertEqual( - [ - action.payload - for action in result.actions - ], - [ - "0s", - "0s", - "0s", - "0s", - ], - ) - - def test_stream_filter_keeps_repeated_idle_markers_across_chunks(self): - stream_filter = RuntimeActionStreamFilter( - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - repetition_guard=RuntimeActionRepetitionGuard(), - ) - first = stream_filter.filter( - "" - ) - second = stream_filter.filter( - "" - ) - - self.assertEqual( - [ - action.payload - for action in ( - *first.actions, - *second.actions, - ) - ], - [ - "3s", - "3s", - ], - ) - self.assertFalse( - first.marker_repetition_exceeded - ) - self.assertFalse( - second.marker_repetition_exceeded - ) - def test_duplicate_idle_actions_queue_one_request_and_flash_bubble(self): - Emitter = FakeEmitter - async def run_case(): - queue = asyncio.Queue() - emitter = Emitter() - context = SimpleNamespace( - background_tasks=set(), - runtime_action_events=[], - runtime_search_calls=[], - runtime_loaded_skills=[], - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_save_session_requested=False, - runtime_save_session_action_emitted=False, - runtime_skill_state_barrier_active=False, - runtime_current_turn_id="turn_000001", - logger=None, - emitter=emitter, - ) - actions = ( - RuntimeActionCall( - name=RUNTIME_ACTION_IDLE, - payload="0s", - ), - RuntimeActionCall( - name=RUNTIME_ACTION_IDLE, - payload="0s", - ), - RuntimeActionCall( - name=RUNTIME_ACTION_IDLE, - payload="0s", - ), - ) - - applied_count = await apply_runtime_action_calls( - context, - actions, - user_message="schedule three ticks", - context_snapshot={ - "system_prompt": "frozen prompt", - "user_prompt": "schedule three ticks", - }, - assistant_message=( - "" - ), - ) - queued = [ - await asyncio.wait_for( - queue.get(), - timeout=1, - ) - for _ in range(1) - ] - - self.assertEqual( - applied_count, - 1, - ) - self.assertEqual( - [ - item["idle_followup"]["id"] - for item in queued - ], - [ - "idle_001", - ], - ) - self.assertEqual( - [ - ( - event.get("id"), - event.get("status"), - event.get("text", ""), - event.get("detail", ""), - ) - for event in emitter.events - ], - [ - ("idle_001", "started", "IDLE: 0s", "0s"), - ("idle_001", "completed", "", "0s"), - ], - ) - self.assertEqual( - { - event.get("runtime_turn_id") - for event in emitter.events - }, - {"turn_000001"}, - ) - - asyncio.run(run_case()) - - - def test_zero_second_idle_queues_followup_with_full_source_message(self): - - async def run_case(): - queue = asyncio.Queue() - context = SimpleNamespace( - background_tasks=set(), - runtime_action_events=[], - runtime_search_calls=[], - runtime_loaded_skills=[], - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_save_session_requested=False, - runtime_save_session_action_emitted=False, - runtime_skill_state_barrier_active=False, - runtime_current_turn_id="turn_000001", - logger=None, - ) - source_message = ( - "I will check this again. " - " " - "The rest of the same message." - ) - result = extract_runtime_actions( - source_message, - enabled_actions=( - RUNTIME_ACTION_IDLE, - ), - ) - - applied_count = await apply_runtime_action_calls( - context, - result.actions, - user_message="original request", - context_snapshot={ - "system_prompt": "frozen prompt", - "user_prompt": "original request", - }, - assistant_message=source_message, - ) - queued = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - - self.assertEqual(applied_count, 1) - self.assertEqual(queued["type"], "idle_followup") - self.assertEqual( - queued["idle_followup"]["source_message"], - source_message, - ) - self.assertEqual( - queued["idle_followup"]["seconds"], - 0, - ) - asyncio.run(run_case()) def test_extract_search_query_unnests_json_string(self): diff --git a/tests/runtime_actions/test_todo.py b/tests/runtime_actions/test_todo.py index c0e80d60..786927bf 100644 --- a/tests/runtime_actions/test_todo.py +++ b/tests/runtime_actions/test_todo.py @@ -11,7 +11,6 @@ from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) diff --git a/tests/runtime_actions/test_update_l4_facts.py b/tests/runtime_actions/test_update_l4_facts.py index 14f0ab78..42f55179 100644 --- a/tests/runtime_actions/test_update_l4_facts.py +++ b/tests/runtime_actions/test_update_l4_facts.py @@ -101,6 +101,22 @@ def test_marker_rejects_destructive_plain_text_note(self): self.assertEqual(result.text, "before\nafter") self.assertEqual(result.actions, ()) + def test_marker_allows_removing_content_from_existing_fact(self): + result = extract_runtime_actions( + ( + "\n" + "Update F305: Remove white bonfire with cutout from the " + "description. Keep coffee, bong, and bricks.\n" + "" + ), + enabled_actions=(RUNTIME_ACTION_UPDATE_L4_FACTS,), + ) + + self.assertEqual(len(result.actions), 1) + payload = json.loads(result.actions[0].payload) + self.assertEqual(payload["fact_ids"], ["F305"]) + self.assertIn("Remove white bonfire", payload["message"]) + def test_invalid_note_marker_is_removed_without_action(self): result = extract_runtime_actions( ( @@ -232,6 +248,68 @@ async def test_runtime_action_updates_l4_in_background(self): any(event.get("status") == "failed" for event in lifecycle) ) + async def test_runtime_action_does_not_wait_for_cancelled_idle_l4_task(self): + emitter = FakeEmitter() + logger = FakeLogger() + context = RuntimeContext( + websocket=None, + emitter=emitter, + logger=logger, + clients={}, + ) + release_idle = asyncio.Event() + note_started = asyncio.Event() + + async def stubborn_idle_task(): + try: + await release_idle.wait() + except asyncio.CancelledError: + # Simulate a provider request that takes time to unwind after + # local cancellation. Foreground L4 must not wait for it. + await release_idle.wait() + + async def fake_run_l4_jin_note(*, context, note): + del context, note + note_started.set() + return { + "phase": "jin_note", + "status": "completed", + "changed": False, + "change": {}, + } + + idle_task = asyncio.create_task(stubborn_idle_task()) + context.runtime_l4_memory_update_task = idle_task + context.runtime_l4_memory_update_kind = "idle" + action = RuntimeActionCall( + name=RUNTIME_ACTION_UPDATE_L4_FACTS, + payload=json.dumps({ + "fact_ids": ["F1"], + "message": "Update F1: keep the clarified wording.", + }), + ) + + try: + from unittest.mock import patch + + with patch( + "utils.actions.update_l4_facts_actions.run_l4_jin_note", + new=fake_run_l4_jin_note, + ): + applied = await apply_runtime_action_calls( + context, + (action,), + action_display_ids={id(action): "update_l4_facts_001"}, + ) + + self.assertEqual(applied, 1) + await asyncio.wait_for(note_started.wait(), timeout=0.2) + tasks = list(getattr(context, "background_tasks", set())) + await asyncio.wait_for(asyncio.gather(*tasks), timeout=0.2) + finally: + release_idle.set() + await asyncio.gather(idle_task, return_exceptions=True) + async def test_runtime_action_can_create_l4_without_selected_facts(self): emitter = FakeEmitter() logger = FakeLogger() diff --git a/tests/test_active_memory_failure_followup.py b/tests/test_active_memory_failure_followup.py index 1ccd803c..00d0bb1f 100644 --- a/tests/test_active_memory_failure_followup.py +++ b/tests/test_active_memory_failure_followup.py @@ -4,7 +4,6 @@ from agent.nodes.brain import ( BrainNode, - FOLLOWUP_SYSTEM_MESSAGE, action_event_requires_follow_up, ) from runtime.runtime_context import RuntimeContext @@ -138,7 +137,7 @@ async def test_invalid_update_gets_failed_event_and_retry_context(self): latest_action="UPDATE_ACTIVE_MEMORY", ) - self.assertTrue(prompt.startswith(FOLLOWUP_SYSTEM_MESSAGE)) + self.assertNotIn("", prompt) mandatory = prompt.index("") failed = prompt.index("") tools = prompt.index("") diff --git a/tests/test_answer_rating_client_contract.py b/tests/test_answer_rating_client_contract.py index 37d387f6..333ee807 100644 --- a/tests/test_answer_rating_client_contract.py +++ b/tests/test_answer_rating_client_contract.py @@ -108,9 +108,9 @@ def test_disabled_rating_exposes_selectable_text_mode(self): def test_cache_versions_are_bumped_for_rating_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/chat-rating.css?v=rating-persist-1", source) + self.assertIn("/static/css/chat-rating.css?v=rating-persist-1-rating-disable-2&bubble-utility-retry=1", source) self.assertIn("/static/css/theme-win95.css?v=l4-fact-report-link-1", source) - self.assertIn("/static/js/answer-rating.js?v=answer-rating-persist-1", source) + self.assertIn("/static/js/answer-rating.js?v=answer-rating-persist-1-rating-disable-2&hover-labels=2&bubble-utility-retry=1", source) self.assertIn( "/static/js/runtime/runtime-feedback.js?v=runtime-feedback-persist-1", source, diff --git a/tests/test_behavior_contract.py b/tests/test_behavior_contract.py index 9662d361..47489026 100644 --- a/tests/test_behavior_contract.py +++ b/tests/test_behavior_contract.py @@ -94,6 +94,52 @@ def write_contract(trigger: str) -> None: ("second trigger",), ) + def test_runtime_action_enablement_comes_from_contract_metadata(self): + + with tempfile.TemporaryDirectory() as directory: + contracts_dir = Path(directory) + (contracts_dir / "custom_action.json").write_text( + json.dumps({ + "custom_action": { + "runtime_action": "CUSTOM_ACTION", + "enable_flag": "CAN_CUSTOM_ACTION", + "runtime_order": 10, + }, + }), + encoding="utf-8", + ) + + with patch.object( + rules_assembler, + "CONTRACTS_DIR", + contracts_dir, + ): + self.assertEqual( + rules_assembler.get_enabled_runtime_actions({ + "CAN_CUSTOM_ACTION": True, + }), + ("CUSTOM_ACTION",), + ) + self.assertEqual( + rules_assembler.get_enabled_runtime_actions({ + "CAN_CUSTOM_ACTION": False, + }), + (), + ) + + def test_all_contracts_define_runtime_enable_metadata(self): + + for name, contract in get_behavior_contract()["action_guards"].items(): + self.assertTrue( + str(contract.get("enable_flag", "") or "").strip(), + msg=f"{name}.enable_flag must be set", + ) + self.assertIsInstance( + contract.get("runtime_order"), + int, + msg=f"{name}.runtime_order must be an int", + ) + def test_all_contracts_have_trigger_words_and_blockers_as_lists(self): for name, contract in get_behavior_contract()["action_guards"].items(): @@ -313,7 +359,7 @@ def test_runtime_action_instruction_blocks_are_separated(self): instructions = build_runtime_action_instructions(( "CLEAN_TOOL_RESULTS", - "IDLE", + "JIN_COLOR", )) self.assertIn( @@ -321,7 +367,7 @@ def test_runtime_action_instruction_blocks_are_separated(self): "Emit at any moment in you answer to clean redundant " "tool results and only if they are present in the context " "inside block.\n\n" - "" + "" ), instructions, ) diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 61f3c0a7..af08b30b 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -7,13 +7,10 @@ from agent.nodes.brain import ( BrainNode, - FOLLOWUP_SYSTEM_MESSAGE, POTENTIAL_LOOP_FOLLOWUP_MESSAGE, action_batch_requires_follow_up, action_event_requires_follow_up, - build_idle_followup_system_prompt, build_context_limit_recovery_context, - build_followup_system_message, build_reasoning_recovery_context, format_followup_action_from_event, format_followup_actions_from_events, @@ -85,17 +82,12 @@ def _assert_latest_request_payload( payload, "", ) - expected_followup_message = build_followup_system_message( - latest_action_fragment or "", - ) test_case.assertTrue( - system_prompt.startswith( - expected_followup_message - ), - system_prompt, + call_kwargs.get("followup_tick"), + call_kwargs, ) - test_case.assertIn( - expected_followup_message, + test_case.assertNotIn( + "", system_prompt, ) test_case.assertLess( @@ -265,31 +257,22 @@ async def test_followup_always_contains_tool_results_block(self): prompt, ) - async def test_followup_places_runtime_instruction_under_header(self): + async def test_followup_places_generic_instruction_without_followup_header(self): - instruction = ( - "Action-specific follow-up instruction." - ) + instruction = "Generic follow-up instruction." prompt = BrainNode.build_followup_system_prompt( "system rules", "continue the task", instruction=instruction, - latest_action="web_search", ) - self.assertLess( - prompt.index( - "This is follow-up tick for JIN latest action: web_search." - ), - prompt.index(instruction), + self.assertNotIn( + "", + prompt, ) self.assertLess( prompt.index(instruction), - prompt.index(""), - ) - self.assertLess( - prompt.index(""), - prompt.index(""), + prompt.index(""), ) async def test_followup_places_confirm_result_inside_tool_results(self): @@ -432,10 +415,8 @@ async def test_followup_collects_scattered_tool_results_below_current_sequence(s "continue", ) - self.assertTrue( - prompt.startswith( - build_followup_system_message() - ), + self.assertNotIn( + "", prompt, ) self.assertLess( @@ -815,20 +796,14 @@ async def test_loaded_delayed_memory_is_under_latest_request(self): context ) ) - self.assertTrue( - prompt.startswith( - build_followup_system_message() - ), + self.assertNotIn( + "", prompt, ) self.assertLess( prompt.index(""), prompt.index(""), ) - self.assertIn( - build_followup_system_message(), - prompt, - ) self.assertIn( "INITIAL_SEQUENCE_INSTRUCTION: append the delayed memory", prompt, @@ -1009,59 +984,6 @@ async def test_followup_places_current_sequence_under_latest_request(self): prompt, ) - async def test_idle_followup_keeps_original_sequence_action_history(self): - - context = SimpleNamespace( - runtime_current_turn_id="idle_000002", - runtime_current_sequence_turn_id="turn_000001", - runtime_turn_started_at=1031.0, - runtime_current_sequence_started_at=1000.0, - runtime_action_sequence_turn_ids=[], - runtime_session_action_history=[ - { - "text": "IDLE - 30s", - "created_at": 1000.0, - "runtime_turn_id": "turn_000001", - }, - { - "text": "IDLE - 20s, WEB_SEARCH", - "created_at": 1031.0, - "runtime_turn_id": "turn_000001", - }, - ], - runtime_recent_turns=[], - runtime_loaded_delayed_memory={}, - ) - - with patch( - "utils.context.context_exports.time.time", - return_value=1032.0, - ): - prompt = BrainNode.build_followup_system_prompt( - "state", - "first idle 30s, then idle 20s and search", - context=context, - latest_action="web_search, idle", - ) - - self.assertIn( - "turn_000001", - context.runtime_action_sequence_turn_ids, - ) - self.assertIn( - "\n" - "INITIAL_SEQUENCE_INSTRUCTION: first idle 30s, then idle 20s and search ( 32s ago )\n" - "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" - " --- Sequence started ---\n" - " JIN message 1 executed: IDLE: 30s ( 32s ago )\n" - " JIN message 2 executed: IDLE: 20s, WEB_SEARCH ( 1s ago )\n" - "", - prompt, - ) - self.assertIn( - "first idle 30s, then idle 20s and search ( 32s ago )", - prompt, - ) async def test_list_skills_followup_text_is_emitted_when_no_asset_action_follows(self): @@ -1365,11 +1287,10 @@ async def run(self, generator): context=context, brain_runtime=_brain_runtime(), brain_client=object(), - system_prompt=build_followup_system_message( - "list_skills" - ), + system_prompt="system prompt", brain_payload="", runtime_actions={}, + followup_tick=True, ) self.assertIn( @@ -1688,13 +1609,11 @@ async def fake_run_brain_stream(**kwargs): "", ) self.assertTrue( - kwargs["system_prompt"].startswith( - FOLLOWUP_SYSTEM_MESSAGE - ), - kwargs["system_prompt"], + kwargs.get("followup_tick"), + kwargs, ) - self.assertIn( - FOLLOWUP_SYSTEM_MESSAGE, + self.assertNotIn( + "", kwargs["system_prompt"], ) return ( @@ -1769,13 +1688,11 @@ async def fake_run_brain_stream(**kwargs): "", ) self.assertTrue( - kwargs["system_prompt"].startswith( - FOLLOWUP_SYSTEM_MESSAGE - ), - kwargs["system_prompt"], + kwargs.get("followup_tick"), + kwargs, ) - self.assertIn( - FOLLOWUP_SYSTEM_MESSAGE, + self.assertNotIn( + "", kwargs["system_prompt"], ) return ( @@ -1896,9 +1813,11 @@ async def fake_run_brain_stream(**kwargs): "", ) self.assertTrue( - system_prompt.startswith( - FOLLOWUP_SYSTEM_MESSAGE - ), + kwargs.get("followup_tick"), + kwargs, + ) + self.assertNotIn( + "", system_prompt, ) self.assertIn( @@ -1932,9 +1851,11 @@ async def fake_run_brain_stream(**kwargs): "", ) self.assertTrue( - system_prompt.startswith( - FOLLOWUP_SYSTEM_MESSAGE - ), + kwargs.get("followup_tick"), + kwargs, + ) + self.assertNotIn( + "", system_prompt, ) self.assertEqual( @@ -2816,213 +2737,8 @@ async def fake_emit_brain_text(**kwargs): ) - async def test_idle_action_never_triggers_immediate_follow_up(self): - - self.assertFalse( - action_batch_requires_follow_up( - [ - { - "name": "idle", - "seconds": 0, - "deferred_follow_up": True, - }, - ], - "", - ) - ) - - self.assertFalse( - action_batch_requires_follow_up( - [ - { - "name": "idle", - "seconds": 1, - "deferred_follow_up": True, - }, - { - "name": "idle", - "seconds": 2, - "deferred_follow_up": True, - }, - ], - "", - ) - ) - - async def test_idle_runtime_turn_restores_sequence_origin_and_history(self): - - calls = [] - - async def fake_run_brain_stream(**kwargs): - calls.append(kwargs) - return "Sequence complete.", "" - - origin_request = ( - "first idle 30s, then idle 20s and search" - ) - context = _context() - context.runtime_current_turn_id = "idle_000002" - context.runtime_current_sequence_turn_id = "turn_000001" - context.runtime_turn_started_at = 1030.0 - context.runtime_current_sequence_started_at = 1000.0 - context.runtime_turn_user_message = origin_request - context.runtime_action_sequence_turn_ids = [] - context.runtime_session_action_history = [ - { - "text": "IDLE - 30s", - "created_at": 1000.0, - "runtime_turn_id": "turn_000001", - }, - ] - context.runtime_recent_turns = [] - context.runtime_loaded_delayed_memory = {} - - state = AgentState( - user_input=origin_request, - ) - state.user_input = origin_request - state.metadata["idle_followup"] = { - "id": "idle_001", - "seconds": 30, - "origin_user_request": origin_request, - "context_snapshot": { - "system_prompt": ( - "stale\n" - "stale\n" - "stale\n" - "frozen state" - ), - }, - } - - with patch( - "agent.nodes.brain.get_brain_runtime_config", - return_value=_brain_runtime(), - ), patch( - "agent.nodes.brain.emit_active_memory_records_update_if_dirty", - new=lambda _context: _async_noop(), - ), patch.object( - BrainNode, - "run_brain_stream", - staticmethod(fake_run_brain_stream), - ), patch( - "utils.context.context_exports.time.time", - return_value=1030.0, - ): - await BrainNode().run( - state, - context, - ) - - self.assertEqual( - len(calls), - 1, - ) - prompt = calls[0]["system_prompt"] - idle_instruction = ( - "This is a follow-up tick from an IDLE timer JIN chose to set." - ) - self.assertEqual( - prompt.count(idle_instruction), - 1, - prompt, - ) - self.assertLess( - prompt.index( - "This is follow-up tick for JIN latest action: idle." - ), - prompt.index(idle_instruction), - ) - self.assertLess( - prompt.index(idle_instruction), - prompt.index(""), - ) - self.assertLess( - prompt.index(""), - prompt.index(""), - prompt.index(""), - ) - self.assertEqual( - prompt.count(""), - 0, - prompt, - ) - self.assertEqual( - prompt.count(""), - 1, - prompt, - ) - self.assertEqual( - prompt.count(""), - 0, - prompt, - ) - self.assertIn( - origin_request + " ( 30s ago )", - prompt, - ) - self.assertIn( - "JIN message 1 executed: IDLE: 30s ( 30s ago )", - prompt, - ) - self.assertNotIn( - ">stale<", - prompt, - ) - - async def test_idle_followup_prompt_contains_tool_result_and_frozen_context(self): - with patch( - "agent.nodes.brain.time.time", - return_value=1150.0, - ): - prompt = build_idle_followup_system_prompt({ - "id": "idle_1", - "seconds": 5, - "origin_user_request": "original request", - "source_message": "reason ", - "sequence_started_at": 1000.0, - "context_snapshot": { - "system_prompt": ( - "frozen state" - ), - }, - }) - self.assertIn( - '', - prompt, - ) - self.assertNotIn( - "original request", - prompt, - ) - self.assertNotIn( - "reason <IDLE: 5s />", - prompt, - ) - self.assertTrue( - prompt.startswith( - "" - ), - prompt, - ) - self.assertEqual( - prompt.count( - "" - ), - 1, - ) - self.assertIn( - ( - "" - "frozen state" - ), - prompt, - ) async def test_no_follow_up_action_without_visible_answer_does_not_trigger_tick(self): diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 0c24bede..11ee0166 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -1037,12 +1037,6 @@ def test_session_history_keeps_full_jin_visual_values_in_context(self): "payloads": ["x:120px y:80px"], "raw_payloads": ["x:120px y:80px"], }, - { - "name": "IDLE", - "marker_count": 1, - "payloads": ["30s"], - "raw_payloads": ["30s"], - }, ] with patch( @@ -1069,8 +1063,7 @@ def test_session_history_keeps_full_jin_visual_values_in_context(self): ( "1. JIN_SIZE: w:300px h:300px, " "JIN_COLOR: #ff00ff, " - "JIN_POSITION: x:120px y:80px, " - "IDLE: 30s ( 1h ago )" + "JIN_POSITION: x:120px y:80px ( 1h ago )" ), history, ) diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 658a714e..ed16f760 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -84,10 +84,6 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: "CLEAN_TOOL_RESULTS" ) - if bool(runtime_actions.get("CAN_IDLE", False)): - expected_actions.append( - "IDLE" - ) if bool(runtime_actions.get("CAN_JIN_COLOR", False)): expected_actions.append( @@ -363,6 +359,83 @@ async def collect(context): ) ) + def test_stream_rejected_update_l4_facts_marker_retires_started_bubble(self): + + class FakeEmitter: + + def __init__(self): + self.events = [] + + async def emit(self, payload): + self.events.append(dict(payload)) + + class FakeBrainClient: + + async def stream(self, **_kwargs): + yield { + "type": "content", + "content": "Reply.\n", + } + yield { + "type": "content", + "content": "\nDelete F1 from long-term memory.\n", + } + yield { + "type": "content", + "content": "", + } + + async def fake_apply_runtime_action_calls( + _context, + actions, + **_kwargs, + ): + applied.extend(actions) + return len(tuple(actions)) + + async def collect(context): + return [ + chunk + async for chunk in ask_brain_stream( + client=FakeBrainClient(), + text="delete memory", + context=context, + runtime_actions={ + "CAN_UPDATE_L4_FACTS": True, + }, + ) + ] + + emitter = FakeEmitter() + context = SimpleNamespace(emitter=emitter) + applied = [] + original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN + config.USE_SERVICE_AS_BRAIN = False + + try: + with patch( + "clients.brain_client.apply_runtime_action_calls", + new=fake_apply_runtime_action_calls, + ): + asyncio.run(collect(context)) + finally: + config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain + + lifecycle = [ + event + for event in emitter.events + if event.get("action") == "update_l4_facts" + and event.get("id") + and event.get("status") in {"started", "failed"} + ] + + self.assertEqual([event["status"] for event in lifecycle], [ + "started", + "failed", + ]) + self.assertEqual(lifecycle[0]["id"], lifecycle[1]["id"]) + self.assertEqual(applied, []) + def test_image_attachments_do_not_enter_model_payload_by_default(self): context = SimpleNamespace( @@ -1657,7 +1730,6 @@ def test_session_history_groups_current_marker_parts_for_turn(self): [ RuntimeActionCall(name="WEB_SEARCH", payload="latest news"), RuntimeActionCall(name="SAVE_ACTIVE_MEMORY", payload="remember coffee"), - RuntimeActionCall(name="IDLE", payload="5s"), RuntimeActionCall(name="LOAD_SKILL", payload="wildcards"), RuntimeActionCall(name="LOAD_SKILL", payload="file_manager"), ], @@ -1668,7 +1740,6 @@ def test_session_history_groups_current_marker_parts_for_turn(self): [[ {"text": "WEB_SEARCH", "detail": "latest news"}, {"text": "SAVE_ACTIVE_MEMORY", "detail": "remember coffee"}, - {"text": "IDLE", "detail": "5s"}, {"text": "LOAD_SKILL: wildcards"}, {"text": "LOAD_SKILL: file_manager"}, ]], @@ -4298,78 +4369,6 @@ def test_prompt_does_not_render_legacy_memory_recall_block(self): ) - def test_idle_history_keeps_payloads_and_separate_turn_entries(self): - - context = SimpleNamespace( - runtime_session_action_history=[], - runtime_current_turn_id="turn-1", - ) - - replace_session_action_history_since( - context, - 0, - [ - RuntimeActionCall( - name="IDLE", - payload="5s", - ), - RuntimeActionCall( - name="IDLE", - payload="12s", - ), - ], - ) - - self.assertEqual( - len(context.runtime_session_action_history), - 1, - ) - self.assertEqual( - context.runtime_session_action_history[0]["parts"], - [ - { - "text": "IDLE", - "detail": "5s, 12s", - "count": 2, - }, - ], - ) - - context.runtime_current_turn_id = "turn-2" - replace_session_action_history_since( - context, - len(context.runtime_session_action_history), - [ - RuntimeActionCall( - name="IDLE", - payload="7s", - ), - ], - ) - - self.assertEqual( - len(context.runtime_session_action_history), - 2, - ) - self.assertEqual( - context.runtime_session_action_history[1]["parts"], - [ - { - "text": "IDLE", - "detail": "7s", - }, - ], - ) - self.assertEqual( - [ - item.get("runtime_turn_id") - for item in context.runtime_session_action_history - ], - [ - "turn-1", - "turn-2", - ], - ) if __name__ == "__main__": diff --git a/tests/test_chat_log.py b/tests/test_chat_log.py index c2894900..d8075a48 100644 --- a/tests/test_chat_log.py +++ b/tests/test_chat_log.py @@ -14,6 +14,7 @@ get_chat_log_path, migrate_legacy_chat_logs, resume_chat_log_session, + replace_latest_chat_log_entry, save_chat_bootstrap_context_snapshot, save_chat_context_snapshot, save_turn_reasoning, @@ -217,6 +218,62 @@ def test_append_chat_log_entry_uses_date_session_directory(self): ], ) + def test_replace_latest_jin_entry_keeps_single_visible_pair(self): + + context = SimpleNamespace( + session_id="retry-session", + runtime_turn_counter=1, + runtime_current_turn_id="turn_000001", + runtime_turn_attachments=[], + active_memory_records=[], + runtime_loaded_delayed_memory_ids=[], + runtime_turn_reasoning_log_path="", + ) + now = datetime( + 2026, + 8, + 24, + 17, + 10, + 0, + tzinfo=timezone.utc, + ) + + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + path = append_chat_log_entry( + context, + role="user", + text="same request", + now=now, + root=root, + ) + append_chat_log_entry( + context, + role="jin", + text="old answer", + now=now, + root=root, + ) + + context.runtime_current_turn_id = "user_retry_000002" + replace_latest_chat_log_entry( + context, + role="jin", + text="new answer", + now=now, + root=root, + ) + + entries = [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + ] + + self.assertEqual([entry["role"] for entry in entries], ["user", "jin"]) + self.assertEqual(entries[-1]["text"], "new answer") + self.assertEqual(entries[-1]["turn_id"], "turn_000001") + def test_append_chat_log_entry_repairs_missing_trailing_newline(self): context = SimpleNamespace( session_id="missing-newline-session", diff --git a/tests/test_chat_reasoning_spacing_client_contract.py b/tests/test_chat_reasoning_spacing_client_contract.py index 358143da..2191f713 100644 --- a/tests/test_chat_reasoning_spacing_client_contract.py +++ b/tests/test_chat_reasoning_spacing_client_contract.py @@ -100,6 +100,20 @@ def test_chat_scrollbar_only_appears_while_scrolling(self): self.assertIn('"scroll"', source) self.assertIn("TRANSIENT_SCROLLBAR_HIDE_MS", source) + + def test_collapsed_reasoning_is_upright_roomier_and_softly_faded(self): + css = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn("line-height: 1.5;", css) + self.assertIn("font-style: normal;", css) + self.assertIn("max-height: calc(1lh + 32px);", css) + self.assertIn( + "body:not(.theme-win95) .jin-think-content.is-collapsed::after", + css, + ) + self.assertIn("height: 12px;", css) + self.assertIn("rgba(12, 12, 14, 0.22)", css) + def test_cache_versions_are_bumped_for_reasoning_spacing_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") diff --git a/tests/test_chat_reference_ids_client_contract.py b/tests/test_chat_reference_ids_client_contract.py index a81da289..0c229a1b 100644 --- a/tests/test_chat_reference_ids_client_contract.py +++ b/tests/test_chat_reference_ids_client_contract.py @@ -11,7 +11,7 @@ class ChatReferenceIdsClientContractTests(unittest.TestCase): - def test_answer_ids_resolve_only_against_live_file_and_delayed_stores(self): + def test_answer_ids_resolve_against_live_file_delayed_and_l4_stores(self): source = REFERENCE_JS.read_text(encoding="utf-8") self.assertIn("window.JinFiles.getFiles()", source) @@ -19,6 +19,9 @@ def test_answer_ids_resolve_only_against_live_file_and_delayed_stores(self): self.assertIn("runtime.getDelayedMemoryReports()", source) self.assertIn('kind: "file"', source) self.assertIn('kind: "delayed"', source) + self.assertIn("window.JinRuntime.l4Memory", source) + self.assertIn('kind: "l4"', source) + self.assertIn("normalizeLongTermFactId", source) self.assertIn('references.has(id)', source) self.assertIn('new RegExp(`\\\\b(?:${ids.map(escapeRegex).join("|")})\\\\b`', source) @@ -29,13 +32,15 @@ def test_image_ids_use_existing_hover_preview_and_attachment_modal(self): self.assertIn("{ hoverPreviewMaxPx: 100 }", source) self.assertIn("window.openJinAttachmentModal(reference.record)", source) - def test_text_ids_show_clean_filename_and_delayed_ids_open_report_modal(self): + def test_text_and_l4_ids_show_descriptive_hover_titles(self): source = REFERENCE_JS.read_text(encoding="utf-8") self.assertIn("cleanPersistentFileName", source) self.assertIn('element.title = cleanPersistentFileName(reference.record) || reference.id;', source) self.assertIn("memoryView.openDelayedMemoryReportModal(reference.record)", source) self.assertIn("reference.record && reference.record.title", source) + self.assertIn("buildLongTermFactTitle(reference.record)", source) + self.assertIn("parseLongTermFactTimestamp(record.created_at)", source) def test_stream_render_decorates_ids_without_replacing_chat_parser(self): source = CHAT_JS.read_text(encoding="utf-8") @@ -52,7 +57,7 @@ def test_reference_ids_have_dark_and_light_theme_treatment(self): def test_reference_parser_is_loaded_before_chat_renderer(self): source = INDEX_HTML.read_text(encoding="utf-8") - reference_script = '/static/js/chat-reference-ids.js?v=reference-ids-1' + reference_script = '/static/js/chat-reference-ids.js?v=reference-ids-2' chat_script = '/static/js/chat.js?v=' self.assertIn(reference_script, source) diff --git a/tests/test_chat_runtime_marker_ui.py b/tests/test_chat_runtime_marker_ui.py index 143b2ff0..0be1d70e 100644 --- a/tests/test_chat_runtime_marker_ui.py +++ b/tests/test_chat_runtime_marker_ui.py @@ -126,6 +126,266 @@ def test_chat_script_cache_version_is_bumped(self): ) + def test_deep_search_stack_opens_by_first_child_click_and_closes_outside(self): + chat_runtime_source = CHAT_RUNTIME_ACTIONS_JS.read_text( + encoding="utf-8" + ) + chat_runtime_css = ( + ROOT + / "ui" + / "static" + / "css" + / "chat-runtime-action.css" + ).read_text(encoding="utf-8") + + self.assertIn( + "function bindDeepSearchStackClick(", + chat_runtime_source, + ) + self.assertIn( + 'row.dataset.runtimeActionDeepSearchClickBound === "true"', + chat_runtime_source, + ) + self.assertIn( + "if (firstChildRow !== row)", + chat_runtime_source, + ) + self.assertIn( + "deepSearchStackHasSelectedText()", + chat_runtime_source, + ) + self.assertIn( + "function handleDeepSearchStackDocumentClick(", + chat_runtime_source, + ) + self.assertIn( + "groupRow.contains(target)", + chat_runtime_source, + ) + self.assertIn( + 'document.addEventListener(\n "click",\n handleDeepSearchStackDocumentClick', + chat_runtime_source, + ) + self.assertNotIn( + "function isDeepSearchGroupPointerWithinHoverZone(", + chat_runtime_source, + ) + self.assertNotIn( + "scheduleDeepSearchHoverValidation(", + chat_runtime_source, + ) + self.assertNotIn( + '"pointermove",\n handleDeepSearchStackPointerMove', + chat_runtime_source, + ) + self.assertIn( + 'const DEEP_SEARCH_STACK_COLLAPSED_REVEAL_PX = 0;', + chat_runtime_source, + ) + self.assertIn( + 'jin-runtime-action-deep-search-child-obscured', + chat_runtime_css, + ) + self.assertIn( + 'opacity: 0.1;', + chat_runtime_css, + ) + self.assertIn( + '-webkit-user-select: text;', + chat_runtime_css, + ) + self.assertIn( + 'user-select: text;', + chat_runtime_css, + ) + self.assertIn( + '[data-runtime-action-deep-search-index="1"]:not(.jin-runtime-action-deep-search-stack-expanded)', + chat_runtime_css, + ) + self.assertIn( + 'transition-duration: 220ms;', + chat_runtime_css, + ) + + @unittest.skipUnless( + shutil.which("node"), + "node is required for the deep-search click interaction test", + ) + def test_deep_search_stack_click_interaction_routes_only_expected_clicks(self): + script = r''' +const fs = require("fs"); +const source = fs.readFileSync(process.argv[1], "utf8"); +const start = source.indexOf("function deepSearchStackHasSelectedText()"); +const end = source.indexOf("function scheduleDeepSearchStackGeometrySync(", start); + +if (start < 0 || end < 0) { + throw new Error("deep-search click helpers were not found"); +} + +let selectedText = ""; +const deepSearchStackExpandedGroups = new Set(); +const rowsByGroup = new Map(); +const stateChanges = []; + +global.window = { + getSelection() { + return { + isCollapsed: !selectedText, + toString() { + return selectedText; + }, + }; + }, +}; + +function makeRow(kind, name) { + const listeners = {}; + const label = { + name: `${name}-label`, + addEventListener(type, callback) { + listeners[type] = callback; + }, + }; + + const row = { + name, + dataset: {}, + classList: { + contains(value) { + return value === `jin-runtime-action-deep-search-${kind}`; + }, + }, + querySelector() { + return label; + }, + contains(target) { + return target === row || target === label; + }, + listeners, + label, + }; + + return row; +} + +const parent = makeRow("parent", "parent"); +const first = makeRow("child", "first"); +const second = makeRow("child", "second"); +[parent, first, second].forEach((row) => { + row.dataset.runtimeActionDeepSearchGroup = "group-1"; +}); +rowsByGroup.set("group-1", [parent, first, second]); + +function readDeepSearchGroupId(row) { + return row.dataset.runtimeActionDeepSearchGroup || ""; +} + +function findDeepSearchGroupRows(groupId) { + return rowsByGroup.get(groupId) || []; +} + +function setDeepSearchStackExpanded(row, expanded) { + const groupId = readDeepSearchGroupId(row); + stateChanges.push([row.name, expanded]); + if (expanded) { + deepSearchStackExpandedGroups.add(groupId); + } else { + deepSearchStackExpandedGroups.delete(groupId); + } +} + +eval(source.slice(start, end)); + +bindDeepSearchStackClick(first); +bindDeepSearchStackClick(second); + +second.listeners.click(); +if (stateChanges.length !== 0) { + throw new Error("non-first child opened the stack"); +} + +selectedText = "copy me"; +first.listeners.click(); +if (stateChanges.length !== 0) { + throw new Error("text selection triggered stack opening"); +} + +selectedText = ""; +first.listeners.click(); +if (stateChanges.length !== 1 || stateChanges[0][1] !== true) { + throw new Error("first child click did not open the stack"); +} + +first.listeners.click(); +if (stateChanges.length !== 1) { + throw new Error("clicking a bubble while expanded changed stack state"); +} + +handleDeepSearchStackDocumentClick({ target: second.label }); +if (stateChanges.length !== 1) { + throw new Error("clicking inside a child bubble collapsed the stack"); +} + +handleDeepSearchStackDocumentClick({ target: { name: "outside" } }); +if (stateChanges.length !== 2 || stateChanges[1][1] !== false) { + throw new Error("outside click did not collapse the stack"); +} +''' + completed = subprocess.run( + [ + shutil.which("node"), + "-e", + script, + str(CHAT_RUNTIME_ACTIONS_JS), + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + completed.returncode, + 0, + completed.stderr or completed.stdout, + ) + + def test_deep_search_parent_waits_for_close_tag_before_showing_objective(self): + socket_runtime_source = SOCKET_RUNTIME_ACTIONS_JS.read_text( + encoding="utf-8" + ) + + self.assertIn( + "function shouldUseDeepSearchStartedDisplayNameOnly(", + socket_runtime_source, + ) + self.assertIn( + 'action === "deep_web_search"', + socket_runtime_source, + ) + self.assertIn( + 'data.deep_search_parent === true', + socket_runtime_source, + ) + self.assertIn( + 'data.deep_search_payload_ready === true', + socket_runtime_source, + ) + self.assertIn( + "&& !deepSearchPayloadReady", + socket_runtime_source, + ) + self.assertIn( + """shouldUseDeepSearchStartedDisplayNameOnly( + action, + status, + deepSearchParent, + deepSearchPayloadReady + ) + ? displayName""", + socket_runtime_source, + ) + + def test_save_active_memory_does_not_add_second_token_hot_path_parser(self): runtime_stream_source = RUNTIME_STREAM_PY.read_text( encoding="utf-8" @@ -249,11 +509,11 @@ def test_active_memory_bubbles_use_title_and_full_record_hover(self): socket_runtime_source, ) self.assertIn( - 'node.title = detail;', + 'node.title = hoverDetail;', chat_runtime_source, ) self.assertIn( - 'row.title = detail;', + 'row.title = hoverDetail;', chat_runtime_source, ) self.assertIn( @@ -323,9 +583,45 @@ def test_deep_search_runtime_actions_use_stacked_child_bubbles(self): runtime_action_css, ) self.assertIn( - "margin-top: -1.35rem", + "const DEEP_SEARCH_STACK_MOTION_MS = 220;", + chat_runtime_source, + ) + self.assertNotIn( + "DEEP_SEARCH_STACK_FLOW_MOTION_MS", + chat_runtime_source, + ) + self.assertIn( + "const DEEP_SEARCH_STACK_COLLAPSED_REVEAL_PX = 0;", + chat_runtime_source, + ) + self.assertIn( + "function startDeepSearchStackFlip(", + chat_runtime_source, + ) + self.assertIn( + "captureDeepSearchStackMotionEntries(", + chat_runtime_source, + ) + self.assertIn( + "--jin-deep-search-stack-motion-y", + runtime_action_css, + ) + self.assertIn( + "transition-duration: 220ms !important;", runtime_action_css, ) + self.assertIn( + "function primeDeepSearchInsertedChild(", + chat_runtime_source, + ) + self.assertIn( + "settleDeepSearchStackGeometry(", + chat_runtime_source, + ) + self.assertNotIn( + "DEEP_SEARCH_STACK_COLLAPSE_DELAY_MS", + chat_runtime_source, + ) self.assertIn( "function insertRuntimeActionRow(", chat_runtime_source, @@ -339,15 +635,27 @@ def test_deep_search_runtime_actions_use_stacked_child_bubbles(self): chat_runtime_source, ) self.assertIn( - "--jin-deep-search-stack-motion: 150ms", + "function bindDeepSearchStackClick(", + chat_runtime_source, + ) + self.assertIn( + "clickTarget.addEventListener(", + chat_runtime_source, + ) + self.assertIn( + "function handleDeepSearchStackDocumentClick(", + chat_runtime_source, + ) + self.assertIn( + "--jin-deep-search-stack-translate-y", runtime_action_css, ) self.assertIn( - "margin-top var(--jin-deep-search-stack-motion) cubic-bezier(0.16, 1, 0.3, 1)", + "overflow-anchor: none", runtime_action_css, ) self.assertIn( - "transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19)", + "0 10px 24px rgba(0, 0, 0, 0.36)", runtime_action_css, ) self.assertIn( diff --git a/tests/test_deep_web_search_flow.py b/tests/test_deep_web_search_flow.py index 7bb0c917..ca6819ad 100644 --- a/tests/test_deep_web_search_flow.py +++ b/tests/test_deep_web_search_flow.py @@ -136,6 +136,56 @@ def test_worker_response_parser_accepts_fenced_json(self): self.assertEqual(parsed["report"], "ok") self.assertFalse(parsed["done"]) + def test_worker_response_parser_recovers_adjacent_json_objects(self): + parsed = parse_deep_search_worker_response( + '{"queries":["dark trip-hop cinematic score industrial ambient",' + '"Nordic Noir electronic soundtrack minimalist techno",' + '"industrial ambient noir music artists"]}\n' + '{"spawn":[],"report":"Search those three directions.",' + '"done":false}' + ) + + self.assertEqual( + parsed["queries"], + [ + "dark trip-hop cinematic score industrial ambient", + "Nordic Noir electronic soundtrack minimalist techno", + "industrial ambient noir music artists", + ], + ) + self.assertEqual(parsed["spawn"], []) + self.assertEqual(parsed["report"], "Search those three directions.") + self.assertFalse(parsed["done"]) + self.assertFalse(parsed["invalid_json"]) + + async def test_adjacent_json_worker_response_still_executes_searches(self): + service = FakeServiceClient([ + ( + '{"queries":["q1","q2","q3"]}\n' + '{"spawn":[],"report":"continue research","done":false}' + ), + worker_response([], report="enough evidence", done=True), + worker_response([], report="final report", done=True), + ]) + provider = FakeSearchProvider() + context = make_context(service, provider) + + result = await run_deep_web_search( + context=context, + objective="research split json handling", + ) + + self.assertEqual(provider.queries, ["q1", "q2", "q3"]) + self.assertIn("final report", result) + + def test_worker_response_parser_does_not_truthify_string_false(self): + parsed = parse_deep_search_worker_response( + '{"queries":["q1"],"spawn":[],"report":"ok","done":"false"}' + ) + + self.assertEqual(parsed["queries"], ["q1"]) + self.assertFalse(parsed["done"]) + def test_current_sequence_contains_shared_budget_and_previous_steps(self): pool = DeepSearchPool( objective="find similar music", diff --git a/tests/test_deep_web_search_marker_lifecycle.py b/tests/test_deep_web_search_marker_lifecycle.py new file mode 100644 index 00000000..fbd691b5 --- /dev/null +++ b/tests/test_deep_web_search_marker_lifecycle.py @@ -0,0 +1,80 @@ +import asyncio +from types import SimpleNamespace +import unittest + +from contracts.rules_assembler import RUNTIME_ACTION_DEEP_WEB_SEARCH +from runtime.stream import RuntimeStream +from utils.actions import RuntimeActionCall + + +class FakeEmitter: + def __init__(self): + self.events = [] + + async def emit(self, payload): + self.events.append(dict(payload)) + + +class DeepWebSearchMarkerLifecycleTests(unittest.TestCase): + + def build_runtime_stream(self): + context = SimpleNamespace( + emitter=FakeEmitter(), + runtime_action_events=[], + runtime_active_action_markers=[], + runtime_current_turn_id="turn_000001", + runtime_deep_web_search_action_sequence=0, + delayed_memory_reports={}, + ) + runtime_stream = RuntimeStream.__new__(RuntimeStream) + runtime_stream.context = context + runtime_stream.context_snapshot = {} + runtime_stream.stream = SimpleNamespace(message_id="message_000001") + runtime_stream.deep_web_search_action_ids = {} + runtime_stream.started_deep_web_search_action_ids = [] + runtime_stream.started_active_memory_action_ids = [] + runtime_stream.started_delayed_memory_action_ids = [] + runtime_stream.started_update_l4_facts_action_ids = [] + runtime_stream.jin_color_action_id = "" + runtime_stream.jin_size_action_ids = {} + runtime_stream.update_l4_facts_action_ids = {} + runtime_stream.action_guard_confirmation_ids = {} + return runtime_stream, context + + def test_opening_marker_emits_empty_parent_bubble_immediately(self): + runtime_stream, context = self.build_runtime_stream() + + asyncio.run(runtime_stream.emit_started_runtime_actions(( + RuntimeActionCall(name=RUNTIME_ACTION_DEEP_WEB_SEARCH), + ))) + + self.assertEqual(len(context.emitter.events), 1) + event = context.emitter.events[0] + self.assertEqual(event["action"], "deep_web_search") + self.assertEqual(event["status"], "started") + self.assertEqual(event["text"], "DEEP_WEB_SEARCH") + self.assertTrue(event["deep_search_parent"]) + self.assertFalse(event["deep_search_payload_ready"]) + self.assertTrue(event["id"]) + + def test_closing_marker_reuses_opening_parent_bubble_id(self): + runtime_stream, _context = self.build_runtime_stream() + opening = RuntimeActionCall(name=RUNTIME_ACTION_DEEP_WEB_SEARCH) + completed = RuntimeActionCall( + name=RUNTIME_ACTION_DEEP_WEB_SEARCH, + payload='{"query": "Noir jazz history and instrumentation"}', + ) + + opening_id = runtime_stream.get_runtime_action_display_id(opening) + completed_id = runtime_stream.get_runtime_action_display_id(completed) + + self.assertEqual(completed_id, opening_id) + self.assertEqual(runtime_stream.started_deep_web_search_action_ids, []) + self.assertEqual( + runtime_stream.deep_web_search_action_ids[completed.payload], + opening_id, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_l1_reconnect_resume.py b/tests/test_l1_reconnect_resume.py new file mode 100644 index 00000000..33120ea0 --- /dev/null +++ b/tests/test_l1_reconnect_resume.py @@ -0,0 +1,297 @@ +import asyncio +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +import runtime.L1_memory as l1_memory +import runtime.L1_memory_pending as l1_pending +from tests.helpers.memory import ( + FakeLogger, + FakeServiceClient, +) + + +class L1ReconnectResumeTests( + unittest.IsolatedAsyncioTestCase +): + + @staticmethod + def build_context( + *, + service_client, + session_id="reconnect-session", + runtime_memory_updates=0, + runtime_persistent_writes_restricted=False, + ): + emitter = SimpleNamespace( + events=[], + emit=None, + ) + + async def emit(event): + emitter.events.append( + event + ) + + emitter.emit = emit + + return SimpleNamespace( + session_id=session_id, + runtime_persistent_writes_restricted=( + runtime_persistent_writes_restricted + ), + clients={ + "service": service_client, + }, + logger=FakeLogger(), + emitter=emitter, + runtime_memory="Initial memory.", + runtime_memory_stable="Initial memory.", + runtime_memory_updates=runtime_memory_updates, + runtime_memory_pending_turns=[], + runtime_memory_pending_base_updates=0, + runtime_memory_update_task=None, + background_tasks=set(), + runtime_memory_snapshots=[], + runtime_memory_snapshot_index=0, + ) + + async def test_interrupted_l1_request_replays_after_backend_restart(self): + with tempfile.TemporaryDirectory() as directory: + pending_dir = Path(directory) + + with patch.object( + l1_pending, + "PENDING_L1_DIR", + pending_dir, + ): + first_context = self.build_context( + service_client=FakeServiceClient( + "First attempt should not matter." + ) + ) + + first_task = l1_memory.schedule_runtime_memory_update( + context=first_context, + user_message="Remember the interrupted turn.", + assistant_message="I will keep it in L1.", + ) + + self.assertIsNotNone( + first_task + ) + self.assertEqual( + len(list(pending_dir.glob("*.l1_pending.json"))), + 1, + ) + + # Simulate the backend process disappearing while the L1 job is + # still owned by that process. The durable checkpoint must stay. + first_task.cancel() + with self.assertRaises( + asyncio.CancelledError + ): + await first_task + + restarted_service = FakeServiceClient( + "Recovered runtime memory." + ) + restarted_context = self.build_context( + service_client=restarted_service, + ) + + restored = l1_pending.restore_pending_l1_update( + restarted_context + ) + + self.assertTrue( + restored + ) + self.assertEqual( + restarted_context.runtime_memory_pending_turns, + [ + { + "user_message": "Remember the interrupted turn.", + "assistant_message": "I will keep it in L1.", + }, + ], + ) + + resumed_task = l1_memory.resume_runtime_memory_pending_update( + restarted_context + ) + + self.assertIsNotNone( + resumed_task + ) + + await resumed_task + + self.assertEqual( + len(restarted_service.calls), + 1, + ) + self.assertIn( + "Remember the interrupted turn.", + restarted_service.calls[0]["user_prompt"], + ) + self.assertEqual( + restarted_context.runtime_memory_updates, + 1, + ) + self.assertTrue( + any( + event.get("type") == "runtime_memory_update" + for event in restarted_context.emitter.events + ) + ) + + async def test_restricted_mode_replays_pending_l1_after_backend_restart(self): + with tempfile.TemporaryDirectory() as directory: + pending_dir = Path(directory) + + with patch.object( + l1_pending, + "PENDING_L1_DIR", + pending_dir, + ): + first_context = self.build_context( + service_client=FakeServiceClient( + "First attempt should not matter." + ), + runtime_persistent_writes_restricted=True, + ) + + first_task = l1_memory.schedule_runtime_memory_update( + context=first_context, + user_message="Keep this L1 turn across reconnect.", + assistant_message="This request must be replayed.", + ) + + self.assertIsNotNone( + first_task + ) + self.assertEqual( + len(list(pending_dir.glob("*.l1_pending.json"))), + 1, + ) + + first_task.cancel() + with self.assertRaises( + asyncio.CancelledError + ): + await first_task + + restarted_service = FakeServiceClient( + "Recovered restricted-mode runtime memory." + ) + restarted_context = self.build_context( + service_client=restarted_service, + runtime_persistent_writes_restricted=True, + ) + + self.assertTrue( + l1_pending.restore_pending_l1_update( + restarted_context + ) + ) + + resumed_task = l1_memory.resume_runtime_memory_pending_update( + restarted_context + ) + + self.assertIsNotNone( + resumed_task + ) + + await resumed_task + + self.assertEqual( + len(restarted_service.calls), + 1, + ) + self.assertTrue( + any( + message == "[MEMORY:L1] L1 summarizer request" + for message, _details + in restarted_context.logger.summarizer_logs + ) + ) + self.assertIn( + "Keep this L1 turn across reconnect.", + restarted_service.calls[0]["user_prompt"], + ) + self.assertEqual( + restarted_context.runtime_memory_updates, + 1, + ) + self.assertTrue( + any( + event.get("type") == "runtime_memory_update" + for event in restarted_context.emitter.events + ) + ) + + async def test_newer_browser_snapshot_discards_stale_pending_checkpoint(self): + with tempfile.TemporaryDirectory() as directory: + pending_dir = Path(directory) + + with patch.object( + l1_pending, + "PENDING_L1_DIR", + pending_dir, + ): + source_context = self.build_context( + service_client=FakeServiceClient( + "Unused." + ), + runtime_memory_updates=4, + ) + source_context.runtime_memory_pending_turns = [ + { + "user_message": "Already committed.", + "assistant_message": "Already visible in browser L1.", + }, + ] + source_context.runtime_memory_pending_base_updates = 4 + + self.assertTrue( + l1_pending.persist_pending_l1_update( + source_context + ) + ) + + resumed_context = self.build_context( + service_client=FakeServiceClient( + "Must not run." + ), + runtime_memory_updates=5, + ) + + self.assertTrue( + l1_pending.restore_pending_l1_update( + resumed_context + ) + ) + + resumed_task = l1_memory.resume_runtime_memory_pending_update( + resumed_context + ) + + self.assertIsNone( + resumed_task + ) + self.assertEqual( + resumed_context.runtime_memory_pending_turns, + [], + ) + self.assertEqual( + list(pending_dir.glob("*.l1_pending.json")), + [], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 0bdfbf78..ab3593f1 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -3,13 +3,18 @@ import tempfile import unittest from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import patch from runtime.L4_memory import ( + apply_facts_memory_store_sync, apply_l4_memory_store_sync, + bind_l4_runtime_app_state, build_runtime_l4_memory_context, cancel_l4_memory_idle_update, delete_l4_memory_fact, ensure_runtime_l4_state, + get_l4_scheduler_interval_seconds, maybe_update_runtime_l4_memory, remap_delayed_memory_l4_fact_ids, restore_l4_memory_fact, @@ -17,6 +22,7 @@ runtime_l4_memory_update_running, schedule_l4_memory_idle_update, ) +import runtime.L4_memory as l4_memory_module from runtime.L4_memory_utils import ( add_l4_pending_candidates, apply_l4_jin_note_result, @@ -1797,7 +1803,7 @@ async def test_failed_batch_isolated_then_poison_pending_is_deferred_without_blo ["PF1"], ) - async def test_idle_scheduler_enforces_server_side_minute_cadence(self): + async def test_idle_scheduler_skips_empty_ticks_and_enforces_configured_cadence(self): context = RuntimeContext( websocket=None, emitter=FakeEmitter(), @@ -1805,6 +1811,23 @@ async def test_idle_scheduler_enforces_server_side_minute_cadence(self): clients={}, ) + context.runtime_l4_idle_last_started_at = 123.0 + empty = schedule_l4_memory_idle_update( + context=context, + user_idle_seconds=61, + ) + self.assertIsNone(empty) + self.assertEqual(context.runtime_l4_idle_last_started_at, 123.0) + + context.runtime_long_term_memory_store = normalize_l4_store({ + "pending_facts": [{ + "id": "PF1", + "key": "project.pending", + "value": "Still pending.", + "category": "project_fact", + }], + }) + first = schedule_l4_memory_idle_update( context=context, user_idle_seconds=61, @@ -1814,7 +1837,7 @@ async def test_idle_scheduler_enforces_server_side_minute_cadence(self): second = schedule_l4_memory_idle_update( context=context, - user_idle_seconds=62, + user_idle_seconds=61, ) self.assertIsNone(second) @@ -1827,6 +1850,89 @@ async def test_idle_scheduler_enforces_server_side_minute_cadence(self): ) self.assertIsNone(third) + def test_server_scheduler_uses_exact_closed_tab_third_cadence(self): + with patch.object( + l4_memory_module.config, + "L4_IDLE_SECONDS", + 15, + ): + self.assertEqual( + get_l4_scheduler_interval_seconds( + tabs_open=True, + ), + 15.0, + ) + self.assertEqual( + get_l4_scheduler_interval_seconds( + tabs_open=False, + ), + 5.0, + ) + + def test_server_facts_memory_reconciles_analyzed_state_after_closed_tab_work(self): + app_state = SimpleNamespace( + l4_memory_scheduler_wake_event=asyncio.Event(), + l4_facts_memory_records=[], + l4_runtime_context=None, + ) + analyzed_context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={}, + ) + bind_l4_runtime_app_state( + analyzed_context, + app_state, + ) + analyzed_sync = apply_facts_memory_store_sync( + analyzed_context, + [{ + "session_id": "session-a", + "signals": { + "gpu": { + "content": "RTX 4090", + "runtime_snapshot_id": "runtime-a", + "l4_status": "analyzed", + "l4_analyzed_at": "2026-08-24T09:00:00Z", + }, + }, + }], + ) + self.assertEqual(analyzed_sync["pending_count"], 0) + + reopened_context = RuntimeContext( + websocket=None, + emitter=FakeEmitter(), + logger=FakeLogger(), + clients={}, + ) + bind_l4_runtime_app_state( + reopened_context, + app_state, + ) + stale_browser_sync = apply_facts_memory_store_sync( + reopened_context, + [{ + "session_id": "session-a", + "signals": { + "gpu": { + "content": "RTX 4090", + "runtime_snapshot_id": "runtime-a", + "l4_status": "pending", + }, + }, + }], + ) + + self.assertEqual(stale_browser_sync["pending_count"], 0) + field = reopened_context.runtime_facts_memory_records[0]["signals"]["gpu"] + self.assertEqual(field["l4_status"], "analyzed") + self.assertEqual( + field["l4_analyzed_at"], + "2026-08-24T09:00:00Z", + ) + async def test_user_activity_preempts_idle_l4_task_without_consuming_pending(self): context = RuntimeContext( websocket=None, @@ -2939,7 +3045,7 @@ def test_brain_context_always_injects_complete_long_term_memory(self): self.assertIn("user.hardware.main_gpu:", prompt) self.assertIn("user.preference.response_style:", prompt) - async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once(self): + async def test_idle_job_chains_extraction_into_merge_without_waiting_for_next_tick(self): gpu_value = "User's main GPU is RTX 4090." language_value = "User prefers Russian replies." gpu_pending_id = build_l4_fact_id(sequence=1, pending=True) @@ -2953,6 +3059,17 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( "source_keys": ["gpu"] }}] }}''', + f'''{{ + "operations": [ + {{ + "action": "create", + "pending_id": "{gpu_pending_id}", + "key": "user.hardware.main_gpu", + "value": "{gpu_value}", + "category": "environment" + }} + ] + }}''', f'''{{ "facts": [{{ "key": "user.preference.response_language", @@ -2963,13 +3080,6 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( }}''', f'''{{ "operations": [ - {{ - "action": "create", - "pending_id": "{gpu_pending_id}", - "key": "user.hardware.main_gpu", - "value": "{gpu_value}", - "category": "environment" - }}, {{ "action": "create", "pending_id": "{language_pending_id}", @@ -3002,9 +3112,12 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( context=context, user_idle_seconds=61, ) - self.assertEqual(first["phase"], "extract") - self.assertEqual(len(context.runtime_long_term_memory_store["pending_facts"]), 1) - self.assertEqual(context.runtime_long_term_memory_store["facts"], []) + self.assertEqual(first["phase"], "merge") + self.assertEqual(first["status"], "completed") + self.assertEqual(first["extraction_result"]["phase"], "extract") + self.assertEqual(context.runtime_long_term_memory_store["pending_facts"], []) + self.assertEqual(len(context.runtime_long_term_memory_store["facts"]), 1) + self.assertEqual(len(service_client.calls), 2) context.runtime_facts_memory_records = normalize_facts_memory_records([ { @@ -3027,20 +3140,20 @@ async def test_idle_ticks_extract_until_facts_memory_is_drained_then_merge_once( context=context, user_idle_seconds=61, ) - self.assertEqual(second["phase"], "extract") - self.assertEqual(len(context.runtime_long_term_memory_store["pending_facts"]), 2) - self.assertEqual(context.runtime_long_term_memory_store["facts"], []) - - third = await maybe_update_runtime_l4_memory( - context=context, - user_idle_seconds=61, - ) - self.assertEqual(third["phase"], "merge") + self.assertEqual(second["phase"], "merge") + self.assertEqual(second["status"], "completed") + self.assertEqual(second["extraction_result"]["phase"], "extract") self.assertEqual(context.runtime_long_term_memory_store["pending_facts"], []) self.assertEqual(len(context.runtime_long_term_memory_store["facts"]), 2) - self.assertEqual(len(service_client.calls), 3) - self.assertIn("cross-session long-term memory", service_client.calls[0]["system_prompt"]) - self.assertIn("consolidate pending candidates", service_client.calls[2]["system_prompt"].lower()) + self.assertEqual(len(service_client.calls), 4) + self.assertIn( + "cross-session long-term memory", + service_client.calls[0]["system_prompt"], + ) + self.assertIn( + "consolidate pending candidates", + service_client.calls[1]["system_prompt"].lower(), + ) def test_restore_l4_fact_preserves_deleted_fact_object(self): fact = normalize_l4_store({ diff --git a/tests/test_l4_memory_client_contract.py b/tests/test_l4_memory_client_contract.py index 811bf331..4130742b 100644 --- a/tests/test_l4_memory_client_contract.py +++ b/tests/test_l4_memory_client_contract.py @@ -22,7 +22,7 @@ def test_long_term_memory_keeps_shared_local_storage_key(self): source, ) - def test_idle_l4_waits_a_full_minute_and_resets_while_user_idle_is_paused(self): + def test_idle_l4_scheduler_is_not_driven_by_browser_javascript(self): source = ( ROOT / "ui" @@ -31,12 +31,30 @@ def test_idle_l4_waits_a_full_minute_and_resets_while_user_idle_is_paused(self): / "runtime" / "runtime-l4-memory.js" ).read_text(encoding="utf-8") + template = ( + ROOT + / "ui" + / "templates" + / "index.html" + ).read_text(encoding="utf-8") + runtime_source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime.js" + ).read_text(encoding="utf-8") - self.assertIn("const idleTickIntervalMs = 60000;", source) - self.assertIn("const idleMinimumSeconds = 60;", source) - self.assertIn("if (idleContext.user_idle_paused)", source) - self.assertIn("lastIdleTickAt = now;", source) - self.assertIn("if (userIdleSeconds < idleMinimumSeconds)", source) + self.assertNotIn("l4_memory_idle_tick", source) + self.assertNotIn("startIdleMonitor", source) + self.assertNotIn("maybeSendIdleTick", source) + self.assertNotIn('id="jin-l4-config"', template) + self.assertIn( + 'typeof window.syncFactsMemoryToRuntime === "function"', + runtime_source, + ) + self.assertIn("window.syncFactsMemoryToRuntime();", runtime_source) def test_equal_revision_update_cannot_replace_larger_local_snapshot(self): source = ( diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index 1c74883a..9e1ea863 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -136,12 +136,14 @@ def test_win95_l4_fact_report_links_are_blue_and_bold(self): def test_cache_versions_are_bumped_for_citation_sync_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=l4-report-link-1", source) - self.assertIn("/static/css/theme-win95.css?v=l4-fact-report-link-1", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=files-panel-1", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=jin-glow-1", source) - self.assertIn("/static/js/think-citations.js?v=think-citations-6", source) - self.assertIn("/static/js/chat.js?v=jin-size-1", source) + self.assertIn("/static/css/runtime-memory.css?v=", source) + self.assertIn("/static/css/theme-win95.css?v=", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1", source) + self.assertIn("lazy-rows=5", source) + self.assertIn("l4-priority-bubble=1", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=", source) + self.assertIn("/static/js/think-citations.js?v=think-citations-8", source) + self.assertIn("/static/js/chat.js?v=stream-avatar-2-reference-ids-1", source) if __name__ == "__main__": diff --git a/tests/test_runtime_memory_lazy_rows_client_contract.py b/tests/test_runtime_memory_lazy_rows_client_contract.py index 7f6d8c13..1c741737 100644 --- a/tests/test_runtime_memory_lazy_rows_client_contract.py +++ b/tests/test_runtime_memory_lazy_rows_client_contract.py @@ -38,7 +38,10 @@ def test_memory_panels_use_independent_lazy_batch_sizes(self): "getRuntimeMemoryLazyBatchSize(runtimeMemoryLazyMode)", source, ) - self.assertIn("start + batchSize", source) + self.assertIn("start + nextBatchSize", source) + self.assertIn("nextBatchSize = batchSize", source) + self.assertIn("options.initialBatchSize", source) + self.assertIn("const initialBatchSize =", source) self.assertNotIn( "const RUNTIME_MEMORY_LAZY_BATCH_SIZE =", source, @@ -88,7 +91,8 @@ def test_native_scroll_path_is_not_debounced_after_downward_progress(self): def test_memory_view_cache_key_is_bumped_for_l4_lazy_optimization(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("lazy-rows=4", source) + self.assertIn("lazy-rows=5", source) + self.assertIn("l4-priority-bubble=1", source) if __name__ == "__main__": diff --git a/tests/test_session_actions_deep_search_hover_client_contract.py b/tests/test_session_actions_deep_search_hover_client_contract.py new file mode 100644 index 00000000..28ba9cfe --- /dev/null +++ b/tests/test_session_actions_deep_search_hover_client_contract.py @@ -0,0 +1,76 @@ +from pathlib import Path +from types import SimpleNamespace +import unittest + +from runtime.deep_web_search import _record_sequence_line + + +ROOT = Path(__file__).resolve().parents[1] +SESSION_ACTIONS_JS = ( + ROOT / "ui" / "static" / "js" / "logger" / "session-actions.js" +) +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class DeepSearchSessionActionHoverTests(unittest.IsolatedAsyncioTestCase): + + async def test_completion_history_keeps_full_deep_search_text_for_hover(self): + context = SimpleNamespace( + runtime_session_action_history=[], + emitter=None, + ) + + await _record_sequence_line( + context, + "DEEP_WEB_SEARCH complete: 8/10 searches", + hover_text=( + "DEEP_WEB_SEARCH: Deep dive into Noir Jazz genres and " + "essential albums." + ), + ) + + self.assertEqual( + context.runtime_session_action_history[-1]["parts"][0][ + "context_detail" + ], + ( + "DEEP_WEB_SEARCH: Deep dive into Noir Jazz genres and " + "essential albums." + ), + ) + + def test_client_uses_deep_search_context_detail_for_hover_title(self): + source = SESSION_ACTIONS_JS.read_text(encoding="utf-8") + + self.assertIn( + 'String(part.context_detail || "").trim()', + source, + ) + self.assertIn( + 'normalizedActionName === "DEEP_WEB_SEARCH"', + source, + ) + self.assertIn( + '? part.contextDetail', + source, + ) + self.assertIn( + 'action.title =\n hoverText;', + source, + ) + self.assertIn( + 'context_detail: part.contextDetail', + source, + ) + + def test_session_actions_cache_version_is_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn( + '/static/js/logger/session-actions.js?v=logger-session-actions-13', + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_session_bootstrap_chat_tail.py b/tests/test_session_bootstrap_chat_tail.py new file mode 100644 index 00000000..0b8a4154 --- /dev/null +++ b/tests/test_session_bootstrap_chat_tail.py @@ -0,0 +1,161 @@ +import unittest +from pathlib import Path +from types import SimpleNamespace + +from utils.session_restore import _build_recent_turns +from websocket.bootstrap import ( + apply_archived_session_continuation_state, + build_session_bootstrap_chat_tail, +) +from websocket.messages import append_runtime_recent_turn + + +ROOT = Path(__file__).resolve().parents[1] +SOCKET_HANDLERS_JS = ( + ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" +) + + +class SessionBootstrapChatTailTests(unittest.TestCase): + + def test_archive_tail_keeps_three_newest_complete_pairs_with_reasoning(self): + entries = [] + reasoning = {} + for turn in range(1, 5): + turn_id = f"turn_{turn:06d}" + entries.append({ + "turn": turn, + "turn_id": turn_id, + "role": "user", + "text": f"user {turn}", + }) + if turn != 3: + entries.append({ + "turn": turn, + "turn_id": turn_id, + "role": "jin", + "text": f"jin {turn}", + }) + reasoning[turn_id] = ( + "captured_at: now\n\n--- REASONING ---\n" + f"reasoning {turn}" + ) + + turns = _build_recent_turns(entries, reasoning) + + self.assertEqual( + [(turn["user"], turn["jin"]) for turn in turns], + [ + ("user 1", "jin 1"), + ("user 2", "jin 2"), + ("user 4", "jin 4"), + ], + ) + self.assertEqual( + [turn["reasoning"] for turn in turns], + ["reasoning 1", "reasoning 2", "reasoning 4"], + ) + + def test_bootstrap_hydration_preserves_turn_reasoning_and_ui_tail(self): + context = SimpleNamespace( + runtime_recent_turns=[], + runtime_previous_reasoning_content="", + runtime_previous_reasoning_loop_contents=[], + runtime_session_restore_delayed_memory_metadata=[], + runtime_session_restore_attached_file_metadata=[], + runtime_session_restore_reasoning_dump="", + runtime_session_restore_l4_fact_ids=[], + runtime_session_restore_pending_attached_file_ids=[], + runtime_archived_session_id="", + runtime_session_restore_priming=False, + ) + + apply_archived_session_continuation_state( + context, + { + "recent_turns": [ + { + "user": ( + "hello\n\nAttached context:\n" + "- /assets/files/a.png: image [ id: a ]" + ), + "jin": "world", + "reasoning": "saved reasoning", + }, + ], + "previous_reasoning": "latest reasoning", + }, + ) + + self.assertEqual( + context.runtime_recent_turns[0]["reasoning"], + "saved reasoning", + ) + self.assertEqual( + build_session_bootstrap_chat_tail(context), + [ + { + "user": "hello", + "jin": "world", + "reasoning": "saved reasoning", + } + ], + ) + + def test_live_recent_turn_persists_reasoning(self): + context = SimpleNamespace( + runtime_recent_turns=[], + runtime_restored_session_dialog="", + ) + + append_runtime_recent_turn( + context, + user_message="u", + assistant_message="j", + reasoning="r", + ) + + self.assertEqual( + context.runtime_recent_turns, + [{"user": "u", "jin": "j", "reasoning": "r"}], + ) + + def test_client_renders_bootstrap_tail_with_existing_chat_primitives(self): + source = SOCKET_HANDLERS_JS.read_text(encoding="utf-8") + + self.assertIn('"session_bootstrap_chat_tail"', source) + self.assertIn("handleSessionBootstrapChatTail", source) + self.assertIn("appendChatMessage(", source) + self.assertIn("appendThinkingChunk(", source) + self.assertIn("appendStreamChunk(", source) + self.assertIn("finishStreamMessage(", source) + self.assertIn("window.jinArchivedSessionRestorePayload", source) + self.assertIn(".slice(-3)", source) + + def test_client_places_current_session_boundary_above_new_response(self): + source = SOCKET_HANDLERS_JS.read_text(encoding="utf-8") + + self.assertIn("appendSessionBootstrapBoundary", source) + self.assertIn('"jin-session-restore-divider"', source) + self.assertIn('"jin-session-restore-divider-label"', source) + self.assertIn("window.activateLiveUserTurnViewport", source) + self.assertIn("`${now.getDate()} `", source) + self.assertIn("+ `${months[now.getMonth()]}, `", source) + self.assertIn("+ weekdays[now.getDay()]", source) + + handler_start = source.index( + "function handleSessionBootstrapChatTail" + ) + handler_end = source.index( + "function handleSessionActionsUpdate", + handler_start, + ) + handler_source = source[handler_start:handler_end] + self.assertNotIn( + "chatHistory.scrollTop =\n chatHistory.scrollHeight", + handler_source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_user_retry_contract.py b/tests/test_user_retry_contract.py new file mode 100644 index 00000000..1cc357d2 --- /dev/null +++ b/tests/test_user_retry_contract.py @@ -0,0 +1,201 @@ +from pathlib import Path +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +from rules.brain_context_builder import _append_user_retry_context +from runtime.L1_memory import discard_latest_runtime_memory_pending_turn +from websocket.messages import ( + build_user_retry_request, + discard_latest_visible_turn_for_user_retry, + format_runtime_memory_user_message, +) + + +ROOT = Path(__file__).resolve().parents[1] +ANSWER_RATING_JS = ROOT / "ui" / "static" / "js" / "answer-rating.js" +CHAT_RATING_CSS = ROOT / "ui" / "static" / "css" / "chat-rating.css" +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +SOCKET_JS = ROOT / "ui" / "static" / "js" / "socket.js" +EVENT_HANDLERS_JS = ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" +WEBSOCKET_INIT = ROOT / "websocket" / "__init__.py" + + +class UserRetryContractTests(unittest.TestCase): + + def test_release_disables_rating_without_deleting_old_rating_code(self): + source = ANSWER_RATING_JS.read_text(encoding="utf-8") + + self.assertIn("const ANSWER_RATING_ENABLED = false;", source) + self.assertIn("function addRatingHoverZones(root)", source) + self.assertIn('"jin-rating-zone jin-rating-zone-minus"', source) + self.assertIn('"jin-rating-zone jin-rating-zone-plus"', source) + self.assertIn("recordJinAnswerRating", source) + self.assertIn("addBubbleUtilityZones(root);", source) + + def test_padding_zones_own_copy_and_long_tap_only(self): + source = ANSWER_RATING_JS.read_text(encoding="utf-8") + css = CHAT_RATING_CSS.read_text(encoding="utf-8") + + self.assertIn('"Double-click: Copy all\\nLong-tap: Delete"', source) + self.assertIn("const BUBBLE_RETRY_HOLD_MS = 1500;", source) + self.assertIn('zone.addEventListener("dblclick"', source) + self.assertIn('zone.addEventListener("pointerdown"', source) + self.assertIn('bubble.style.opacity = "0";', source) + self.assertIn(".jin-bubble-utility-zone-top", css) + self.assertIn(".jin-bubble-utility-zone-left", css) + self.assertIn("cursor: pointer;", css) + self.assertIn("cursor: text;", css) + self.assertIn("filter: brightness(1.03);", css) + + def test_only_explicitly_completed_live_answer_becomes_retryable(self): + chat_source = CHAT_JS.read_text(encoding="utf-8") + handlers_source = EVENT_HANDLERS_JS.read_text(encoding="utf-8") + + self.assertIn("options.retryable === true", chat_source) + self.assertIn("window.markJinCompletedAnswerBubble", chat_source) + self.assertIn("window.jinCurrentResponseRetryable", handlers_source) + self.assertIn("data && data.retryable_response", handlers_source) + self.assertGreaterEqual(handlers_source.count("{ retryable: false }"), 2) + + + def test_retryability_commits_only_after_agent_runtime_end(self): + handlers_source = EVENT_HANDLERS_JS.read_text(encoding="utf-8") + + message_end_start = handlers_source.index("function handleMessageEnd(") + message_end_end = handlers_source.index("function handleMessageError(") + message_end_source = handlers_source[message_end_start:message_end_end] + self.assertIn("retryCandidate: Boolean(", message_end_source) + self.assertNotIn("retryable: Boolean(", message_end_source) + + runtime_end_start = handlers_source.index("function handleAgentRuntimeEnd(data)") + runtime_end_end = handlers_source.index("function handleMessageStart(") + runtime_end_source = handlers_source[runtime_end_start:runtime_end_end] + self.assertIn("data && data.retryable_response === true", runtime_end_source) + self.assertIn("commitJinCompletedAnswerRetryCandidate", runtime_end_source) + self.assertIn("clearJinCompletedAnswerRetryCandidate", runtime_end_source) + + def test_server_promotes_retry_source_only_after_completed_noninterrupted_response(self): + source = (ROOT / "websocket" / "messages.py").read_text(encoding="utf-8") + + process_start = source.index("async def process_message(") + process_source = source[process_start:] + clear_index = process_source.index("context.runtime_last_retryable_request = {}") + interrupted_index = process_source.index('"runtime_turn_interrupted"') + promote_index = process_source.index( + "context.runtime_last_retryable_request = deepcopy(\n retry_source_candidate" + ) + terminal_index = process_source.index('"type": "agent_runtime_end"') + + self.assertLess(clear_index, interrupted_index) + self.assertLess(interrupted_index, promote_index) + self.assertLess(promote_index, terminal_index) + + def test_retry_socket_request_has_no_new_visible_user_message(self): + source = SOCKET_JS.read_text(encoding="utf-8") + + start = source.index("window.requestJinLastResponseRetry") + retry_source = source[start:start + 2500] + self.assertIn('type: "retry_last_response"', retry_source) + self.assertIn("setGenerationState(", retry_source) + self.assertNotIn("appendChatMessage(", retry_source) + + def test_server_rebuilds_same_request_and_refreshes_live_runtime_state(self): + context = SimpleNamespace( + runtime_last_retryable_request={ + "text": "same request", + "attachments": [{"id": "A1"}], + }, + runtime_recent_turns=[{ + "user": "same request", + "jin": "discard me", + }], + ) + + retry = build_user_retry_request( + context, + { + "runtime_avatar": {"size": 44}, + "active_memory_records": [{"id": "M1"}], + "user_idle": "must not replay", + }, + ) + + self.assertEqual(retry["type"], "retry_last_response") + self.assertEqual(retry["text"], "same request") + self.assertEqual(retry["attachments"], [{"id": "A1"}]) + self.assertEqual(retry["runtime_avatar"], {"size": 44}) + self.assertEqual(retry["active_memory_records"], [{"id": "M1"}]) + self.assertNotIn("user_idle", retry) + + def test_retry_discards_previous_prompt_turn_and_reasoning(self): + context = SimpleNamespace( + runtime_recent_turns=[{"user": "u", "jin": "old"}], + runtime_metabolism_recent_turns=[{"user": "u", "jin": "old"}], + runtime_previous_reasoning_content="old reasoning", + runtime_previous_reasoning_loop_contents=["old loop"], + ) + + previous = discard_latest_visible_turn_for_user_retry(context) + + self.assertEqual(previous["jin"], "old") + self.assertEqual(context.runtime_recent_turns, []) + self.assertEqual(context.runtime_metabolism_recent_turns, []) + self.assertEqual(context.runtime_previous_reasoning_content, "") + self.assertEqual(context.runtime_previous_reasoning_loop_contents, []) + + def test_retry_is_explicit_in_brain_and_l1_context(self): + context = SimpleNamespace( + runtime_user_retry_active=True, + runtime_user_retry_count=2, + runtime_repeated_input_count=0, + ) + parts = [] + + _append_user_retry_context(parts, context) + l1_message = format_runtime_memory_user_message(context, "same request") + + self.assertIn('', parts[0]) + self.assertIn("previous JIN answer has been discarded", parts[0]) + self.assertIn("user_retry: true", l1_message) + self.assertIn("previous_jin_answer_discarded: true", l1_message) + + def test_websocket_has_retry_rejection_and_l1_discard_path(self): + source = WEBSOCKET_INIT.read_text(encoding="utf-8") + + self.assertIn('if message_type == "retry_last_response":', source) + self.assertIn("build_user_retry_request(", source) + self.assertIn("discard_latest_runtime_memory_pending_turn(", source) + self.assertIn('"retry_last_response_rejected"', source) + + +class UserRetryAsyncContractTests(unittest.IsolatedAsyncioTestCase): + + async def test_l1_retry_discards_latest_pending_turn_before_replacement(self): + context = SimpleNamespace( + runtime_memory_update_task=None, + runtime_memory_pending_turns=[{ + "user_message": "same request", + "assistant_message": "discarded answer", + }], + runtime_memory_pending_base_updates=4, + runtime_memory_updates=4, + ) + + with ( + patch("runtime.L1_memory.clear_pending_l1_update") as clear_pending, + patch("runtime.L1_memory.persist_pending_l1_update") as persist_pending, + patch("runtime.L1_memory.resume_runtime_memory_pending_update") as resume_pending, + ): + discarded = await discard_latest_runtime_memory_pending_turn(context) + + self.assertTrue(discarded) + self.assertEqual(context.runtime_memory_pending_turns, []) + clear_pending.assert_called_once_with(context) + persist_pending.assert_not_called() + resume_pending.assert_not_called() + + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_websocket_pending_usage.py b/tests/test_websocket_pending_usage.py index 486ac05c..295dfb47 100644 --- a/tests/test_websocket_pending_usage.py +++ b/tests/test_websocket_pending_usage.py @@ -10,13 +10,11 @@ ) from utils.brain_client_utils import ( get_brain_runtime_config, - schedule_idle_followup, ) from utils.tool_results import ( clear_runtime_tool_results, ) from websocket import ( - PendingRequestQueue, apply_runtime_resume, build_runtime_action_guard_retry_request, apply_session_bootstrap, @@ -26,7 +24,6 @@ reject_when_all_models_offline, refresh_pending_brain_usage, wait_for_runtime_memory_update, - merge_runtime_idle_followup_turn, ) from utils.runtime_action_abort import ( mark_runtime_action_started, @@ -392,253 +389,10 @@ async def finished(): "SAVE_DELAYED_MEMORY: ABORTED", ) - def test_idle_followups_replace_same_recent_turn_instead_of_duplicating_it(self): - context = SimpleNamespace( - runtime_recent_turns=[ - { - "user": "run the timed experiment", - "jin": "Timer armed.", - }, - ], - ) - - merge_runtime_idle_followup_turn( - context, - origin_user_request="run the timed experiment", - assistant_message="First follow-up result.", - assistant_created_at=10.0, - idle_followup_id="idle_001", - ) - merge_runtime_idle_followup_turn( - context, - origin_user_request="run the timed experiment", - assistant_message="Final follow-up result.", - assistant_created_at=20.0, - idle_followup_id="idle_002", - ) - - self.assertEqual( - len(context.runtime_recent_turns), - 1, - ) - self.assertEqual( - context.runtime_recent_turns[0]["user"], - "run the timed experiment", - ) - self.assertEqual( - context.runtime_recent_turns[0]["jin"], - "Final follow-up result.", - ) - self.assertEqual( - context.runtime_recent_turns[0]["idle_followup_id"], - "idle_002", - ) - - async def test_clean_tool_results_invalidates_pending_idle_snapshot(self): - - queue = asyncio.Queue() - context = SimpleNamespace( - background_tasks=set(), - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_tool_results_generation=0, - runtime_tool_results=[], - runtime_tool_results_turn_count=1, - runtime_search_result="old search", - runtime_search_result_id="search_1", - runtime_asset_results=[], - runtime_asset_retry_results=[], - runtime_asset_retry_context=[], - runtime_delayed_memory_results=[], - runtime_turn_attachments=[], - ) - - schedule_idle_followup( - context, - seconds=0, - source_message="", - user_message="continue later", - context_snapshot={ - "system_prompt": ( - "\n" - "old\n" - "\n\nRULES" - ), - }, - ) - clear_runtime_tool_results( - context - ) - - queued = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - frozen_prompt = queued[ - "idle_followup" - ]["context_snapshot"]["system_prompt"] - - self.assertEqual( - frozen_prompt, - "RULES", - ) - self.assertEqual( - queued["idle_followup"]["tool_results_generation"], - 1, - ) - - async def test_idle_followup_preserves_root_sequence_identity(self): - - queue = asyncio.Queue() - context = SimpleNamespace( - background_tasks=set(), - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_tool_results_generation=0, - runtime_turn_attachments=[], - runtime_current_turn_id="idle_000002", - runtime_current_sequence_turn_id="turn_000001", - runtime_turn_started_at=1031.0, - runtime_current_sequence_started_at=1000.0, - ) - - schedule_idle_followup( - context, - seconds=0, - source_message="", - user_message="timed sequence", - context_snapshot={ - "system_prompt": "frozen prompt", - }, - ) - - queued = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - followup = queued["idle_followup"] - - self.assertEqual( - followup["sequence_turn_id"], - "turn_000001", - ) - self.assertEqual( - followup["sequence_started_at"], - 1000.0, - ) - - async def test_idle_followup_inherits_sequence_attachments(self): - - queue = asyncio.Queue() - context = SimpleNamespace( - background_tasks=set(), - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_tool_results_generation=0, - runtime_turn_attachments=[], - runtime_current_turn_id="idle_000002", - runtime_current_sequence_turn_id="turn_000001", - runtime_turn_started_at=1031.0, - runtime_current_sequence_started_at=1000.0, - runtime_current_sequence_attachments_turn_id="turn_000001", - runtime_current_sequence_attachments=[ - { - "name": "README.md", - "kind": "text", - "text_content": "body", - }, - ], - ) - - schedule_idle_followup( - context, - seconds=0, - source_message="", - user_message="timed sequence", - context_snapshot={ - "system_prompt": "frozen prompt", - }, - ) - - queued = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - followup = queued["idle_followup"] - - self.assertEqual( - followup["attachments"], - [ - { - "name": "README.md", - "kind": "text", - "text_content": "body", - }, - ], - ) - - async def test_due_idle_followup_runs_before_queued_dialogue_requests(self): - - queue = PendingRequestQueue() - context = SimpleNamespace( - background_tasks=set(), - runtime_pending_requests_queue=queue, - runtime_pending_idle_followups=[], - runtime_idle_action_sequence=0, - runtime_turn_attachments=[], - ) - - await queue.put({ - "type": "message", - "text": "queued user request", - }) - schedule_idle_followup( - context, - seconds=0, - source_message="wait and continue ", - user_message="start idle", - context_snapshot={ - "system_prompt": "frozen context", - }, - ) - for _ in range(3): - await asyncio.sleep(0) - - self.assertEqual( - queue.qsize(), - 2, - ) - - idle_request = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - queued_user_request = await asyncio.wait_for( - queue.get(), - timeout=1, - ) - - self.assertEqual( - idle_request["type"], - "idle_followup", - ) - self.assertEqual( - idle_request["idle_followup"]["origin_user_request"], - "start idle", - ) - self.assertEqual( - queued_user_request["text"], - "queued user request", - ) - queue.task_done() - queue.task_done() async def test_arm_save_session_prearms_without_banner(self): diff --git a/ui/static/css/chat-rating.css b/ui/static/css/chat-rating.css index d2d7e902..daa4fe64 100644 --- a/ui/static/css/chat-rating.css +++ b/ui/static/css/chat-rating.css @@ -397,3 +397,107 @@ .jin-chat-bubble-rateable.jin-rating-committed .jin-chat-pre { pointer-events: auto; } + +/* ========================= + RELEASE BUBBLE UTILITIES + Rating implementation above is intentionally kept dormant. Only the + bubble's empty padding owns gestures; rendered text stays selectable. + ========================= */ + +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-rateable, +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-service, +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-brain { + cursor: default; +} + +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-rateable .jin-chat-pre, +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-service .jin-chat-pre, +body:not(.jin-answer-rating-enabled) .jin-chat-bubble-brain .jin-chat-pre { + pointer-events: auto; + cursor: text; + user-select: text; + -webkit-user-select: text; + transform: none; +} + +.jin-bubble-utility-enabled .jin-bubble-utility-zones { + position: absolute; + inset: 0; + z-index: 7; + border-radius: inherit; + pointer-events: none; +} + +.jin-bubble-utility-enabled .jin-bubble-utility-zone { + position: absolute; + display: block; + background: transparent; + pointer-events: auto; + cursor: pointer; + touch-action: manipulation; +} + +/* Match the normal chat bubble padding (14px 18px) so controls never cover + message text. Win95 uses the same horizontal padding and a 12-14px vertical + edge, therefore these strips remain inside its empty frame too. */ +.jin-bubble-utility-enabled .jin-bubble-utility-zone-top { + top: 0; + right: 0; + left: 0; + height: 14px; +} + +.jin-bubble-utility-enabled .jin-bubble-utility-zone-bottom { + right: 0; + bottom: 0; + left: 0; + height: 14px; +} + +.jin-bubble-utility-enabled .jin-bubble-utility-zone-left { + top: 14px; + bottom: 14px; + left: 0; + width: 18px; +} + +.jin-bubble-utility-enabled .jin-bubble-utility-zone-right { + top: 14px; + right: 0; + bottom: 14px; + width: 18px; +} + +/* Reuse the old neutral rating glow for the whole bubble. No red/green edge, + skew or directional motion survives while rating is disabled. */ +body:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover { + box-shadow: + 0 14px 42px rgba(0, 0, 0, 0.24), + 0 0 30px rgba(226, 232, 240, 0.070), + 0 0 28px rgba(45, 212, 191, 0.040), + inset 0 1px 0 rgba(248, 250, 252, 0.060), + inset 0 0 24px rgba(226, 232, 240, 0.035), + inset 0 0 30px rgba(103, 232, 249, 0.010); + filter: brightness(1.03); +} + +body:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover::before, +body:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover::after { + opacity: 0; + animation: none; +} + +body:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover .jin-chat-pre { + transform: none; + text-shadow: + 0 0 10px rgba(226, 232, 240, 0.070), + 0 0 14px rgba(103, 232, 249, 0.018); +} + +body.theme-win95:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover { + box-shadow: + 1px 1px 0 var(--win95-text), + 0 8px 18px rgba(0, 0, 0, 0.16), + 0 0 0 1px rgba(255, 255, 255, 0.14); + filter: brightness(1.025); +} diff --git a/ui/static/css/chat-runtime-action.css b/ui/static/css/chat-runtime-action.css index 3783a468..8fea3f15 100644 --- a/ui/static/css/chat-runtime-action.css +++ b/ui/static/css/chat-runtime-action.css @@ -6,6 +6,7 @@ position: relative; perspective: 520px; transform-style: preserve-3d; + overflow: visible; } .jin-runtime-action-row.jin-runtime-action-enter { @@ -33,10 +34,6 @@ .jin-runtime-action-row.jin-runtime-action-enter { animation: none; } - - .jin-runtime-action-deep-search-child { - transition: none; - } } .jin-runtime-action-label { @@ -119,39 +116,104 @@ } .jin-runtime-action-deep-search-child { - --jin-deep-search-stack-motion: 150ms; --jin-runtime-action-label-nudge: 0px; padding-left: 104px; z-index: var(--jin-deep-search-stack-z); - transition: - margin-top var(--jin-deep-search-stack-motion) cubic-bezier(0.16, 1, 0.3, 1), - transform var(--jin-deep-search-stack-motion) cubic-bezier(0.16, 1, 0.3, 1), - opacity var(--jin-deep-search-stack-motion) cubic-bezier(0.16, 1, 0.3, 1); - will-change: margin-top, transform, opacity; + opacity: var(--jin-runtime-action-rest-opacity); + transition: opacity 220ms cubic-bezier(0.22, 0.72, 0.22, 1); } -.jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-stack-expanded { - transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); +.jin-runtime-action-deep-search-child.jin-runtime-action-enter { + animation: jinDeepSearchChildFadeIn 120ms ease-out; +} + +@keyframes jinDeepSearchChildFadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: var(--jin-runtime-action-rest-opacity); + } } .jin-runtime-action-deep-search-child .jin-runtime-action-label { + position: relative; margin-left: 0; max-width: min(100%, 58rem); + transform: translate3d( + 0, + var(--jin-deep-search-stack-translate-y, 0px), + 0 + ); + will-change: transform, opacity; + transition-property: transform, color, background-color, border-color, opacity, box-shadow; + transition-duration: 220ms; + transition-timing-function: cubic-bezier(0.22, 0.72, 0.22, 1); + opacity: 1; + background: rgba(8, 14, 18, 0.75); + -webkit-user-select: text; + user-select: text; + cursor: text; + box-shadow: + 0 10px 24px rgba(0, 0, 0, 0.36), + 0 3px 8px rgba(0, 0, 0, 0.28), + 0 0 0 1px rgba(34, 211, 238, 0.07); +} + +.jin-runtime-action-deep-search-child[data-runtime-action-deep-search-index="1"]:not(.jin-runtime-action-deep-search-stack-expanded) .jin-runtime-action-label { + cursor: pointer; +} + +.jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-child-obscured .jin-runtime-action-label { + opacity: 0.1; + box-shadow: none; +} + +.jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-stack-expanded { + opacity: 1; +} + +.jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-stack-expanded .jin-runtime-action-label { + opacity: 1; +} + +#chat-history.jin-deep-search-stack-animating { + overflow-anchor: none; +} + +.jin-deep-search-stack-motion { + translate: 0 var(--jin-deep-search-stack-motion-y, 0px); + will-change: translate; + transition-property: translate !important; + transition-duration: 220ms !important; + transition-timing-function: cubic-bezier(0.22, 0.72, 0.22, 1) !important; +} + +.jin-deep-search-stack-motion-prep { + transition-duration: 0ms !important; +} + +@media (prefers-reduced-motion: reduce) { + .jin-deep-search-stack-motion { + transition-duration: 0ms !important; + } } #chat-history > .jin-runtime-action-deep-search-parent + .jin-runtime-action-deep-search-child { - margin-top: 0.35rem !important; + margin-top: 5px !important; } #chat-history > .jin-runtime-action-deep-search-child + .jin-runtime-action-deep-search-child { - margin-top: -1.35rem !important; + /* JS replaces this with an exact previous-height + 5px reveal. */ + margin-top: -1.5rem !important; } #chat-history > .jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-stack-expanded + .jin-runtime-action-deep-search-child.jin-runtime-action-deep-search-stack-expanded { - margin-top: 0.14rem !important; + margin-top: 4px !important; } .jin-runtime-action-confirm-prefix { diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 948847a4..258eff09 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -503,6 +503,52 @@ white-space: pre; } +.jin-chat-markdown .jin-chat-table-wrap { + margin: 0.7em 0 0.95em; + width: fit-content; + max-width: 100%; + overflow: hidden; +} + +.jin-chat-markdown .jin-chat-table { + width: 100%; + max-width: 100%; + border-collapse: collapse; + table-layout: auto; + font-size: 0.96em; + line-height: 1.5; +} + +.jin-chat-markdown .jin-chat-table th, +.jin-chat-markdown .jin-chat-table td { + min-width: 0; + border: 1px solid rgba(148, 163, 184, 0.18); + padding: 0.45em 0.62em; + vertical-align: top; + text-align: left; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; +} + +.jin-chat-markdown .jin-chat-table th { + background: rgba(148, 163, 184, 0.055); + color: rgba(230, 247, 250, 0.94); + font-weight: 700; +} + +.jin-chat-markdown .jin-chat-table tbody tr:nth-child(even) td { + background: rgba(148, 163, 184, 0.018); +} + +.jin-chat-markdown .jin-chat-table .jin-chat-table-align-center { + text-align: center; +} + +.jin-chat-markdown .jin-chat-table .jin-chat-table-align-right { + text-align: right; +} + .jin-chat-markdown blockquote { margin: 0.65em 0 0.95em; border-left: 2px solid rgba(165, 180, 252, 0.34); @@ -547,10 +593,10 @@ /* Think чуть мельче обычного ответа. */ font-size: var(--theme-dark-think-font-size); - line-height: 1.65; + line-height: 1.5; - /* Курсив отделяет мысль от обычного голоса. */ - font-style: italic; + /* Reasoning остаётся визуально тихим без курсивного наклона. */ + font-style: normal; /* Сохраняет переносы текста think. */ white-space: pre-wrap; @@ -597,13 +643,30 @@ } .jin-think-content.is-collapsed { - max-height: calc(1lh + 28px); + position: relative; + max-height: calc(1lh + 32px); border-color: rgba(148, 163, 184, 0.11); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22), inset 0 -18px 18px rgba(12, 12, 14, 0.42); } +body:not(.theme-win95) .jin-think-content.is-collapsed::after { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 12px; + border-radius: 0 0 9px 9px; + background: linear-gradient( + to bottom, + rgba(12, 12, 14, 0), + rgba(12, 12, 14, 0.22) + ); + pointer-events: none; +} + .think-rule-hit { color: inherit; font-weight: inherit; diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 6b01840e..94748322 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -1042,7 +1042,7 @@ body.theme-win95 .jin-think-content { font-family: Calibri, Arial, sans-serif; font-size: var(--theme-light-think-font-size); font-weight: 500; - line-height: 1.55; + line-height: 1.5; text-shadow: none; backdrop-filter: none; box-shadow: none; diff --git a/ui/static/js/answer-rating.js b/ui/static/js/answer-rating.js index 45227876..34553e28 100644 --- a/ui/static/js/answer-rating.js +++ b/ui/static/js/answer-rating.js @@ -33,6 +33,322 @@ + ".jin-chat-bubble-brain.jin-rating-selected-active:not(.jin-rating-committed)"; let latestRatingBubbleSequence = 0; + // Rating stays in the codebase, but the release interaction is now the + // neutral bubble utility surface. Flip this back to true to restore the + // old three-zone rating behavior without resurrecting deleted code. + const ANSWER_RATING_ENABLED = false; + const BUBBLE_RETRY_HOLD_MS = 1500; + const bubbleUtilitySelector = + ".jin-chat-bubble-service, .jin-chat-bubble-brain"; + const bubbleCopyText = new WeakMap(); + let latestRetryableBubble = null; + let retryableBubbleCandidate = null; + let pendingRetryRemoval = null; + + if (document.body) { + document.body.classList.toggle( + "jin-answer-rating-enabled", + ANSWER_RATING_ENABLED + ); + } + + function getBubbleUtilityText(bubble) { + const storedText = bubbleCopyText.get(bubble); + if (typeof storedText === "string" && storedText.trim()) { + return storedText; + } + + const content = bubble && bubble.querySelector + ? bubble.querySelector(".jin-chat-pre") + : null; + return String( + content && (content.innerText || content.textContent) || "" + ).trim(); + } + + async function copyBubbleUtilityText(bubble) { + const text = getBubbleUtilityText(bubble); + if (!text) { + return false; + } + + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + } catch (error) { + // Fall through to the old textarea copy path. + } + + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + + let copied = false; + try { + copied = document.execCommand("copy"); + } catch (error) { + copied = false; + } + + textarea.remove(); + return copied; + } + + function getBubbleUtilityLabel(bubble) { + return bubble && bubble.classList.contains("jin-bubble-utility-retryable") + ? "Double-click: Copy all\nLong-tap: Delete" + : "Double-click: Copy all"; + } + + function syncBubbleUtilityLabels(bubble) { + if (!bubble) { + return; + } + + const label = getBubbleUtilityLabel(bubble); + bubble + .querySelectorAll(":scope > .jin-bubble-utility-zones > .jin-bubble-utility-zone") + .forEach((zone) => { + zone.title = label; + zone.setAttribute("aria-label", label.replace("\n", "; ")); + }); + } + + function clearRetryableBubble() { + if (!latestRetryableBubble) { + return; + } + + latestRetryableBubble.classList.remove("jin-bubble-utility-retryable"); + delete latestRetryableBubble.dataset.bubbleUtilityRetryable; + syncBubbleUtilityLabels(latestRetryableBubble); + latestRetryableBubble = null; + } + + function restoreBubbleAfterCancelledHold(bubble) { + if (!bubble || !bubble.isConnected) { + return; + } + + bubble.style.transition = "opacity 0.18s ease"; + bubble.style.opacity = "1"; + window.setTimeout(() => { + if (!bubble.isConnected) { + return; + } + bubble.style.removeProperty("transition"); + bubble.style.removeProperty("opacity"); + }, 190); + } + + function restorePendingRetryRemoval() { + const pending = pendingRetryRemoval; + pendingRetryRemoval = null; + if (!pending || !pending.parent || !pending.wrapper) { + return false; + } + + const reference = ( + pending.nextSibling + && pending.nextSibling.parentNode === pending.parent + ) + ? pending.nextSibling + : null; + pending.parent.insertBefore(pending.wrapper, reference); + restoreBubbleAfterCancelledHold(pending.bubble); + + latestRetryableBubble = pending.bubble; + pending.bubble.classList.add("jin-bubble-utility-retryable"); + pending.bubble.dataset.bubbleUtilityRetryable = "true"; + syncBubbleUtilityLabels(pending.bubble); + return true; + } + + window.restoreJinDeletedRetryBubble = restorePendingRetryRemoval; + window.confirmJinLastResponseRetryStarted = function () { + pendingRetryRemoval = null; + }; + + function startBubbleRetryHold(event, bubble) { + if ( + event.button !== 0 + || bubble !== latestRetryableBubble + || bubble.dataset.bubbleUtilityRetryable !== "true" + || isRatingInteractionBlocked() + ) { + return; + } + + const zone = event.currentTarget; + const state = { + triggered: false, + timer: null, + }; + zone.__jinBubbleHoldState = state; + + bubble.style.transition = `opacity ${BUBBLE_RETRY_HOLD_MS}ms linear`; + bubble.style.opacity = "0"; + + state.timer = window.setTimeout(() => { + state.triggered = true; + state.timer = null; + + const requestRetry = window.requestJinLastResponseRetry; + const sent = requestRetry ? requestRetry() : false; + if (!sent) { + restoreBubbleAfterCancelledHold(bubble); + return; + } + + clearRetryableBubble(); + + const wrapper = bubble.closest(".jin-stream-wrapper") + || bubble.closest(".jin-message-row") + || bubble; + const parent = wrapper.parentNode; + if (parent) { + pendingRetryRemoval = { + wrapper, + bubble, + parent, + nextSibling: wrapper.nextSibling, + }; + wrapper.remove(); + } + }, BUBBLE_RETRY_HOLD_MS); + } + + function cancelBubbleRetryHold(event, bubble) { + const zone = event.currentTarget; + const state = zone.__jinBubbleHoldState; + if (!state || state.triggered) { + return; + } + + if (state.timer !== null) { + window.clearTimeout(state.timer); + } + zone.__jinBubbleHoldState = null; + restoreBubbleAfterCancelledHold(bubble); + } + + function bindBubbleUtilityInteractions(bubble) { + if (!bubble || bubble.dataset.bubbleUtilityBound === "true") { + return; + } + + bubble.dataset.bubbleUtilityBound = "true"; + bubble.classList.add("jin-bubble-utility-enabled"); + + // Clear dormant rating presentation only; all rating implementation + // remains below this release feature flag. + bubble.classList.remove( + ...ratingSelectionClasses, + "jin-rating-disabled", + "jin-rating-l1-waiting", + "jin-rating-interaction-blocked" + ); + const oldRatingZones = bubble.querySelector(":scope > .jin-rating-hover-zones"); + if (oldRatingZones) { + oldRatingZones.remove(); + } + + const zones = document.createElement("div"); + zones.className = "jin-bubble-utility-zones"; + zones.setAttribute("aria-hidden", "true"); + + ["top", "right", "bottom", "left"].forEach((edge) => { + const zone = document.createElement("div"); + zone.className = `jin-bubble-utility-zone jin-bubble-utility-zone-${edge}`; + + zone.addEventListener("dblclick", (event) => { + event.preventDefault(); + event.stopPropagation(); + void copyBubbleUtilityText(bubble); + }); + + zone.addEventListener("pointerdown", (event) => { + startBubbleRetryHold(event, bubble); + }); + ["pointerup", "pointerleave", "pointercancel"].forEach((eventName) => { + zone.addEventListener(eventName, (event) => { + cancelBubbleRetryHold(event, bubble); + }); + }); + zones.appendChild(zone); + }); + + bubble.appendChild(zones); + syncBubbleUtilityLabels(bubble); + } + + function addBubbleUtilityZones(root) { + const scope = root instanceof Element ? root : document; + const bubbles = Array.from(scope.querySelectorAll(bubbleUtilitySelector)); + if ( + scope !== document + && scope.matches + && scope.matches(bubbleUtilitySelector) + ) { + bubbles.unshift(scope); + } + bubbles.forEach(bindBubbleUtilityInteractions); + } + + function markBubbleRetryable(bubble) { + if (!bubble) { + return; + } + + clearRetryableBubble(); + latestRetryableBubble = bubble; + bubble.classList.add("jin-bubble-utility-retryable"); + bubble.dataset.bubbleUtilityRetryable = "true"; + syncBubbleUtilityLabels(bubble); + } + + window.markJinCompletedAnswerBubble = function (bubble, copyText, options = {}) { + if (!bubble) { + return; + } + + bindBubbleUtilityInteractions(bubble); + bubbleCopyText.set(bubble, String(copyText || "")); + + if (options.retryCandidate === true) { + retryableBubbleCandidate = bubble; + } + + if (options.retryable === true) { + retryableBubbleCandidate = null; + markBubbleRetryable(bubble); + return; + } + + syncBubbleUtilityLabels(bubble); + }; + + window.commitJinCompletedAnswerRetryCandidate = function () { + const bubble = retryableBubbleCandidate; + retryableBubbleCandidate = null; + if (!bubble || !bubble.isConnected) { + return false; + } + + markBubbleRetryable(bubble); + return true; + }; + + window.clearJinCompletedAnswerRetryCandidate = function () { + retryableBubbleCandidate = null; + }; + function isRatingInteractionBlocked() { return Boolean( ( @@ -582,6 +898,11 @@ } function addRatingHoverZones(root) { + if (!ANSWER_RATING_ENABLED) { + addBubbleUtilityZones(root); + return; + } + const scope = root instanceof Element ? root : document; syncLatestRateableBubbleState(scope); @@ -733,6 +1054,9 @@ }); window.addEventListener("jin:generation-state-changed", () => { + if (!ANSWER_RATING_ENABLED) { + return; + } document .querySelectorAll(ratingBubbleSelector) .forEach(markBubbleRatingL1State); @@ -761,6 +1085,12 @@ const chatForm = document.getElementById("chat-form"); if (chatForm) { chatForm.addEventListener("submit", () => { + clearRetryableBubble(); + retryableBubbleCandidate = null; + if (!ANSWER_RATING_ENABLED) { + return; + } + document .querySelectorAll(ratingBubbleSelector) .forEach((bubble) => { diff --git a/ui/static/js/chat-reference-ids.js b/ui/static/js/chat-reference-ids.js index 2bdea072..124bbaf9 100644 --- a/ui/static/js/chat-reference-ids.js +++ b/ui/static/js/chat-reference-ids.js @@ -86,6 +86,99 @@ }).filter(Boolean); } + function normalizeLongTermFactId(value) { + const match = String(value || "").trim().match(/^F([1-9]\d*)$/i); + return match ? `f${Number(match[1])}` : ""; + } + + function parseLongTermFactTimestamp(value) { + if (value === null || value === undefined || value === "") { + return null; + } + + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric > 0) { + return numeric; + } + + const milliseconds = Date.parse(String(value)); + if (!Number.isFinite(milliseconds) || milliseconds <= 0) { + return null; + } + + return milliseconds / 1000; + } + + function formatLongTermFactAgeLabel(timestamp, now = Date.now() / 1000) { + const createdAt = Number(timestamp); + if (!Number.isFinite(createdAt) || createdAt <= 0) { + return ""; + } + + const seconds = Math.max(1, Math.floor(Number(now) - createdAt)); + if (seconds < 60) return `${seconds}s ago`; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + + const days = Math.floor(hours / 24); + return `${days}d ago`; + } + + function getLongTermFacts() { + const l4Memory = + window.JinRuntime + && window.JinRuntime.l4Memory; + + if (!l4Memory) { + return []; + } + + try { + if (typeof l4Memory.getFacts === "function") { + return l4Memory.getFacts() || []; + } + if (typeof l4Memory.getVisibleFacts === "function") { + return l4Memory.getVisibleFacts() || []; + } + } catch (_error) { + return []; + } + + return []; + } + + function buildLongTermFactTitle(record) { + if (!record || typeof record !== "object" || Array.isArray(record)) { + return ""; + } + + const key = String(record.key || "").trim(); + const value = String(record.value || record.content || "").trim(); + const age = formatLongTermFactAgeLabel( + parseLongTermFactTimestamp(record.created_at) + ); + const parts = []; + + if (key || value) { + parts.push([key, value].filter(Boolean).join(": ")); + } + + if (age) { + const lastIndex = parts.length - 1; + if (lastIndex >= 0) { + parts[lastIndex] = `${parts[lastIndex]} (${age})`; + } else { + parts.push(age); + } + } + + return parts.join(" ").trim(); + } + function buildReferenceCache() { const references = new Map(); @@ -116,6 +209,17 @@ }); }); + getLongTermFacts().forEach((fact) => { + const id = normalizeLongTermFactId(fact && fact.id); + if (!id || references.has(id)) return; + + references.set(id, { + kind: "l4", + id, + record: fact, + }); + }); + const ids = Array.from(references.keys()); const pattern = ids.length ? new RegExp(`\\b(?:${ids.map(escapeRegex).join("|")})\\b`, "gi") @@ -186,6 +290,7 @@ if (reference.kind === "file") { openFileReference(reference); + return; } } @@ -238,6 +343,8 @@ element.title = cleanPersistentFileName(reference.record) || reference.id; } bindImageHoverPreview(element, reference); + } else if (reference.kind === "l4") { + element.title = buildLongTermFactTitle(reference.record) || reference.id.toUpperCase(); } element.addEventListener("click", (event) => { diff --git a/ui/static/js/chat-response-formatter.js b/ui/static/js/chat-response-formatter.js index 7cdc0208..dc81da6b 100644 --- a/ui/static/js/chat-response-formatter.js +++ b/ui/static/js/chat-response-formatter.js @@ -464,6 +464,173 @@ } + function splitMarkdownTableRow(line) { + + let source = + String(line || "").trim(); + + if (!source.includes("|")) { + return null; + } + + if (source.startsWith("|")) { + source = source.slice(1); + } + + if (source.endsWith("|")) { + source = source.slice(0, -1); + } + + const cells = []; + let cell = ""; + let escaped = false; + let inCode = false; + + for (let index = 0; index < source.length; index += 1) { + const char = source[index]; + + if (escaped) { + cell += char; + escaped = false; + continue; + } + + if (char === "\\") { + escaped = true; + cell += char; + continue; + } + + if (char === "`") { + inCode = !inCode; + cell += char; + continue; + } + + if (char === "|" && !inCode) { + cells.push(cell.trim()); + cell = ""; + continue; + } + + cell += char; + } + + cells.push(cell.trim()); + return cells; + + } + + function getTableStart(lines, startIndex) { + + if (startIndex + 1 >= lines.length) { + return null; + } + + const headerCells = + splitMarkdownTableRow(lines[startIndex]); + const delimiterCells = + splitMarkdownTableRow(lines[startIndex + 1]); + + if ( + !headerCells + || !delimiterCells + || headerCells.length < 2 + || headerCells.length !== delimiterCells.length + || delimiterCells.some((cell) => !/^:?-{3,}:?$/.test(cell)) + ) { + return null; + } + + return { + headerCells, + delimiterCells, + }; + + } + + function getTableAlignmentClass(delimiterCell) { + + const source = String(delimiterCell || ""); + + if (source.startsWith(":") && source.endsWith(":")) { + return "jin-chat-table-align-center"; + } + + if (source.endsWith(":")) { + return "jin-chat-table-align-right"; + } + + return ""; + + } + + function renderTableCells(cells, tag, alignmentClasses) { + + return cells.map((cell, index) => { + const alignmentClass = alignmentClasses[index]; + const classAttribute = alignmentClass + ? ` class="${alignmentClass}"` + : ""; + + return ( + `<${tag}${classAttribute}>` + + renderInlineMarkdown(cell) + + `` + ); + }).join(""); + + } + + function renderTable(lines, startIndex) { + + const tableStart = getTableStart(lines, startIndex); + + if (!tableStart) { + return null; + } + + const columnCount = tableStart.headerCells.length; + const alignmentClasses = + tableStart.delimiterCells.map(getTableAlignmentClass); + const rows = []; + let index = startIndex + 2; + + while (index < lines.length) { + if (isBlank(lines[index])) { + break; + } + + const cells = splitMarkdownTableRow(lines[index]); + + if (!cells || cells.length !== columnCount) { + break; + } + + rows.push( + "" + + renderTableCells(cells, "td", alignmentClasses) + + "" + ); + index += 1; + } + + return { + html: ( + '
      ' + + '' + + "" + + renderTableCells(tableStart.headerCells, "th", alignmentClasses) + + "" + + `${rows.join("")}` + + "
      " + + "
      " + ), + nextIndex: index, + }; + + } + function isBlockStart(line) { return ( @@ -626,7 +793,10 @@ while (index < lines.length) { if ( index !== startIndex - && isBlockStart(lines[index]) + && ( + isBlockStart(lines[index]) + || getTableStart(lines, index) + ) ) { break; } @@ -681,6 +851,21 @@ continue; } + const tableResult = + renderTable( + lines, + index + ); + + if (tableResult) { + blocks.push( + tableResult.html + ); + index = + tableResult.nextIndex; + continue; + } + if (isHeading(lines[index])) { blocks.push( renderHeading( diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 14523f3e..9d592767 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -3,6 +3,14 @@ let runtimeActionRowCounter = 0; let sceneSearchFadeTimer = null; const activeSceneSearchRuntimeActions = new Set(); +const DEEP_SEARCH_STACK_MOTION_MS = 220; +const DEEP_SEARCH_STACK_COLLAPSED_REVEAL_PX = 0; +const DEEP_SEARCH_STACK_FIRST_GAP_PX = 5; +const DEEP_SEARCH_STACK_EXPANDED_GAP_PX = 4; +const deepSearchStackAnimations = new Map(); +const deepSearchStackExpandedGroups = new Set(); +let deepSearchStackResizeFrameId = null; + function getSceneRoot() { return document.querySelector("main"); @@ -528,17 +536,581 @@ function findDeepSearchGroupRows( } -function setDeepSearchStackExpanded( +function readDeepSearchStackMarginTop( + row +) { + + const marginTop = Number.parseFloat( + window.getComputedStyle( + row + ).marginTop || "0" + ); + + return Number.isFinite(marginTop) + ? marginTop + : 0; + +} + +function writeDeepSearchStackVisualOffset( + row, + offset +) { + + const label = row.querySelector( + ":scope > .jin-runtime-action-label" + ); + + if (!label) { + return; + } + + const normalizedOffset = Number.isFinite(offset) + ? offset + : 0; + + if (Math.abs(normalizedOffset) <= 0.05) { + delete row.dataset.runtimeActionDeepSearchVisualOffset; + label.style.removeProperty( + "--jin-deep-search-stack-translate-y" + ); + return; + } + + row.dataset.runtimeActionDeepSearchVisualOffset = + String(normalizedOffset); + label.style.setProperty( + "--jin-deep-search-stack-translate-y", + `${normalizedOffset}px` + ); + +} + +function writeDeepSearchStackMarginTop( row, + marginTop +) { + + row.style.setProperty( + "margin-top", + `${marginTop}px`, + "important" + ); + +} + +function buildDeepSearchStackTargetMargins( + childRows, + expanded +) { + + return childRows.map((childRow, index) => { + if (index === 0) { + return DEEP_SEARCH_STACK_FIRST_GAP_PX; + } + + if (expanded) { + return DEEP_SEARCH_STACK_EXPANDED_GAP_PX; + } + + const previousRow = childRows[index - 1]; + const previousHeight = Math.max( + 0, + previousRow.offsetHeight || 0 + ); + + return ( + DEEP_SEARCH_STACK_COLLAPSED_REVEAL_PX + - previousHeight + ); + }); + +} + +function syncDeepSearchStackScrollAnchor() { + + if (!chatHistory) { + return; + } + + chatHistory.classList.toggle( + "jin-deep-search-stack-animating", + deepSearchStackAnimations.size > 0 + ); + +} + +function clearDeepSearchStackMotionNode( + node +) { + + if (!node) { + return; + } + + node.classList.remove( + "jin-deep-search-stack-motion", + "jin-deep-search-stack-motion-prep" + ); + node.style.removeProperty( + "--jin-deep-search-stack-motion-y" + ); + +} + +function cancelDeepSearchStackAnimation( + groupId +) { + + const activeAnimation = + deepSearchStackAnimations.get( + groupId + ); + + if (!activeAnimation) { + return; + } + + if (activeAnimation.frameId !== null) { + window.cancelAnimationFrame( + activeAnimation.frameId + ); + } + + if (activeAnimation.cleanupTimer !== null) { + window.clearTimeout( + activeAnimation.cleanupTimer + ); + } + + ( + activeAnimation.motionNodes || [] + ).forEach( + clearDeepSearchStackMotionNode + ); + + deepSearchStackAnimations.delete( + groupId + ); + syncDeepSearchStackScrollAnchor(); + +} + +function buildDeepSearchStackMotionEntries( + groupRows, + childRows +) { + + const entries = []; + const seenNodes = new Set(); + + childRows.forEach((childRow) => { + const label = childRow.querySelector( + ":scope > .jin-runtime-action-label" + ); + + if (!label || seenNodes.has(childRow)) { + return; + } + + seenNodes.add(childRow); + entries.push({ + node: childRow, + measureNode: label, + }); + }); + + const directGroupRows = groupRows.filter((groupRow) => ( + groupRow.parentElement === chatHistory + )); + const lastGroupRow = + directGroupRows[directGroupRows.length - 1]; + let sibling = + lastGroupRow + ? lastGroupRow.nextElementSibling + : null; + + while (sibling) { + if (!seenNodes.has(sibling)) { + seenNodes.add(sibling); + entries.push({ + node: sibling, + measureNode: sibling, + }); + } + + sibling = sibling.nextElementSibling; + } + + return entries; + +} + +function captureDeepSearchStackMotionEntries( + groupRows, + childRows +) { + + return buildDeepSearchStackMotionEntries( + groupRows, + childRows + ).map((entry) => ({ + ...entry, + beforeTop: + entry.measureNode.getBoundingClientRect().top, + })); + +} + +function finishDeepSearchStackAnimation( + groupId, + animationState +) { + + if ( + deepSearchStackAnimations.get( + groupId + ) !== animationState + ) { + return; + } + + ( + animationState.motionNodes || [] + ).forEach( + clearDeepSearchStackMotionNode + ); + + deepSearchStackAnimations.delete( + groupId + ); + syncDeepSearchStackScrollAnchor(); + +} + +function startDeepSearchStackFlip( + groupId, + motionEntries, + animationState +) { + + const movedEntries = []; + + motionEntries.forEach((entry) => { + if ( + !entry.node + || !entry.node.isConnected + || !entry.measureNode + || !entry.measureNode.isConnected + ) { + return; + } + + const afterTop = + entry.measureNode.getBoundingClientRect().top; + const deltaY = + entry.beforeTop - afterTop; + + if (Math.abs(deltaY) <= 0.1) { + return; + } + + entry.node.classList.add( + "jin-deep-search-stack-motion", + "jin-deep-search-stack-motion-prep" + ); + entry.node.style.setProperty( + "--jin-deep-search-stack-motion-y", + `${deltaY}px` + ); + movedEntries.push(entry); + }); + + animationState.motionNodes = + movedEntries.map((entry) => entry.node); + + if (!movedEntries.length) { + finishDeepSearchStackAnimation( + groupId, + animationState + ); + return; + } + + // Commit the inverse FLIP position with transitions disabled. The layout + // is already in its final state, so the rest of the chat does not reflow + // frame-by-frame while the visual motion plays on the compositor. + void chatHistory.offsetHeight; + + movedEntries.forEach((entry) => { + entry.node.classList.remove( + "jin-deep-search-stack-motion-prep" + ); + }); + + animationState.frameId = + window.requestAnimationFrame(() => { + if ( + deepSearchStackAnimations.get( + groupId + ) !== animationState + ) { + return; + } + + animationState.frameId = null; + + movedEntries.forEach((entry) => { + entry.node.style.setProperty( + "--jin-deep-search-stack-motion-y", + "0px" + ); + }); + + animationState.cleanupTimer = + window.setTimeout(() => { + finishDeepSearchStackAnimation( + groupId, + animationState + ); + }, DEEP_SEARCH_STACK_MOTION_MS + 40); + }); + +} + +function buildDeepSearchStackExpandedVisualOffsets( + childRows +) { + + let cumulativeOffset = 0; + + return childRows.map((childRow, index) => { + if (index === 0) { + return 0; + } + + const previousRow = childRows[index - 1]; + const previousHeight = Math.max( + 0, + previousRow.offsetHeight || 0 + ); + + cumulativeOffset += ( + previousHeight + + DEEP_SEARCH_STACK_EXPANDED_GAP_PX + - DEEP_SEARCH_STACK_COLLAPSED_REVEAL_PX + ); + + return cumulativeOffset; + }); + +} + +function syncDeepSearchStackChildVisibility( + groupId, + expanded +) { + + findDeepSearchGroupRows( + groupId + ).filter((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-child" + ) + )).forEach((childRow, index) => { + childRow.classList.toggle( + "jin-runtime-action-deep-search-child-obscured", + !expanded && index > 0 + ); + }); + +} + +function settleDeepSearchStackGeometry( + groupId, expanded ) { const groupRows = findDeepSearchGroupRows( - readDeepSearchGroupId( - row + groupId + ); + const parentRow = groupRows.find((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-parent" + ) + )); + const childRows = groupRows.filter((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-child" + ) + )); + + if (!parentRow || !childRows.length) { + return; + } + + cancelDeepSearchStackAnimation( + groupId + ); + + groupRows.forEach((groupRow) => { + groupRow.classList.toggle( + "jin-runtime-action-deep-search-stack-expanded", + expanded + ); + }); + + const targetMargins = + buildDeepSearchStackTargetMargins( + childRows, + false + ); + const targetVisualOffsets = expanded + ? buildDeepSearchStackExpandedVisualOffsets( + childRows + ) + : childRows.map(() => 0); + + childRows.forEach((childRow, index) => { + writeDeepSearchStackMarginTop( + childRow, + targetMargins[index] + ); + writeDeepSearchStackVisualOffset( + childRow, + targetVisualOffsets[index] + ); + }); + + syncDeepSearchStackChildVisibility( + groupId, + expanded + ); + +} + +function primeDeepSearchInsertedChild( + row, + groupId, + expanded +) { + + if (!row || !row.offsetHeight) { + return false; + } + + cancelDeepSearchStackAnimation( + groupId + ); + + writeDeepSearchStackMarginTop( + row, + -row.offsetHeight + ); + writeDeepSearchStackVisualOffset( + row, + 0 + ); + + window.requestAnimationFrame(() => { + if (!row.isConnected) { + return; + } + + settleDeepSearchStackGeometry( + groupId, + expanded + ); + }); + + return true; + +} + +function setDeepSearchStackExpanded( + row, + expanded, + options = {} +) { + + const groupId = readDeepSearchGroupId( + row + ); + const groupRows = findDeepSearchGroupRows( + groupId + ); + + if (!groupId || !groupRows.length) { + return; + } + + const wasExpanded = + deepSearchStackExpandedGroups.has( + groupId + ); + + if (expanded) { + deepSearchStackExpandedGroups.add( + groupId + ); + } else { + deepSearchStackExpandedGroups.delete( + groupId + ); + } + + const parentRow = groupRows.find((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-parent" + ) + )); + const childRows = groupRows.filter((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-child" ) + )); + + if (!parentRow || !childRows.length) { + return; + } + + if ( + wasExpanded === expanded + && options.force !== true + ) { + syncDeepSearchStackChildVisibility( + groupId, + expanded + ); + return; + } + + cancelDeepSearchStackAnimation( + groupId ); + const targetMargins = + buildDeepSearchStackTargetMargins( + childRows, + false + ); + const targetVisualOffsets = expanded + ? buildDeepSearchStackExpandedVisualOffsets( + childRows + ) + : childRows.map(() => 0); + + childRows.forEach((childRow, index) => { + writeDeepSearchStackMarginTop( + childRow, + targetMargins[index] + ); + writeDeepSearchStackVisualOffset( + childRow, + targetVisualOffsets[index] + ); + }); + groupRows.forEach((groupRow) => { groupRow.classList.toggle( "jin-runtime-action-deep-search-stack-expanded", @@ -546,63 +1118,217 @@ function setDeepSearchStackExpanded( ); }); + syncDeepSearchStackChildVisibility( + groupId, + expanded + ); + +} + +function deepSearchStackHasSelectedText() { + + if (!window.getSelection) { + return false; + } + + const selection = window.getSelection(); + + return Boolean( + selection + && !selection.isCollapsed + && String(selection).trim() + ); + } -function bindDeepSearchStackHover( +function bindDeepSearchStackClick( row ) { if ( - !row - || row.dataset.runtimeActionDeepSearchHoverBound === "true" + !row + || !row.classList.contains( + "jin-runtime-action-deep-search-child" + ) + || row.dataset.runtimeActionDeepSearchClickBound === "true" ) { return; } - row.dataset.runtimeActionDeepSearchHoverBound = + const clickTarget = row.querySelector( + ":scope > .jin-runtime-action-label" + ); + + if (!clickTarget) { + return; + } + + row.dataset.runtimeActionDeepSearchClickBound = "true"; - row.addEventListener( - "mouseenter", + clickTarget.addEventListener( + "click", () => { - setDeepSearchStackExpanded( - row, - true + const groupId = readDeepSearchGroupId( + row ); - } - ); - - row.addEventListener( - "mouseleave", - (event) => { - const nextRow = - event.relatedTarget - && event.relatedTarget.closest - ? event.relatedTarget.closest( - ".jin-runtime-action-row" - ) - : null; if ( - nextRow - && readDeepSearchGroupId( - nextRow - ) === readDeepSearchGroupId( - row - ) + !groupId + || deepSearchStackExpandedGroups.has( + groupId + ) + || deepSearchStackHasSelectedText() ) { return; } + const firstChildRow = findDeepSearchGroupRows( + groupId + ).find((groupRow) => ( + groupRow.classList.contains( + "jin-runtime-action-deep-search-child" + ) + )); + + if (firstChildRow !== row) { + return; + } + setDeepSearchStackExpanded( row, - false + true ); } ); } +function handleDeepSearchStackDocumentClick( + event +) { + + if (!deepSearchStackExpandedGroups.size) { + return; + } + + const target = event.target; + + Array.from( + deepSearchStackExpandedGroups + ).forEach((groupId) => { + const groupRows = findDeepSearchGroupRows( + groupId + ); + + if ( + target + && groupRows.some((groupRow) => ( + groupRow.contains(target) + )) + ) { + return; + } + + const anchorRow = groupRows[0]; + + if (!anchorRow) { + deepSearchStackExpandedGroups.delete( + groupId + ); + return; + } + + setDeepSearchStackExpanded( + anchorRow, + false + ); + }); + +} + +function scheduleDeepSearchStackGeometrySync() { + + if (deepSearchStackResizeFrameId !== null) { + window.cancelAnimationFrame( + deepSearchStackResizeFrameId + ); + } + + deepSearchStackResizeFrameId = window.requestAnimationFrame( + () => { + deepSearchStackResizeFrameId = null; + + const groupIds = new Set( + Array.from( + chatHistory.querySelectorAll( + ".jin-runtime-action-deep-search-parent" + ) + ).map( + readDeepSearchGroupId + ).filter(Boolean) + ); + + groupIds.forEach((groupId) => { + settleDeepSearchStackGeometry( + groupId, + deepSearchStackExpandedGroups.has( + groupId + ) + ); + }); + } + ); + +} + +function syncRuntimeActionDetailHover( + row, + label, + detail, + suppress = false +) { + + const hoverDetail = + suppress + ? "" + : String(detail || "").trim(); + + if (hoverDetail) { + label.title = hoverDetail; + row.title = hoverDetail; + label + .querySelectorAll( + ".jin-runtime-action-name, .jin-runtime-action-marker-count" + ) + .forEach(node => { + node.title = hoverDetail; + }); + label.classList.add( + "cursor-help" + ); + return; + } + + label.removeAttribute( + "title" + ); + row.removeAttribute( + "title" + ); + label + .querySelectorAll( + ".jin-runtime-action-name, .jin-runtime-action-marker-count" + ) + .forEach(node => { + node.removeAttribute("title"); + }); + label.classList.remove( + "cursor-help" + ); + +} + function syncDeepSearchChildStack( row ) { @@ -649,6 +1375,16 @@ function insertRuntimeActionRow( row.classList.add( "jin-runtime-action-enter" ); + + if ( + row.classList.contains( + "jin-runtime-action-deep-search" + ) + ) { + bindDeepSearchStackClick( + row + ); + } } if ( @@ -711,6 +1447,14 @@ function insertRuntimeActionRow( row ); + primeDeepSearchInsertedChild( + row, + groupId, + deepSearchStackExpandedGroups.has( + groupId + ) + ); + } function syncRuntimeActionSearchState( @@ -833,7 +1577,7 @@ function syncRuntimeActionSearchState( .forEach((icon) => { icon.remove(); }); - bindDeepSearchStackHover( + bindDeepSearchStackClick( row ); syncDeepSearchChildStack( @@ -843,7 +1587,7 @@ function syncRuntimeActionSearchState( } if (isDeepSearchParent) { - bindDeepSearchStackHover( + bindDeepSearchStackClick( row ); } @@ -2467,39 +3211,23 @@ function updateRuntimeActionRow( incomingDetail; } - if (detail) { - label.title = detail; - row.title = detail; - label - .querySelectorAll( - ".jin-runtime-action-name, .jin-runtime-action-marker-count" - ) - .forEach(node => { - node.title = detail; - }); - label.classList.add( - "cursor-help" - ); - } else { + if (!detail) { delete row.dataset.runtimeActionDetail; - label.removeAttribute( - "title" - ); - row.removeAttribute( - "title" - ); - label - .querySelectorAll( - ".jin-runtime-action-name, .jin-runtime-action-marker-count" - ) - .forEach(node => { - node.removeAttribute("title"); - }); - label.classList.remove( - "cursor-help" - ); } + syncRuntimeActionDetailHover( + row, + label, + detail, + row.classList.contains( + "jin-runtime-action-deep-search-child" + ) + || isDeepSearchChildRuntimeAction( + action, + options + ) + ); + if (action === "asset_action") { bindAssetResultPreview( label, @@ -3090,20 +3818,15 @@ function appendRuntimeAction( if (detail) { row.dataset.runtimeActionDetail = detail; - label.title = detail; - row.title = detail; - label - .querySelectorAll( - ".jin-runtime-action-name, .jin-runtime-action-marker-count" - ) - .forEach(node => { - node.title = detail; - }); - label.classList.add( - "cursor-help" - ); } + syncRuntimeActionDetailHover( + row, + label, + detail, + omitIcon + ); + if (action === "asset_action") { bindAssetResultPreview( label, @@ -3208,6 +3931,16 @@ window.addEventListener( scheduleRuntimeActionGuardGeometryUpdate ); +window.addEventListener( + "resize", + scheduleDeepSearchStackGeometrySync +); + +document.addEventListener( + "click", + handleDeepSearchStackDocumentClick +); + window.requestAnimationFrame( () => { updateRuntimeActionGuardGeometries(); diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index ae5b09bb..3e3a04a1 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -2508,7 +2508,8 @@ function appendStreamChunk( // STREAM END function finishStreamMessage( - messageId + messageId, + options = {} ) { const stream = @@ -2587,6 +2588,33 @@ function finishStreamMessage( flushRuntimeActionsAfterResponse( stream.role ); + + const answerBubble = ( + stream.group.answerContent + && stream.group.answerContent.closest + ) + ? stream.group.answerContent.closest(".jin-chat-bubble") + : null; + + if ( + answerBubble + && window.markJinCompletedAnswerBubble + ) { + const visibleAnswerText = String( + stream.group.answerContent.innerText + || stream.group.answerContent.textContent + || stream.answer + || "" + ).trim(); + window.markJinCompletedAnswerBubble( + answerBubble, + visibleAnswerText, + { + retryable: options.retryable === true, + retryCandidate: options.retryCandidate === true, + } + ); + } } window.JinThinkCitations.startThinkRuleCitationAnalysis( diff --git a/ui/static/js/logger/session-actions.js b/ui/static/js/logger/session-actions.js index df85a10f..44f78aa2 100644 --- a/ui/static/js/logger/session-actions.js +++ b/ui/static/js/logger/session-actions.js @@ -28,6 +28,35 @@ function normalizeSessionActionName(value) { .toUpperCase(); } +function normalizeDeepSearchSessionActionDisplay( + text, + detail = "", +) { + const normalizedText = + String(text || "").trim(); + const normalizedDetail = + String(detail || "").trim(); + const queryMatch = + normalizedText.match( + /^DEEP_WEB_SEARCH\s*:\s*(.+)$/i + ); + + if (!queryMatch) { + return { + text: normalizedText, + detail: normalizedDetail, + }; + } + + const query = + String(queryMatch[1] || "").trim(); + + return { + text: "DEEP_WEB_SEARCH", + detail: query || normalizedDetail, + }; +} + function normalizeSessionActionColor(value) { const match = String(value || "") .trim() @@ -186,19 +215,28 @@ function normalizeSessionActionParts( return null; } - const text = + let text = String(part.text || "").trim(); if (!text) { return null; } - const detail = + let detail = String(part.detail || "").trim(); + ({ text, detail } = + normalizeDeepSearchSessionActionDisplay( + text, + detail + )); + const message = String(part.message || "").trim(); + const contextDetail = + String(part.context_detail || "").trim(); + const id = String(part.id || "").trim(); @@ -224,6 +262,7 @@ function normalizeSessionActionParts( text, detail, message, + contextDetail, id, colors, count, @@ -245,6 +284,26 @@ function normalizeSessionActionParts( return []; } + const deepSearchDisplay = + normalizeDeepSearchSessionActionDisplay( + text + ); + + if ( + deepSearchDisplay.text === "DEEP_WEB_SEARCH" + && deepSearchDisplay.detail + ) { + return [{ + text: deepSearchDisplay.text, + detail: deepSearchDisplay.detail, + message: "", + id: "", + colors: [], + count: 0, + cancelled: false, + }]; + } + const detailSeparator = " - "; @@ -575,8 +634,20 @@ function buildSessionActionRow( ); } + const isDeepWebSearchAction = + normalizedActionName === "DEEP_WEB_SEARCH" + || normalizedActionName.startsWith( + "DEEP_WEB_SEARCH " + ); + const hoverText = - part.message || part.detail; + part.message + || part.detail + || ( + isDeepWebSearchAction + ? part.contextDetail + : "" + ); if (hoverText) { action.title = @@ -1092,6 +1163,7 @@ function markSessionActionCancelled( text: part.text, detail: part.detail, message: part.message, + context_detail: part.contextDetail, id: part.id, colors: part.colors, count: part.count, diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 1ecae886..56ffd0e4 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -12,11 +12,6 @@ const longTermFactsStorageKey = "jin.longTermFacts.v1"; const anonymousLongTermFactsStorageKey = "jin.longTermFacts.anonymous.v1"; - const idleTickIntervalMs = 60000; - const idleMinimumSeconds = 60; - - let lastIdleTickAt = 0; - let idleTimer = null; function normalizeText(value) { return String(value || "") @@ -647,63 +642,6 @@ }); } - function maybeSendIdleTick() { - if ( - typeof window.getJinUserIdleContext !== "function" - || typeof window.isJinGenerationRunning !== "function" - || window.isJinGenerationRunning() - ) { - return false; - } - - const idleContext = window.getJinUserIdleContext(); - if (!idleContext) { - return false; - } - - const now = Date.now(); - const userIdleSeconds = Math.floor( - Number(idleContext.user_idle_seconds || 0) - ); - - // While the user is actively typing/interacting, the shared idle clock is - // paused. Reset the L4 cadence too: when activity stops, background L4 - // maintenance must earn a fresh full idle minute before it can run again. - if (idleContext.user_idle_paused) { - lastIdleTickAt = now; - return false; - } - - if (userIdleSeconds < idleMinimumSeconds) { - return false; - } - - if (now - lastIdleTickAt < idleTickIntervalMs) { - return false; - } - - const sent = sendIfOpen({ - type: "l4_memory_idle_tick", - user_idle_seconds: userIdleSeconds, - records: storage.collectFactsMemoryRecords - ? storage.collectFactsMemoryRecords() - : [], - store: readStore(), - }); - - if (sent) { - lastIdleTickAt = now; - } - return sent; - } - - function startIdleMonitor() { - if (idleTimer !== null) { - return; - } - idleTimer = window.setInterval(maybeSendIdleTick, 5000); - } - const api = { readStore, writeStore, @@ -723,20 +661,10 @@ deleteFactLocally, requestFactDelete, requestFactRestore, - maybeSendIdleTick, - startIdleMonitor, }; window.JINRuntimeL4Memory = api; window.JinRuntime.l4Memory = api; window.syncFactsMemoryToRuntime = syncFactsMemoryToRuntime; window.syncLongTermMemoryToRuntime = syncLongTermMemoryToRuntime; - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", startIdleMonitor, { - once: true, - }); - } else { - window.setTimeout(startIdleMonitor, 0); - } }()); diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index e64db9ca..292776b2 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -637,10 +637,19 @@ runtimeMemoryLazyAppendBatch = null; } - function beginRuntimeMemoryLazyCollection(items, renderItem) { + function beginRuntimeMemoryLazyCollection(items, renderItem, options = {}) { const source = Array.isArray(items) ? items : []; const batchSize = getRuntimeMemoryLazyBatchSize(runtimeMemoryLazyMode); + const requestedInitialBatchSize = + Math.max( + 0, + Math.floor(Number(options.initialBatchSize)) + ); + let nextBatchSize = + requestedInitialBatchSize > 0 + ? requestedInitialBatchSize + : batchSize; clearRuntimeMemoryLazyCollection(); runtimeMemoryLazyTotalCount = source.length; @@ -649,7 +658,7 @@ const start = runtimeMemoryLazyRenderedCount; const end = Math.min( runtimeMemoryLazyTotalCount, - start + batchSize + start + nextBatchSize ); for (let index = start; index < end; index += 1) { @@ -657,6 +666,7 @@ } runtimeMemoryLazyRenderedCount = end; + nextBatchSize = batchSize; if (runtimeMemoryLazyRenderedCount >= runtimeMemoryLazyTotalCount) { runtimeMemoryLazyAppendBatch = null; @@ -666,7 +676,7 @@ }; runtimeMemoryLazyAppendBatch = appendBatch; - appendBatch(); + runtimeMemoryLazyAppendBatch(); } function resetRuntimeMemoryLazyRows(options = {}) { @@ -701,16 +711,45 @@ return false; } - const appended = runtimeMemoryLazyAppendBatch(); + let appendedAny = false; + let guard = 0; + + while (typeof runtimeMemoryLazyAppendBatch === "function") { + const appended = runtimeMemoryLazyAppendBatch(); + + if (!appended) { + break; + } + + appendedAny = true; + guard += 1; - if (!appended) { + applyMemoryReferenceHighlights({ + animateSort: false, + }); + + if ( + !memoryScroll + || guard >= 8 + ) { + break; + } + + const remaining = + memoryScroll.scrollHeight + - memoryScroll.clientHeight + - memoryScroll.scrollTop; + + if (remaining > RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX) { + break; + } + } + + if (!appendedAny) { return false; } runtimeMemoryLastBatchRevealAt = Date.now(); - applyMemoryReferenceHighlights({ - animateSort: false, - }); return true; } @@ -867,6 +906,10 @@ const sourceText = getActiveMemoryReferenceText(); + const longTermReferencedFactIds = + getLongTermMemoryReferencedFactIdsFromText( + sourceText + ); const rows = Array.isArray(options.rows) ? options.rows @@ -876,8 +919,8 @@ ) ); - // L4 is citation-gated. Keep it completely out of generic alias matching - // instead of building aliases only to reject the row afterwards. + // L4 is citation-gated for key/value alias matching, but explicit F### + // ids in the answer or reasoning still count as direct row references. const persistentRows = rows.filter((row) => ( !row.classList.contains("runtime-memory-l4-row") @@ -893,14 +936,22 @@ return; } - if (!row.classList.contains("runtime-memory-reference-hit")) { - return; - } - const wasBubbled = isLongTermMemoryRowBubbled(row); + const factId = + normalizeDelayedMemoryFactId( + row.dataset.longTermFactId + ); + const matched = + Boolean( + factId + && longTermReferencedFactIds.has(factId) + ); - row.classList.remove("runtime-memory-reference-hit"); + row.classList.toggle( + "runtime-memory-reference-hit", + matched + ); if (wasBubbled !== isLongTermMemoryRowBubbled(row)) { syncLongTermMemoryRowValueDisplay(row); @@ -1213,6 +1264,172 @@ }; } + function getLongTermMemoryReferencedFactIdsFromText(text) { + const matches = + String(text || "").match(/\bF[1-9]\d*\b/gi) || []; + const factIds = new Set(); + + matches.forEach((match) => { + const factId = normalizeDelayedMemoryFactId(match); + + if (factId) { + factIds.add(factId); + } + }); + + return factIds; + } + + function buildLongTermMemoryPriorityState( + records, + contextLoadedFactIds = new Set() + ) { + const activeIdentities = + getActiveThinkMemoryCitationIdentitySets(); + const explicitFactIds = + getLongTermMemoryReferencedFactIdsFromText( + getActiveMemoryReferenceText() + ); + const keyUsage = new Map(); + + records.forEach((fact) => { + const key = + normalizeRuntimeCitationIdentity( + String(fact && fact.key || "") + ); + + if (!key) { + return; + } + + keyUsage.set( + key, + Number(keyUsage.get(key) || 0) + 1 + ); + }); + + const priorityFactIds = new Set(); + + records.forEach((fact) => { + const factId = + normalizeDelayedMemoryFactId( + fact && fact.id + ); + const key = + String(fact && fact.key || "").trim(); + const value = + String(fact && fact.value || "").trim(); + const lineIdentity = + normalizeRuntimeCitationIdentity( + buildCitationRecordIdentity( + factId, + key, + value + ) + ); + const lineKey = + normalizeRuntimeCitationIdentity(key); + const lineText = + normalizeRuntimeCitationIdentity( + `${key}: ${value}` + ); + const matchedByStructuredCitation = + Boolean( + lineIdentity + && activeIdentities.lineIdentities.has(lineIdentity) + ) + || Boolean( + lineText + && activeIdentities.lineTexts.has(lineText) + ) + || Boolean( + lineKey + && Number(keyUsage.get(lineKey) || 0) === 1 + && activeIdentities.lineKeys.has(lineKey) + ); + + if ( + !factId + || ( + !contextLoadedFactIds.has(factId) + && !explicitFactIds.has(factId) + && !matchedByStructuredCitation + ) + ) { + return; + } + + priorityFactIds.add(factId); + }); + + return { + explicitFactIds, + priorityFactIds, + }; + } + + function syncLongTermMemoryPriorityRows() { + if ( + getRuntimeMemoryDisplayMode() !== "long_term" + || !runtimeMemoryText + ) { + return false; + } + + const records = getLongTermMemoryFactRecords(); + + if (!records.length) { + return false; + } + + const delayedReports = + getDelayedMemoryReportRecords(); + const contextLoadedFactIds = + buildContextLoadedDelayedMemoryFactIds( + delayedReports + ); + const priorityState = + buildLongTermMemoryPriorityState( + records, + contextLoadedFactIds + ); + const desiredPriorityIds = + priorityState.priorityFactIds; + const currentPriorityIds = new Set(); + + runtimeMemoryText + .querySelectorAll(".runtime-memory-l4-row[data-long-term-fact-id]") + .forEach((row) => { + const factId = + normalizeDelayedMemoryFactId( + row.dataset.longTermFactId + ); + + if ( + factId + && ( + row.classList.contains("runtime-memory-reference-hit") + || row.classList.contains("runtime-memory-citation-hit") + || row.classList.contains("runtime-memory-context-loaded-hit") + ) + ) { + currentPriorityIds.add(factId); + } + }); + + if ( + currentPriorityIds.size === desiredPriorityIds.size + && Array.from(desiredPriorityIds).every( + factId => currentPriorityIds.has(factId) + ) + ) { + return false; + } + + renderLongTermMemoryFacts(); + return true; + } + function applyThinkMemoryCitationHighlights(options = {}) { if (!runtimeMemoryText) { return; @@ -1323,6 +1540,7 @@ if (detail.active !== true) { activeThinkMemoryCitationSources.delete(sourceId); + syncLongTermMemoryPriorityRows(); applyThinkMemoryCitationHighlights(); return; } @@ -1368,6 +1586,7 @@ ); } + syncLongTermMemoryPriorityRows(); applyThinkMemoryCitationHighlights(); } @@ -1388,6 +1607,7 @@ // reasoning analysis publishes its own exact runtime-line matches. activeThinkMemoryCitationSources.clear(); + syncLongTermMemoryPriorityRows(); applyMemoryReferenceHighlights(); } function bindMemoryReferenceHighlightEvents() { @@ -3337,6 +3557,18 @@ activeMemoryId; } + if (options.interactiveLongTermMemory) { + const longTermFactId = + normalizeDelayedMemoryFactId( + line && line.id + ); + + if (longTermFactId) { + row.dataset.longTermFactId = + longTermFactId; + } + } + setRuntimeMemoryRowState( row, { @@ -3637,8 +3869,16 @@ persistGlow ); } + }, { + initialBatchSize: options.initialBatchSize, }); + applyMemoryReferenceHighlights({ + animateSort: false, + }); + sortHighlightedMemoryRows({ + animateSort: false, + }); } function getRuntimeMemoryLineStatus(line) { @@ -7857,6 +8097,38 @@ buildDelayedMemoryFactReportIndex( delayedReports ); + const priorityState = + buildLongTermMemoryPriorityState( + records, + contextLoadedFactIds + ); + const priorityRecords = []; + const overflowRecords = []; + + records.forEach((fact) => { + const factId = + normalizeDelayedMemoryFactId( + fact && fact.id + ); + + if ( + factId + && priorityState.priorityFactIds.has(factId) + ) { + priorityRecords.push(fact); + return; + } + + overflowRecords.push(fact); + }); + + const orderedRecords = [ + ...priorityRecords, + ...overflowRecords, + ]; + const initialBatchSize = + priorityRecords.length + + LONG_TERM_MEMORY_LAZY_BATCH_SIZE; if (runtimeMemoryText) { runtimeMemoryText.innerHTML = ""; @@ -7872,11 +8144,12 @@ "No long-term facts stored."; } else { appendRuntimeMemoryLineRows( - records, + orderedRecords, false, { applyFlash: false, interactiveLongTermMemory: true, + initialBatchSize, buildLine: fact => buildLongTermMemoryLine( fact, contextLoadedFactIds, diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 64c64372..6e67b967 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -631,6 +631,7 @@ session_snapshot: { ...previousSessionSnapshot, tool_results: [], + tool_results_cleared_at: new Date().toISOString(), }, }); diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 17f6aa9b..6ad3cc53 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -516,6 +516,13 @@ function persistRuntimeMemorySnapshot( persistedSnapshot ); + // Facts Memory still originates in the browser profile, but scheduling no + // longer does. Hand fresh pending fields to the backend immediately; from + // this point L4 consolidation is driven entirely by the server scheduler. + if (typeof window.syncFactsMemoryToRuntime === "function") { + window.syncFactsMemoryToRuntime(); + } + writeLatestRuntimeMemory({ version: 1, session_id: diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 6c739a76..0d384416 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -169,6 +169,71 @@ function sendSocketMessage( window.sendSocketMessage = sendSocketMessage; +window.requestJinLastResponseRetry = function () { + if ( + generationRunning + || !isWebSocketOpen() + ) { + return false; + } + + const runtimeStatus = ( + window.jinRuntimeConfig + && window.jinRuntimeConfig.runtimeStatus + ) || {}; + if ( + runtimeStatus.brain === false + && runtimeStatus.service === false + ) { + return false; + } + + const payload = { + type: "retry_last_response", + }; + + if ( + window.JinPanels + && typeof window.JinPanels.getRuntimeAvatarSnapshot === "function" + ) { + payload.runtime_avatar = + window.JinPanels.getRuntimeAvatarSnapshot(); + } + + if ( + window.JinRuntime + && window.JinRuntime.runtime + && window.JinRuntime.runtime.getActiveMemoryRecords + ) { + payload.active_memory_records = + window.JinRuntime.runtime.getActiveMemoryRecords(); + } + + if (window.clearLatestJinMemoryReferenceText) { + window.clearLatestJinMemoryReferenceText(); + } + + if (window.focusBrainContextTab) { + window.focusBrainContextTab(); + } + + const sent = sendSocketMessage( + payload + ); + if (!sent) { + return false; + } + + if (window.markSessionActivityDirty) { + window.markSessionActivityDirty(); + } + + setGenerationState( + true + ); + return true; +}; + window.sendRuntimeMemoryDeleteSlot = function (payload) { const key = String( payload diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 1b271a47..e43baa1c 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -10,6 +10,169 @@ function resolveMessageRole( } +function appendSessionBootstrapBoundary( + chatHistory +) { + + if (!chatHistory) { + return null; + } + + const now = new Date(); + const months = [ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december", + ]; + const weekdays = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + const labelText = + `${now.getDate()} ` + + `${months[now.getMonth()]}, ` + + weekdays[now.getDay()]; + + const divider = + document.createElement("div"); + divider.className = + "jin-session-restore-divider"; + divider.setAttribute( + "role", + "separator" + ); + divider.setAttribute( + "aria-label", + `Current session: ${labelText}` + ); + + const label = + document.createElement("span"); + label.className = + "jin-session-restore-divider-label"; + label.textContent = + labelText; + + divider.appendChild( + label + ); + chatHistory.appendChild( + divider + ); + + return divider; + +} + +function handleSessionBootstrapChatTail( + data +) { + + if ( + window.jinArchivedSessionRestorePayload + || !data + || !Array.isArray(data.turns) + ) { + return; + } + + const chatHistory = + document.getElementById( + "chat-history" + ); + + if (!chatHistory) { + return; + } + + const existingMessages = + chatHistory.querySelectorAll( + ".jin-message-shell" + ); + + if (existingMessages.length) { + return; + } + + const turns = data.turns + .filter(turn => ( + turn + && typeof turn === "object" + && String(turn.user || "").trim() + && String(turn.jin || "").trim() + )) + .slice(-3); + + turns.forEach((turn, index) => { + appendChatMessage( + "user", + String(turn.user || "") + ); + + const messageId = + `bootstrap-tail-${String( + data.source_session_id + || "session" + ).replace(/[^a-zA-Z0-9_.:-]/g, "_")}-${index}`; + + startStreamMessage( + messageId, + "brain", + null + ); + + const reasoning = + String(turn.reasoning || "").trim(); + + if (reasoning) { + appendThinkingChunk( + messageId, + reasoning + ); + } + + appendStreamChunk( + messageId, + String(turn.jin || "") + ); + finishStreamMessage( + messageId, + { retryable: false } + ); + }); + + const divider = + appendSessionBootstrapBoundary( + chatHistory + ); + + if ( + divider + && typeof window.activateLiveUserTurnViewport + === "function" + ) { + window.activateLiveUserTurnViewport( + divider + ); + } + +} + + function handleSessionActionsUpdate( data ) { @@ -76,6 +239,12 @@ function handleSocketError( false ); + if (window.clearJinCompletedAnswerRetryCandidate) { + window.clearJinCompletedAnswerRetryCandidate(); + } + + window.jinCurrentResponseRetryable = false; + if (window.releaseActiveStreamAvatar) { window.releaseActiveStreamAvatar(); } @@ -153,13 +322,29 @@ function handleMetabolismUpdate( } } -function handleAgentRuntimeStart() { +function handleAgentRuntimeStart(data) { + window.jinCurrentResponseRetryable = Boolean( + data && data.retryable_response + ); + + if (window.confirmJinLastResponseRetryStarted) { + window.confirmJinLastResponseRetryStarted(); + } + setGenerationState( true ); } -function handleAgentRuntimeEnd() { +function handleAgentRuntimeEnd(data) { + + if (data && data.retryable_response === true) { + if (window.commitJinCompletedAnswerRetryCandidate) { + window.commitJinCompletedAnswerRetryCandidate(); + } + } else if (window.clearJinCompletedAnswerRetryCandidate) { + window.clearJinCompletedAnswerRetryCandidate(); + } if (window.releaseActiveStreamAvatar) { window.releaseActiveStreamAvatar(); @@ -175,6 +360,7 @@ function handleAgentRuntimeEnd() { false ); + window.jinCurrentResponseRetryable = false; window.jinActiveTurnUserIdleSeconds = 0; if (window.jinResetUserIdleTimer) { @@ -235,7 +421,12 @@ function handleMessageEnd( ); finishStreamMessage( - data.message_id + data.message_id, + { + retryCandidate: Boolean( + window.jinCurrentResponseRetryable + ) + } ); if (window.flushRuntimeTelemetryRender) { @@ -266,9 +457,16 @@ function handleMessageError( } finishStreamMessage( - data.message_id + data.message_id, + { retryable: false } ); + if (window.clearJinCompletedAnswerRetryCandidate) { + window.clearJinCompletedAnswerRetryCandidate(); + } + + window.jinCurrentResponseRetryable = false; + if (window.flushRuntimeTelemetryRender) { window.flushRuntimeTelemetryRender({ final: true @@ -277,6 +475,34 @@ function handleMessageError( } +function handleRetryLastResponseRejected( + data +) { + setGenerationState( + false + ); + + if (window.restoreJinDeletedRetryBubble) { + window.restoreJinDeletedRetryBubble(); + } + + appendLog( + "[RETRY]", + `Retry rejected: ${String(data && data.reason || "unavailable")}` + ); +} + + +registerSocketMessageHandler( + "retry_last_response_rejected", + handleRetryLastResponseRejected +); + +registerSocketMessageHandler( + "session_bootstrap_chat_tail", + handleSessionBootstrapChatTail +); + registerSocketMessageHandler( "session_actions_update", handleSessionActionsUpdate diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 4f5d6144..bbacf5db 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -901,6 +901,27 @@ function buildRuntimeActionDisplayText( } +function shouldUseDeepSearchStartedDisplayNameOnly( + action, + status, + deepSearchParent, + deepSearchPayloadReady +) { + + return ( + action === "deep_web_search" + && deepSearchParent + && [ + "started", + "start", + "pending", + "running", + ].includes(status) + && !deepSearchPayloadReady + ); + +} + function readRuntimeActionObjectValue( value ) { @@ -1471,21 +1492,49 @@ function handleRuntimeAction( ).trim() : ""; + const displayName = + getRuntimeActionDisplayName( + data, + action + ); + + const sceneEffect = + getRuntimeActionSceneEffect( + data + ); + + const deepSearchChild = + data.deep_search_child === true + || data.deepSearchChild === true; + const deepSearchParent = + data.deep_search_parent === true + || data.deepSearchParent === true; + const deepSearchPayloadReady = + data.deep_search_payload_ready === true + || data.deepSearchPayloadReady === true; + const displayText = - activeMemoryUpdateTitle + shouldUseDeepSearchStartedDisplayNameOnly( + action, + status, + deepSearchParent, + deepSearchPayloadReady + ) + ? displayName + : activeMemoryUpdateTitle ? ( - `${getRuntimeActionDisplayName(data, action)}: ` + `${displayName}: ` + activeMemoryUpdateTitle ) : updateL4FactsMessage ? ( - `${getRuntimeActionDisplayName(data, action)}: ` + `${displayName}: ` + updateL4FactsMessage ) : reportScopedDelayedAction && delayedMemoryPreview.title ? ( - `${getRuntimeActionDisplayName(data, action)}: ` + `${displayName}: ` + delayedMemoryPreview.title + ( delayedMemoryTriggerDetail @@ -1494,24 +1543,6 @@ function handleRuntimeAction( ) ) : baseDisplayText; - - const displayName = - getRuntimeActionDisplayName( - data, - action - ); - - const sceneEffect = - getRuntimeActionSceneEffect( - data - ); - - const deepSearchChild = - data.deep_search_child === true - || data.deepSearchChild === true; - const deepSearchParent = - data.deep_search_parent === true - || data.deepSearchParent === true; const deepSearchParentId = String( data.deep_search_parent_id diff --git a/ui/templates/index.html b/ui/templates/index.html index 4aee8643..8866ed50 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -11,9 +11,9 @@ - - - + + + @@ -489,7 +489,7 @@ - + @@ -508,13 +508,13 @@ - + - - + + @@ -523,20 +523,20 @@ - + - - - + + + - + - + - - + + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index ea755295..6929cb53 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -43,7 +43,6 @@ is_delayed_memory_report_id, slugify_delayed_memory_title, ) -from .idle_utils import parse_idle_seconds from .update_l4_facts_utils import parse_update_l4_facts_payload from .update_active_memory_utils import parse_update_active_memory_payload from .jin_color_utils import ( @@ -155,7 +154,6 @@ "normalize_delayed_memory_fact_roles", "normalize_long_term_fact_ids", "parse_delayed_memory_payload", - "parse_idle_seconds", "parse_jin_position_payload", "parse_jin_size_payload", "refresh_active_memory_runtime_metadata", diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 4a5d8867..6aac4fb5 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -15,7 +15,6 @@ RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, RUNTIME_ACTION_JIN_POSITION, @@ -49,7 +48,6 @@ from .check_todo_utils import build_check_todo_payload from .save_active_memory_utils import build_save_active_memory_payload from .create_todo_list_utils import build_create_todo_list_payload -from .idle_utils import build_idle_payload from .jin_color_utils import build_jin_color_payload from .jin_size_utils import build_jin_size_payload from .jin_position_utils import build_jin_position_payload @@ -107,7 +105,6 @@ def format_runtime_trigger_words_message( ) REPEATABLE_RUNTIME_ACTIONS = frozenset({ - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, RUNTIME_ACTION_JIN_POSITION, @@ -568,7 +565,6 @@ def normalize_runtime_action_name( "INTERNAL_ACTION_CREATE_TODO_LIST": RUNTIME_ACTION_CREATE_TODO_LIST, "RESOLVE_TODO": RUNTIME_ACTION_RESOLVE_TODO, "CHECK_TODO": RUNTIME_ACTION_CHECK_TODO, - "IDLE": RUNTIME_ACTION_IDLE, "JIN_SIZE": RUNTIME_ACTION_JIN_SIZE, "JIN_POSITION": RUNTIME_ACTION_JIN_POSITION, "JIN_SPEED": RUNTIME_ACTION_JIN_SPEED, @@ -671,7 +667,6 @@ def build_deep_web_search_payload( _ACTION_PAYLOAD_BUILDERS = { - RUNTIME_ACTION_IDLE: build_idle_payload, RUNTIME_ACTION_JIN_COLOR: build_jin_color_payload, RUNTIME_ACTION_JIN_SIZE: build_jin_size_payload, RUNTIME_ACTION_JIN_POSITION: build_jin_position_payload, @@ -1056,16 +1051,9 @@ def handle_marker( ) if action is None: - # IDLE is intentionally numeric: ``IDLE: `` with an - # optional ``s``/``ms`` suffix is a runtime marker. The suffix is - # ignored and the number always means seconds. Plain prose and - # malformed candidates such as ```` stay visible. - if ( - normalized_action_name == RUNTIME_ACTION_IDLE - or _is_payloadless_jin_color_marker( - raw_marker, - query, - ) + if _is_payloadless_jin_color_marker( + raw_marker, + query, ): return raw_marker diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index 27df61ed..9c67c630 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -7,7 +7,6 @@ RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, RUNTIME_ACTION_JIN_POSITION, @@ -51,7 +50,6 @@ normalize_jin_speed_value, normalize_jin_size_dict, normalize_jin_size_payload, - parse_idle_seconds, parse_update_active_memory_payload, ) from utils.skills_asset_utils import ( @@ -86,9 +84,6 @@ apply_attachment_actions, ) from utils.actions.update_l4_facts_actions import schedule_update_l4_facts_actions -from utils.actions.jin_color_actions import ( - apply_idle_actions, -) from utils.actions.jin_visual_sequence_actions import ( emit_jin_visual_sequences, ) @@ -266,7 +261,6 @@ def with_action_context(payload: dict) -> dict: skill_workflow_action_names = { *skill_state_action_names, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_DEEP_WEB_SEARCH, RUNTIME_ACTION_WEB_SEARCH, RUNTIME_ACTION_JIN_COLOR, @@ -398,18 +392,6 @@ def build_runtime_action_dedup_key( payload_identity = normalize_skill_name( action.payload ) - elif action_name == RUNTIME_ACTION_IDLE: - seconds = parse_idle_seconds( - action.payload - ) - payload_identity = ( - f"{seconds}s" - if seconds is not None - else str( - action.payload - or "" - ) - ) else: payload_identity = str( action.payload @@ -778,27 +760,6 @@ def accept_runtime_action_once_per_message( rejected_action_events[id(action)] = rejection_event continue - if action.name == RUNTIME_ACTION_IDLE: - seconds = parse_idle_seconds( - action.payload - ) - if seconds is None: - continue - - if not accept_runtime_action_once_per_message( - action, - f"{seconds}s", - ): - continue - - accepted_action_names.add( - action_event_name - ) - filtered_actions.append( - action - ) - continue - if action.name == RUNTIME_ACTION_JIN_COLOR: current_jin_color = jin_color jin_color_last_by_message[ @@ -1331,15 +1292,6 @@ def accept_runtime_action_once_per_message( "context": action_context_snapshot, }) - elif action.name == RUNTIME_ACTION_IDLE: - idle_seconds = parse_idle_seconds( - action.payload - ) - if idle_seconds is not None: - action_event["seconds"] = idle_seconds - action_event["payload"] = f"{idle_seconds}s" - action_event["deferred_follow_up"] = True - elif action.name == RUNTIME_ACTION_JIN_COLOR: color = normalize_jin_color_payload( action.payload @@ -1461,33 +1413,6 @@ def accept_runtime_action_once_per_message( f"{deep_search_objective}" ) - deep_search_was_already_started = any( - isinstance( - record, - dict, - ) - and str( - record.get( - "action", - "", - ) - or "" - ).strip().lower() == RUNTIME_ACTION_DEEP_WEB_SEARCH.lower() - and str( - record.get( - "id", - "", - ) - or "" - ).strip() == runtime_action_id - for record in getattr( - context, - "runtime_active_action_markers", - [], - ) - or [] - ) - mark_runtime_action_started( context, action=action_event.get( @@ -1514,10 +1439,7 @@ def accept_runtime_action_once_per_message( context_snapshot=action_context_snapshot, ) - if ( - deep_search_objective - and not deep_search_was_already_started - ): + if deep_search_objective: emitter = getattr( context, "emitter", @@ -1534,12 +1456,13 @@ def accept_runtime_action_once_per_message( "type": "runtime_action", "action": RUNTIME_ACTION_DEEP_WEB_SEARCH.lower(), "id": runtime_action_id, - "status": "started", + "status": "running", "display_name": runtime_action_display_name, "text": runtime_action_display_text, "query": deep_search_objective, "scene_effect": "search", "deep_search_parent": True, + "deep_search_payload_ready": True, "close_tag": runtime_action_has_close_tag( action.name ), @@ -1749,12 +1672,6 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_CLEAN_TOOL_RESULTS ] - idle_actions = [ - action - for action in filtered_actions - if action.name == RUNTIME_ACTION_IDLE - ] - jin_color_actions = [ action for action in filtered_actions @@ -1837,12 +1754,12 @@ def accept_runtime_action_once_per_message( None, ) - idle_records = await apply_idle_actions( + # Explicit L4 edits start immediately on their own background lane. + # Do not make them wait behind unrelated runtime action handlers. + schedule_update_l4_facts_actions( context, - idle_actions, - assistant_message=assistant_message, - resolved_user_message=resolved_user_message, - action_context_snapshot=action_context_snapshot, + update_l4_facts_actions, + action_display_ids=action_display_ids, log_runtime=log_runtime, with_action_context=with_action_context, ) @@ -1959,14 +1876,6 @@ def accept_runtime_action_once_per_message( with_action_context=with_action_context, ) - schedule_update_l4_facts_actions( - context, - update_l4_facts_actions, - action_display_ids=action_display_ids, - log_runtime=log_runtime, - with_action_context=with_action_context, - ) - await emit_saved_asset_results( context, saved_asset_results, @@ -2029,9 +1938,6 @@ def accept_runtime_action_once_per_message( + len( clean_tool_result_actions ) - + len( - idle_records - ) + len( jin_color_actions ) diff --git a/utils/actions/idle_utils.py b/utils/actions/idle_utils.py deleted file mode 100644 index 82b8503f..00000000 --- a/utils/actions/idle_utils.py +++ /dev/null @@ -1,45 +0,0 @@ -import re - - -IDLE_SECONDS_RE = re.compile( - r"^\s*(?P\d+)(?:\s*(?:m?s))?\s*$", - re.IGNORECASE, -) - - - -def parse_idle_seconds( - payload: str, -) -> int | None: - - match = IDLE_SECONDS_RE.fullmatch( - str(payload or "") - ) - - if match is None: - return None - - try: - return int( - match.group("seconds") - ) - except ( - TypeError, - ValueError, - ): - return None - - -def build_idle_payload( - query: str, - placeholder_payloads=(), -) -> str | None: - - seconds = parse_idle_seconds( - query - ) - - if seconds is None: - return None - - return f"{seconds}s" diff --git a/utils/actions/jin_color_actions.py b/utils/actions/jin_color_actions.py index 48e4802c..a1bef202 100644 --- a/utils/actions/jin_color_actions.py +++ b/utils/actions/jin_color_actions.py @@ -1,123 +1,11 @@ from contracts.rules_assembler import ( - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, - get_runtime_action_display_name, - runtime_action_has_close_tag, ) from utils.actions import ( normalize_jin_color_payload, - parse_idle_seconds, ) -async def apply_idle_actions( - context, - idle_actions, - *, - assistant_message, - resolved_user_message, - action_context_snapshot, - log_runtime, - with_action_context, -): - from utils.brain_client_utils import ( - build_runtime_action_event_display_fields, - schedule_idle_followup, - ) - - idle_records = [] - - for action in idle_actions: - seconds = parse_idle_seconds( - action.payload - ) - if seconds is None: - continue - - idle_record = schedule_idle_followup( - context, - seconds=seconds, - source_message=str(assistant_message or ""), - user_message=resolved_user_message, - context_snapshot=action_context_snapshot, - ) - idle_records.append(idle_record) - - if log_runtime is not None: - await log_runtime( - "[RUNTIME ACTION] " - f"idle scheduled for {seconds}s " - f"id={idle_record['id']!r}" - ) - - if idle_records: - emitter = getattr( - context, - "emitter", - None, - ) - emit = getattr( - emitter, - "emit", - None, - ) - runtime_turn_id = str( - getattr( - context, - "runtime_current_turn_id", - "", - ) - or "" - ).strip() - - def with_runtime_turn(payload): - if runtime_turn_id: - payload["runtime_turn_id"] = runtime_turn_id - - return payload - - if emit is not None: - for idle_record in idle_records: - idle_id = str( - idle_record.get( - "id", - "", - ) - or "" - ) - idle_payload = ( - f"{int(idle_record.get('seconds', 0) or 0)}s" - ) - await emit(with_action_context(with_runtime_turn({ - "type": "runtime_action", - "action": "idle", - "id": idle_id, - "status": "started", - **build_runtime_action_event_display_fields( - RUNTIME_ACTION_IDLE, - idle_payload, - ), - "payload": idle_payload, - "detail": idle_payload, - }))) - await emit(with_action_context(with_runtime_turn({ - "type": "runtime_action", - "action": "idle", - "id": idle_id, - "status": "completed", - "display_name": get_runtime_action_display_name( - RUNTIME_ACTION_IDLE - ), - "close_tag": runtime_action_has_close_tag( - RUNTIME_ACTION_IDLE - ), - "payload": idle_payload, - "detail": idle_payload, - }))) - - return idle_records - - async def emit_jin_color_actions( context, jin_color_actions, diff --git a/utils/actions/update_l4_facts_actions.py b/utils/actions/update_l4_facts_actions.py index 349eaba8..3919db28 100644 --- a/utils/actions/update_l4_facts_actions.py +++ b/utils/actions/update_l4_facts_actions.py @@ -1,6 +1,8 @@ from __future__ import annotations import asyncio +import contextlib +import time from contracts.rules_assembler import ( RUNTIME_ACTION_UPDATE_L4_FACTS, @@ -10,9 +12,113 @@ ) from runtime.L4_memory import run_l4_jin_note from utils.actions.update_l4_facts_utils import parse_update_l4_facts_payload +from utils.chat_log import append_chat_runtime_event +from utils.tool_results import ( + TOOL_RESULT_KIND_L4, + record_runtime_tool_result, +) from utils.runtime_action_abort import mark_runtime_action_completed +def _build_update_l4_tool_result( + result: dict, + *, + note: dict, +) -> dict: + + change = ( + result.get("change") + if isinstance(result.get("change"), dict) + else {} + ) + status = str(result.get("status", "") or "").strip() + summary = { + "ok": status == "completed", + "changed": bool(result.get("changed") or change.get("changed")), + } + + action = str(change.get("action", "") or "").strip() + if action: + summary["action"] = action + + source_fact_ids = [ + str(item or "").strip() + for item in ( + change.get("selected_fact_ids", []) + or note.get("fact_ids", []) + or [] + ) + if str(item or "").strip() + ] + if source_fact_ids: + summary["source_fact_ids"] = source_fact_ids + + output_facts = [] + for key in ("replacement_facts", "new_facts"): + for fact in change.get(key, []) or []: + if not isinstance(fact, dict): + continue + compact_fact = { + field: str(fact.get(field, "") or "").strip() + for field in ("id", "key", "value", "category") + if str(fact.get(field, "") or "").strip() + } + if compact_fact: + output_facts.append(compact_fact) + + if len(output_facts) == 1: + fact = output_facts[0] + if fact.get("id"): + summary["fact_id"] = fact["id"] + for field in ("key", "value", "category"): + if fact.get(field): + summary[field] = fact[field] + elif output_facts: + summary["facts"] = output_facts + + if not summary["ok"]: + summary["error"] = str( + result.get("reason") + or "l4_update_failed" + ).strip() + + return summary + + +def _record_update_l4_tool_result( + context, + *, + action_id: str, + note: dict, + result: dict, +) -> None: + + created_at = time.time() + summary = _build_update_l4_tool_result( + result, + note=note, + ) + record_runtime_tool_result( + context, + TOOL_RESULT_KIND_L4, + summary, + result_id=action_id, + created_at=created_at, + ) + + with contextlib.suppress(Exception): + append_chat_runtime_event( + context, + event="runtime_tool_result", + payload={ + "kind": TOOL_RESULT_KIND_L4, + "id": action_id, + "result": summary, + "created_at": created_at, + }, + ) + + async def _emit_update_l4_facts_result( context, *, @@ -83,18 +189,24 @@ async def _run_update_l4_facts_action( and previous_l4_task is not current_task and not previous_l4_task.done() ): - # An explicit JIN note is foreground memory work. It must not wait - # behind an idle consolidation that is using the same model. + # Explicit user-requested L4 work owns the foreground lane. Idle + # consolidation must be cancelled without becoming a prerequisite + # for the edit itself: a provider can take time to unwind a + # cancelled HTTP generation, and awaiting that task here makes the + # visible UPDATE_L4_FACTS bubble look stuck. Give cancellation one + # event-loop turn, then start the focused note immediately. if previous_l4_kind == "idle": previous_l4_task.cancel() - try: - await previous_l4_task - except asyncio.CancelledError: - pass - except Exception: - # The note still gets its own attempt after a failed previous - # background consolidation. - pass + await asyncio.sleep(0) + else: + try: + await previous_l4_task + except asyncio.CancelledError: + pass + except Exception: + # The note still gets its own attempt after a failed + # previous foreground L4 edit. + pass if getattr(context, "runtime_l4_memory_update_task", None) is current_task: context.runtime_l4_memory_update_kind = "jin_note" @@ -114,6 +226,13 @@ async def _run_update_l4_facts_action( ) ) + _record_update_l4_tool_result( + context, + action_id=action_id, + note=note, + result=result, + ) + await _emit_update_l4_facts_result( context, action_id=action_id, @@ -134,6 +253,12 @@ async def _run_update_l4_facts_action( "[RUNTIME ACTION] update_l4_facts failed: " f"{type(error).__name__}" ) + _record_update_l4_tool_result( + context, + action_id=action_id, + note=note, + result=result, + ) await _emit_update_l4_facts_result( context, action_id=action_id, @@ -171,6 +296,34 @@ def schedule_update_l4_facts_actions( action_id = str( action_display_ids.get(id(action), "") or "" ).strip() + created_at = time.time() + message = str(note.get("message", "") or "").strip() + session_action = { + "text": ( + f"UPDATE_L4_FACTS: {message}" + if message + else "UPDATE_L4_FACTS" + ), + "created_at": created_at, + "parts": [{ + "text": "UPDATE_L4_FACTS", + **({"id": action_id} if action_id else {}), + **({"message": message} if message else {}), + }], + } + with contextlib.suppress(Exception): + append_chat_runtime_event( + context, + event="runtime_action_request", + payload={ + "action": RUNTIME_ACTION_UPDATE_L4_FACTS, + "id": action_id, + "fact_ids": list(note.get("fact_ids", []) or []), + "message": message, + "session_action": session_action, + "created_at": created_at, + }, + ) previous_l4_task = getattr( context, "runtime_l4_memory_update_task", diff --git a/utils/actions/update_l4_facts_utils.py b/utils/actions/update_l4_facts_utils.py index 54e16d8f..9f2b8cc4 100644 --- a/utils/actions/update_l4_facts_utils.py +++ b/utils/actions/update_l4_facts_utils.py @@ -8,8 +8,13 @@ L4_FACT_ID_RE = re.compile(r"^F[1-9]\d*$", re.IGNORECASE) L4_FACT_ID_SCAN_RE = re.compile(r"\bF[1-9]\d*\b", re.IGNORECASE) FORBIDDEN_UPDATE_L4_FACTS_NOTE_RE = re.compile( - r"\b(delete|deleted|deleting|remove|removed|removing|erase|erased|" - r"erasing|drop|dropped|dropping|purge|purged|purging)\b", + ( + r"\b(?:delete|erase|drop|purge|remove)\b\s+" + r"(?:(?:the|this)\s+)?" + r"(?:(?:l4|long[- ]term)\s+)?" + r"(?:fact\s+)?" + r"(?:F[1-9]\d*|fact\b|memory\b)" + ), re.IGNORECASE, ) diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index d8c61255..2ffe6304 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -67,7 +67,6 @@ def get_brain_runtime_config(): RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, @@ -114,7 +113,6 @@ def get_brain_runtime_config(): normalize_active_memory_custom_field_value, parse_delayed_memory_payload, parse_update_active_memory_payload, - parse_idle_seconds, normalize_jin_color_payload, normalize_delayed_memory_attachment_ids, normalize_delayed_memory_fact_ids, @@ -140,9 +138,6 @@ def get_brain_runtime_config(): record_runtime_tool_result, remove_runtime_tool_results, ) -from utils.tool_results_context import ( - strip_tools_results_context, -) from utils.runtime_todo import ( apply_runtime_todo_action_result, attach_runtime_todo_item_to_result, @@ -3019,235 +3014,6 @@ async def log_runtime_action_marker_removals( ) -def _track_background_task( - context, - task: asyncio.Task, -) -> None: - - tasks = getattr( - context, - "background_tasks", - None, - ) - - if not isinstance(tasks, set): - tasks = set() - setattr( - context, - "background_tasks", - tasks, - ) - - tasks.add(task) - task.add_done_callback( - tasks.discard - ) - - -async def _enqueue_idle_followup_after_delay( - context, - record: dict, -) -> None: - - seconds = max( - 0, - int(record.get("seconds", 0) or 0), - ) - - await asyncio.sleep(seconds) - - scheduled_generation = int( - record.get( - "tool_results_generation", - 0, - ) - or 0 - ) - current_generation = int( - getattr( - context, - "runtime_tool_results_generation", - 0, - ) - or 0 - ) - context_snapshot = deepcopy( - record.get( - "context_snapshot", - {}, - ) - ) - if not isinstance( - context_snapshot, - dict, - ): - context_snapshot = {} - - if scheduled_generation != current_generation: - context_snapshot["system_prompt"] = ( - strip_tools_results_context( - context_snapshot.get( - "system_prompt", - "", - ) - ) - ) - - record = { - **record, - "context_snapshot": context_snapshot, - "tool_results_generation": current_generation, - "fired_at": time.time(), - } - queue = getattr( - context, - "runtime_pending_requests_queue", - None, - ) - - if queue is not None: - await queue.put({ - "type": "idle_followup", - "idle_followup": record, - }) - return - - pending = getattr( - context, - "runtime_pending_idle_followups", - None, - ) - if not isinstance(pending, list): - pending = [] - setattr( - context, - "runtime_pending_idle_followups", - pending, - ) - - pending.append(record) - - -def schedule_idle_followup( - context, - *, - seconds: int, - source_message: str, - user_message: str, - context_snapshot: dict | None, -) -> dict: - - sequence = int( - getattr( - context, - "runtime_idle_action_sequence", - 0, - ) - or 0 - ) + 1 - context.runtime_idle_action_sequence = sequence - - scheduled_at = time.time() - sequence_turn_id = str( - getattr( - context, - "runtime_current_sequence_turn_id", - "", - ) - or getattr( - context, - "runtime_current_turn_id", - "", - ) - or "" - ).strip() - sequence_started_at = getattr( - context, - "runtime_current_sequence_started_at", - None, - ) - if not isinstance( - sequence_started_at, - (int, float), - ) or sequence_started_at <= 0: - sequence_started_at = getattr( - context, - "runtime_turn_started_at", - scheduled_at, - ) - if not isinstance( - sequence_started_at, - (int, float), - ) or sequence_started_at <= 0: - sequence_started_at = scheduled_at - current_attachments = getattr( - context, - "runtime_turn_attachments", - [], - ) - sequence_attachment_turn_id = str( - getattr( - context, - "runtime_current_sequence_attachments_turn_id", - "", - ) - or "" - ).strip() - sequence_attachments = getattr( - context, - "runtime_current_sequence_attachments", - [], - ) - if ( - not current_attachments - and sequence_attachment_turn_id == sequence_turn_id - ): - current_attachments = sequence_attachments - - record = { - "id": build_runtime_action_id( - RUNTIME_ACTION_IDLE, - sequence, - ), - "action": "idle", - "seconds": seconds, - "scheduled_at": scheduled_at, - "due_at": scheduled_at + seconds, - "source_message": str(source_message or ""), - "origin_user_request": str(user_message or ""), - "sequence_turn_id": sequence_turn_id, - "sequence_started_at": float(sequence_started_at), - "context_snapshot": deepcopy(context_snapshot) - if isinstance(context_snapshot, dict) - else {}, - "tool_results_generation": int( - getattr( - context, - "runtime_tool_results_generation", - 0, - ) - or 0 - ), - "attachments": deepcopy( - current_attachments - or [] - ), - } - - task = asyncio.create_task( - _enqueue_idle_followup_after_delay( - context, - record, - ) - ) - _track_background_task( - context, - task, - ) - - return record - - async def apply_runtime_action_calls( context, actions, diff --git a/utils/chat_log.py b/utils/chat_log.py index cde7168a..cbab7324 100644 --- a/utils/chat_log.py +++ b/utils/chat_log.py @@ -1216,6 +1216,225 @@ def build_chat_log_entry( } +def _append_chat_log_json_entry( + path: Path, + entry: dict, +) -> None: + + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + needs_separator = False + try: + if path.is_file() and path.stat().st_size > 0: + with path.open("rb") as existing_log: + existing_log.seek(-1, 2) + needs_separator = existing_log.read(1) not in {b"\n", b"\r"} + except OSError: + needs_separator = False + + with path.open( + "a", + encoding="utf-8", + newline="\n", + ) as log_file: + if needs_separator: + log_file.write("\n") + + log_file.write( + json.dumps( + entry, + ensure_ascii=False, + separators=( + ",", + ":", + ), + ) + + "\n" + ) + + +def append_chat_runtime_event( + context, + *, + event: str, + payload: dict | None = None, + now: datetime | None = None, + root: Path | str | None = None, +) -> Path | None: + + if not chat_logging_enabled(): + return None + + normalized_event = str( + event + or "" + ).strip() + if not normalized_event: + return None + + timestamp = now or _now() + path = get_chat_log_path( + context, + now=timestamp, + root=root, + ) + entry = { + "ts": timestamp.isoformat(timespec="seconds"), + "turn": int( + getattr(context, "runtime_turn_counter", 0) + or 0 + ), + "turn_id": str( + getattr(context, "runtime_current_turn_id", "") + or "" + ), + "session_id": str( + getattr(context, "session_id", "") + or getattr(context, "runtime_chat_log_session_id", "") + or "" + ), + "role": "runtime", + "text": "", + "event": normalized_event, + "payload": dict(payload or {}), + "dialog_path": _public_project_path(path), + } + + _append_chat_log_json_entry( + path, + entry, + ) + return path + + +def replace_latest_chat_log_entry( + context, + *, + role: str, + text: str, + now: datetime | None = None, + root: Path | str | None = None, +) -> Path | None: + """Replace the latest matching visible dialogue entry in-place. + + Retry uses this instead of appending a second JIN answer, so bootstrap and + archived chat history keep the same user/JIN pair rather than exposing a + hidden retry turn. Runtime event rows are left untouched. + """ + + if not chat_logging_enabled(): + return None + + timestamp = now or _now() + path = get_chat_log_path( + context, + now=timestamp, + root=root, + ) + + if not path.is_file(): + return append_chat_log_entry( + context, + role=role, + text=text, + now=timestamp, + root=root, + ) + + try: + raw_lines = path.read_text( + encoding="utf-8", + errors="replace", + ).splitlines() + except OSError: + return append_chat_log_entry( + context, + role=role, + text=text, + now=timestamp, + root=root, + ) + + normalized_role = str(role or "").casefold() + replacement_index = None + previous_entry = None + + for index in range(len(raw_lines) - 1, -1, -1): + try: + candidate = json.loads(raw_lines[index]) + except (TypeError, ValueError): + continue + + if ( + isinstance(candidate, dict) + and str(candidate.get("role") or "").casefold() == normalized_role + ): + replacement_index = index + previous_entry = candidate + break + + if replacement_index is None: + return append_chat_log_entry( + context, + role=role, + text=text, + now=timestamp, + root=root, + ) + + entry = build_chat_log_entry( + context, + role=role, + text=text, + now=timestamp, + ) + entry["dialog_path"] = _public_project_path(path) + + # Preserve the visible turn identity. The retry is an internal generation, + # not an extra dialogue turn. + for field_name in ("turn", "turn_id", "session_id"): + if field_name in previous_entry: + entry[field_name] = previous_entry[field_name] + + context_path = get_chat_context_path( + context, + now=timestamp, + root=root, + ) + if context_path.exists(): + entry["context_path"] = _public_project_path(context_path) + + reasoning_path = str( + getattr( + context, + "runtime_turn_reasoning_log_path", + "", + ) + or "" + ).strip() + if ( + normalized_role == "jin" + and reasoning_path + and Path(reasoning_path).exists() + ): + entry["reasoning_path"] = _public_project_path(Path(reasoning_path)) + + raw_lines[replacement_index] = json.dumps( + entry, + ensure_ascii=False, + separators=(",", ":"), + ) + path.write_text( + "\n".join(raw_lines) + "\n", + encoding="utf-8", + newline="\n", + ) + return path + + def append_chat_log_entry( context, *, @@ -1272,38 +1491,8 @@ def append_chat_log_entry( Path(reasoning_path) ) - path.parent.mkdir( - parents=True, - exist_ok=True, + _append_chat_log_json_entry( + path, + entry, ) - - needs_separator = False - try: - if path.is_file() and path.stat().st_size > 0: - with path.open("rb") as existing_log: - existing_log.seek(-1, 2) - needs_separator = existing_log.read(1) not in {b"\n", b"\r"} - except OSError: - needs_separator = False - - with path.open( - "a", - encoding="utf-8", - newline="\n", - ) as log_file: - if needs_separator: - log_file.write("\n") - - log_file.write( - json.dumps( - entry, - ensure_ascii=False, - separators=( - ",", - ":", - ), - ) - + "\n" - ) - return path diff --git a/utils/context/tool_results.py b/utils/context/tool_results.py index 4e040706..ca00c4b3 100644 --- a/utils/context/tool_results.py +++ b/utils/context/tool_results.py @@ -1,9 +1,11 @@ # Builds the full tool results context from search, asset, memory, and session results. +import json import time from xml.sax.saxutils import escape from contracts.rules_assembler import ( RUNTIME_ACTION_DEEP_WEB_SEARCH, + RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_WEB_SEARCH, ) from utils.brain_client_utils import ( @@ -17,6 +19,7 @@ TOOL_RESULT_KIND_DEEP_SEARCH, TOOL_RESULT_KIND_SEARCH, TOOL_RESULT_KIND_FILES, + TOOL_RESULT_KIND_L4, get_runtime_tool_result_created_at, get_runtime_tool_results, ) @@ -331,6 +334,38 @@ def _append_recorded_tool_results( appended = True continue + if kind == TOOL_RESULT_KIND_L4: + if not isinstance(result, dict): + continue + + payload = json.dumps( + result, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + if not payload: + continue + + attrs = f'name="{escape(RUNTIME_ACTION_UPDATE_L4_FACTS)}"' + result_id = str( + entry.get( + "id", + "", + ) + or "" + ).strip() + if result_id: + attrs += f' id="{escape(result_id)}"' + + parts.append( + f"{_build_tool_result_open_tag(attrs, created_at=created_at, now=now)}\n" + f"{indent_xml(escape(payload))}\n" + " " + ) + appended = True + continue + if kind == TOOL_RESULT_KIND_DELAYED_MEMORY: sections = format_delayed_memory_result_sections( [result] diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index a125d06a..ee481f8c 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1289,7 +1289,6 @@ def _build_session_action_marker_detail( if normalized_name in { "SAVE_ACTIVE_MEMORY", "RESOLVE_ACTIVE_MEMORY", - "IDLE", "LOAD_DELAYED_MEMORY", "UNLOAD_DELAYED_MEMORY", "ATTACH_FILE", diff --git a/utils/session_restore.py b/utils/session_restore.py index e187bc28..23d0e21c 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -16,7 +16,7 @@ BLOCK_RE_TEMPLATE = r"<{name}(?:\s+[^>]*)?>\s*(?P[\s\S]*?)\s*" TOOL_RESULT_RE = re.compile( - r'[^"]+)"[^>]*>\s*(?P[\s\S]*?)\s*', + r'[^"]+)"(?P[^>]*)>\s*(?P[\s\S]*?)\s*', re.IGNORECASE, ) TRUSTED_VALUE_RE = re.compile( @@ -31,6 +31,12 @@ r"(?\d+)(?![a-zA-Z0-9_])", re.IGNORECASE, ) +UPDATE_L4_FACTS_BLOCK_RE = re.compile( + r"^[ \t]*]*)?>[ \t]*\r?\n" + r"(?P[\s\S]*?)" + r"^[ \t]*
      [ \t]*[\"'`]*$", + re.IGNORECASE | re.MULTILINE, +) ACTION_LABELS = { @@ -200,26 +206,93 @@ def _append_restored_dialog_entry(lines: list[str], entry: dict) -> None: lines.append(f"<{tag}{timestamp_attr}>{escape(text)}") +def _append_restored_reasoning_entry( + lines: list[str], + jin_entry: dict, + reasoning_by_turn_id: dict[str, str], +) -> None: + turn_id = str(jin_entry.get("turn_id", "") or "").strip() + reasoning = _crop_restore_reasoning( + reasoning_by_turn_id.get(turn_id, "") + ) + if not reasoning: + return + + timestamp = str(jin_entry.get("ts", "") or "").strip() + attrs = [] + if turn_id: + attrs.append(f'turn_id="{escape(turn_id)}"') + if timestamp: + attrs.append(f'ts="{escape(timestamp)}"') + attr_text = (" " + " ".join(attrs)) if attrs else "" + lines.append( + f"\n{reasoning}\n" + ) + + def _build_restored_dialog_context( entries: list[dict], session_id: str, + reasoning_by_turn_id: dict[str, str], ) -> str: lines = [ f'', - "This is the exact visible dialogue restored from the archived session. The three newest complete USER/JIN pairs are shown in chronological order, ending at the latest archived JIN response; continue from that interaction state and do not summarize or re-introduce it unless the user asks.", + "This is the exact visible dialogue restored from the archived session. The newest complete USER/JIN pairs are shown in chronological order. For the latest pair, archived JIN reasoning is placed between the USER message and the visible JIN answer when available; continue from that interaction state and do not summarize or re-introduce it unless the user asks.", ] - for user_entry, jin_entry in _recent_restored_dialog_pairs(entries): + pairs = _recent_restored_dialog_pairs(entries) + latest_user_entry = next( + ( + entry + for entry in reversed(entries) + if str(entry.get("role", "")).strip().lower() == "user" + and str(entry.get("text", "") or "").strip() + ), + None, + ) + latest_user_is_unpaired = bool( + latest_user_entry is not None + and (not pairs or pairs[-1][0] is not latest_user_entry) + ) + if latest_user_is_unpaired: + pairs = pairs[-max(RECENT_MESSAGES_MAX_PAIRS - 1, 0):] + + for index, (user_entry, jin_entry) in enumerate(pairs): _append_restored_dialog_entry(lines, user_entry) + if index == len(pairs) - 1 and not latest_user_is_unpaired: + _append_restored_reasoning_entry( + lines, + jin_entry, + reasoning_by_turn_id, + ) _append_restored_dialog_entry(lines, jin_entry) + if latest_user_is_unpaired: + _append_restored_dialog_entry(lines, latest_user_entry) + lines.append("") return "\n".join(lines) -def _build_recent_turns(entries: list[dict]) -> list[dict]: +def _extract_reasoning_body(text: str) -> str: + source = str(text or "").strip() + if not source: + return "" + + marker = "--- REASONING ---" + if marker not in source: + return source + + return source.split(marker, 1)[1].strip() + + +def _build_recent_turns( + entries: list[dict], + reasoning_by_turn_id: dict[str, str] | None = None, +) -> list[dict]: turns: dict[int, dict] = {} ordered_turns = [] + reasoning_by_turn_id = reasoning_by_turn_id or {} for entry in entries: try: @@ -249,15 +322,24 @@ def _build_recent_turns(entries: list[dict]) -> list[dict]: turn["user_created_at"] = timestamp elif role in {"jin", "assistant", "brain", "service"}: turn["jin"] = text + reasoning = _extract_reasoning_body( + reasoning_by_turn_id.get( + str(entry.get("turn_id", "") or "").strip(), + "", + ) + ) + if reasoning: + turn["reasoning"] = reasoning if timestamp: turn["jin_created_at"] = timestamp ordered_turns.sort(key=lambda item: item[0]) - return [ + complete_turns = [ item - for _, item in ordered_turns[-RECENT_MESSAGES_MAX_PAIRS:] - if item.get("user") or item.get("jin") + for _, item in ordered_turns + if item.get("user") and item.get("jin") ] + return complete_turns[-RECENT_MESSAGES_MAX_PAIRS:] def _read_reasoning(session_directory: Path, entries: list[dict]) -> dict[str, str]: @@ -554,6 +636,8 @@ def _tool_result_kind(name: str) -> str: "ATTACH_FILE", }: return "files" + if action_name == "UPDATE_L4_FACTS": + return "l4" return "" @@ -601,11 +685,30 @@ def _parse_restore_tool_results( if not body: continue - items.append({ + result = body + if kind == "l4": + try: + parsed_result = json.loads(body) + except (TypeError, ValueError, json.JSONDecodeError): + parsed_result = None + if isinstance(parsed_result, dict): + result = parsed_result + + item = { "kind": kind, - "result": body, + "result": result, "created_at": fallback_created_at + offset, - }) + } + attrs = str(match.group("attrs") or "") + id_match = re.search( + r'\bid="(?P[^"]+)"', + attrs, + re.IGNORECASE, + ) + if id_match is not None: + item["id"] = unescape(id_match.group("id").strip()) + + items.append(item) offset += 0.001 return items[-20:] @@ -679,6 +782,142 @@ def _build_session_actions(context_text: str, fallback_created_at: float) -> lis return items +def _runtime_event_created_at(entry: dict, payload: dict) -> float: + try: + created_at = float(payload.get("created_at", 0) or 0) + except (TypeError, ValueError): + created_at = 0.0 + return created_at if created_at > 0 else _entry_timestamp(entry) + + +def _build_runtime_event_session_actions(entries: list[dict]) -> list[dict]: + items = [] + + for entry in entries: + if str(entry.get("event", "") or "").strip() != "runtime_action_request": + continue + payload = entry.get("payload") + if not isinstance(payload, dict): + continue + if str(payload.get("action", "") or "").strip().upper() != "UPDATE_L4_FACTS": + continue + + raw_action = payload.get("session_action") + raw_action = raw_action if isinstance(raw_action, dict) else {} + message = " ".join(str(payload.get("message", "") or "").split()).strip() + text = str(raw_action.get("text", "") or "").strip() + if not text: + text = "UPDATE_L4_FACTS" + (f": {message}" if message else "") + + part = {"text": "UPDATE_L4_FACTS"} + if message: + part["message"] = message + action_id = str(payload.get("id", "") or "").strip() + if action_id: + part["id"] = action_id + + item = { + "text": text, + "created_at": _runtime_event_created_at(entry, payload), + "parts": [part], + } + items.append(item) + + return items[-200:] + + +def _build_runtime_event_tool_results(entries: list[dict]) -> list[dict]: + items = [] + + for entry in entries: + if str(entry.get("event", "") or "").strip() != "runtime_tool_result": + continue + payload = entry.get("payload") + if not isinstance(payload, dict): + continue + kind = str(payload.get("kind", "") or "").strip().casefold() + if kind != "l4": + continue + result = payload.get("result") + if not isinstance(result, dict): + continue + + item = { + "kind": "l4", + "result": result, + "created_at": _runtime_event_created_at(entry, payload), + } + result_id = str(payload.get("id", "") or "").strip() + if result_id: + item["id"] = result_id + items.append(item) + + return items[-20:] + + +def _build_reasoning_l4_fallback_action( + entries: list[dict], + reasoning_by_turn_id: dict[str, str], +) -> dict | None: + for entry in reversed(entries): + role = str(entry.get("role", "") or "").strip().casefold() + if role not in {"jin", "assistant", "brain", "service"}: + continue + turn_id = str(entry.get("turn_id", "") or "").strip() + reasoning = str(reasoning_by_turn_id.get(turn_id, "") or "") + matches = list(UPDATE_L4_FACTS_BLOCK_RE.finditer(reasoning)) + if not matches: + continue + + body = unescape(matches[-1].group("body")).strip() + if not body: + continue + message = body + try: + parsed = json.loads(body) + except (TypeError, ValueError, json.JSONDecodeError): + parsed = None + if isinstance(parsed, dict): + message = str(parsed.get("message", "") or "").strip() or body + message = " ".join(message.split()).strip() + if not message: + continue + + return { + "text": f"UPDATE_L4_FACTS: {message}", + "created_at": _entry_timestamp(entry), + "parts": [{ + "text": "UPDATE_L4_FACTS", + "message": message, + }], + } + + return None + + +def _merge_restore_tool_results( + context_results: list[dict], + runtime_results: list[dict], +) -> list[dict]: + merged = [] + keyed_indexes = {} + + for item in [*context_results, *runtime_results]: + if not isinstance(item, dict): + continue + kind = str(item.get("kind", "") or "").strip().casefold() + result_id = str(item.get("id", "") or "").strip() + key = (kind, result_id) if kind and result_id else None + if key is not None and key in keyed_indexes: + merged[keyed_indexes[key]] = item + continue + if key is not None: + keyed_indexes[key] = len(merged) + merged.append(item) + + return merged[-20:] + + def _parse_trusted_values(context_text: str) -> dict: values = {} source = str(context_text or "") @@ -920,11 +1159,56 @@ def build_archived_session_restore_payload( fallback_created_at = _entry_timestamp(visible_entries[0]) or dialog_path.stat().st_mtime session_actions = _build_session_actions(context_text, fallback_created_at) - tool_results = _parse_restore_tool_results( - context_text, - fallback_created_at, + runtime_session_actions = _build_runtime_event_session_actions(entries) + if runtime_session_actions: + session_actions = [ + item + for item in session_actions + if not ( + isinstance(item, dict) + and any( + isinstance(part, dict) + and str(part.get("text", "") or "").strip() + == ACTION_LABELS["UPDATE_L4_FACTS"] + for part in item.get("parts", []) or [] + ) + ) + ] + session_actions.extend(runtime_session_actions) + elif not any( + isinstance(item, dict) + and any( + isinstance(part, dict) + and str(part.get("text", "") or "").strip() + in {"UPDATE_L4_FACTS", ACTION_LABELS["UPDATE_L4_FACTS"]} + for part in item.get("parts", []) or [] + ) + for item in session_actions + ): + fallback_l4_action = _build_reasoning_l4_fallback_action( + visible_entries, + reasoning_by_turn_id, + ) + if fallback_l4_action is not None: + session_actions.append(fallback_l4_action) + session_actions = session_actions[-200:] + + tool_results = _merge_restore_tool_results( + _parse_restore_tool_results( + context_text, + fallback_created_at, + ), + _build_runtime_event_tool_results(entries), ) + archive_tail_at = "" + archive_tail_timestamp = 0.0 + for entry in entries: + entry_timestamp = _entry_timestamp(entry) + if entry_timestamp >= archive_tail_timestamp: + archive_tail_timestamp = entry_timestamp + archive_tail_at = str(entry.get("ts", "") or "").strip() + ui_messages = [] for entry in visible_entries: role = str(entry.get("role", "")).strip().lower() @@ -954,11 +1238,16 @@ def build_archived_session_restore_payload( "dialog_file": dialog_path.name, "context_file": context_path.name if context_path.is_file() else "", "messages": ui_messages, + "archive_tail_at": archive_tail_at, "dialog_context": _build_restored_dialog_context( visible_entries, _clean_session_id(session_id), + reasoning_by_turn_id, + ), + "recent_turns": _build_recent_turns( + visible_entries, + reasoning_by_turn_id, ), - "recent_turns": _build_recent_turns(visible_entries), "previous_reasoning": latest_reasoning, "restore_reasoning_dump": restore_reasoning_dump, "restore_l4_fact_ids": restore_l4_fact_ids, diff --git a/utils/tool_results.py b/utils/tool_results.py index 9b79d9d1..eb9585b5 100644 --- a/utils/tool_results.py +++ b/utils/tool_results.py @@ -9,6 +9,7 @@ TOOL_RESULT_KIND_ACTIVE_MEMORY = "active_memory" TOOL_RESULT_KIND_DELAYED_MEMORY = "delayed_memory" TOOL_RESULT_KIND_FILES = "files" +TOOL_RESULT_KIND_L4 = "l4" RUNTIME_TOOL_RESULT_LIST_ATTRIBUTES = ( "runtime_asset_results", diff --git a/utils/tool_results_context.py b/utils/tool_results_context.py index 73af89d9..05a6698a 100644 --- a/utils/tool_results_context.py +++ b/utils/tool_results_context.py @@ -13,10 +13,6 @@ r"]*)?>.*?", re.IGNORECASE | re.DOTALL, ) -IDLE_TOOL_RESULTS_RE = re.compile( - r"]*\btype\s*=\s*['\"]idle['\"][^>]*>", - re.IGNORECASE, -) TOOLS_RESULTS_CONTEXT_RE = re.compile( r"]*)?>.*?
      " r"|]*)?>.*?", @@ -116,13 +112,3 @@ def strip_tools_results_context( text ) return remainder - - -def is_idle_tool_results_block( - block: str, -) -> bool: - return bool( - IDLE_TOOL_RESULTS_RE.search( - str(block or "") - ) - ) diff --git a/websocket/__init__.py b/websocket/__init__.py index 0009d167..5733a5c4 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -10,7 +10,11 @@ from .logger import WebSocketLogger -from runtime.L1_memory import apply_runtime_response_feedback +from runtime.L1_memory import ( + apply_runtime_response_feedback, + discard_latest_runtime_memory_pending_turn, + resume_runtime_memory_pending_update, +) from runtime.L1_memory_utils import ( emit_runtime_l1_diff_update, ) @@ -22,8 +26,12 @@ emit_facts_memory_store_update, restore_l4_memory_fact, emit_l4_memory_update, + note_l4_foreground_state, + note_l4_user_activity, + register_l4_websocket_connection, runtime_l4_memory_update_running, schedule_l4_memory_idle_update, + unregister_l4_websocket_connection, ) from runtime.fact_check import run_fact_check_once from runtime.anonymous_mode import ( @@ -46,6 +54,7 @@ apply_runtime_memory_slot_delete, apply_runtime_resume, apply_session_bootstrap, + build_session_bootstrap_chat_tail, emit_current_runtime_memory, emit_delayed_memory_store_snapshot, ensure_initial_runtime_snapshot, @@ -55,8 +64,8 @@ ) from .messages import ( build_runtime_action_guard_retry_request, + build_user_retry_request, emit_runtime_action_guard_confirmation_failure, - merge_runtime_idle_followup_turn, process_message, receive_message, refresh_pending_brain_usage, @@ -65,7 +74,6 @@ wait_for_runtime_memory_update, ) from .tasks import ( - PendingRequestQueue, cancel_current_task, ) @@ -123,19 +131,9 @@ async def websocket_endpoint( ) current_task = None - pending_requests = PendingRequestQueue() + pending_requests = asyncio.Queue() context.runtime_pending_requests_queue = pending_requests - pending_idle_followups = list( - getattr( - context, - "runtime_pending_idle_followups", - [], - ) - or [] - ) - context.runtime_pending_idle_followups = [] - async def process_pending_requests(): nonlocal current_task @@ -145,50 +143,46 @@ async def process_pending_requests(): try: - is_idle_followup = ( - message_data.get("type") == "idle_followup" - and isinstance( - message_data.get("idle_followup"), - dict, - ) - ) user_text = ( str( - ( - message_data.get("idle_followup", {}).get( - "origin_user_request", - "", - ) - if is_idle_followup - else message_data.get( - "text", - "", - ) + message_data.get( + "text", + "", ) ).strip() ) - await wait_for_runtime_memory_update( - context - ) - if not is_idle_followup: - await apply_runtime_response_feedback( - context, - ( - message_data.get( - "pending_last_response_rating", - ) - or message_data.get( - "runtime_response_feedback", - ) - ), + if message_data.get("type") == "retry_last_response": + await discard_latest_runtime_memory_pending_turn( + context ) - - await refresh_pending_brain_usage( - context, - user_text, + # If an older batch remains after removing the discarded + # answer, let it settle before building the replacement. + await wait_for_runtime_memory_update( + context + ) + else: + await wait_for_runtime_memory_update( + context ) + await apply_runtime_response_feedback( + context, + ( + message_data.get( + "pending_last_response_rating", + ) + or message_data.get( + "runtime_response_feedback", + ) + ), + ) + + await refresh_pending_brain_usage( + context, + user_text, + ) + active_task = asyncio.create_task( process_message( context, @@ -196,6 +190,10 @@ async def process_pending_requests(): ) ) current_task = active_task + note_l4_foreground_state( + context, + running=True, + ) try: await active_task @@ -211,6 +209,10 @@ async def process_pending_requests(): finally: if current_task is active_task: current_task = None + note_l4_foreground_state( + context, + running=False, + ) finally: pending_requests.task_done() @@ -219,6 +221,12 @@ async def process_pending_requests(): process_pending_requests() ) + register_l4_websocket_connection( + context, + app_state=websocket.app.state, + websocket=websocket, + ) + try: await initialize_connection( @@ -226,12 +234,6 @@ async def process_pending_requests(): skip_initial_runtime_state=skip_initial_runtime_state, ) - for idle_followup in pending_idle_followups: - await pending_requests.put({ - "type": "idle_followup", - "idle_followup": idle_followup, - }) - while True: message_data = ( @@ -264,6 +266,17 @@ async def process_pending_requests(): message_data, ) + resumed_memory_task = ( + resume_runtime_memory_pending_update( + context + ) + ) + + if resumed_memory_task is not None: + await logger.log_runtime( + "[MEMORY:L1] pending update resumed after reconnect" + ) + if restored: await logger.log_system( "[WS] runtime resumed from browser memory" @@ -604,6 +617,23 @@ async def process_pending_requests(): current_sequence=False, ) + chat_tail = build_session_bootstrap_chat_tail( + context + ) + if chat_tail: + await context.emitter.emit({ + "type": "session_bootstrap_chat_tail", + "source_session_id": str( + getattr( + context, + "previous_session_id", + "", + ) + or "" + ).strip(), + "turns": chat_tail, + }) + continue if message_type == "archived_session_resume": @@ -666,6 +696,57 @@ async def process_pending_requests(): ) continue + if message_type == "retry_last_response": + if ( + current_task is not None + and not current_task.done() + ): + await websocket.send_json({ + "type": "retry_last_response_rejected", + "reason": "generation_running", + }) + await logger.log_runtime( + "[USER RETRY] rejected: generation is running" + ) + continue + + retry_request = build_user_retry_request( + context, + message_data, + ) + if retry_request is None: + await websocket.send_json({ + "type": "retry_last_response_rejected", + "reason": "no_retryable_response", + }) + await logger.log_runtime( + "[USER RETRY] rejected: no live retry source" + ) + continue + + note_l4_user_activity(context) + await cancel_l4_memory_idle_update( + context, + reason="user_retry", + ) + + if await reject_when_all_models_offline( + context + ): + await websocket.send_json({ + "type": "retry_last_response_rejected", + "reason": "models_offline", + }) + continue + + await pending_requests.put( + retry_request + ) + await logger.log_runtime( + "[USER RETRY] queued replacement for latest JIN answer" + ) + continue + await logger.log_user( str( message_data.get( @@ -769,6 +850,7 @@ async def process_pending_requests(): # Cancelling here aborts the in-flight background model request # before this user turn enters the generation queue; pending L4 # facts remain in their stores for the next true idle window. + note_l4_user_activity(context) await cancel_l4_memory_idle_update( context, reason="user_message", @@ -850,6 +932,12 @@ async def process_pending_requests(): ) finally: + unregister_l4_websocket_connection( + context, + app_state=websocket.app.state, + websocket=websocket, + ) + if getattr( context, "runtime_pending_requests_queue", @@ -865,54 +953,11 @@ async def process_pending_requests(): ): await pending_processor - pending_idle_records = getattr( - context, - "runtime_pending_idle_followups", - None, - ) - if not isinstance(pending_idle_records, list): - pending_idle_records = [] - context.runtime_pending_idle_followups = ( - pending_idle_records - ) - - pending_idle_ids = { - str(record.get("id", "") or "") - for record in pending_idle_records - if isinstance(record, dict) - } - while True: try: - queued_message = pending_requests.get_nowait() + pending_requests.get_nowait() except asyncio.QueueEmpty: break - - try: - idle_record = queued_message.get( - "idle_followup", - ) - if not isinstance(idle_record, dict): - continue - - idle_id = str( - idle_record.get( - "id", - "", - ) - or "" - ) - if idle_id and idle_id in pending_idle_ids: - continue - - pending_idle_records.append( - idle_record - ) - if idle_id: - pending_idle_ids.add( - idle_id - ) - finally: - pending_requests.task_done() + pending_requests.task_done() diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 160c364d..1935bd6f 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -15,6 +15,9 @@ build_runtime_memory_snapshot, parse_runtime_memory_lines, ) +from runtime.L1_memory_pending import ( + restore_pending_l1_update, +) from runtime.L1_memory_utils import ( build_runtime_memory_context_text, canonicalize_runtime_memory_key, @@ -73,6 +76,7 @@ "deep_search", "delayed_memory", "files", + "l4", "search", } @@ -322,6 +326,13 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: "jin": jin_text, } + reasoning = clean_bootstrap_memory( + turn.get("reasoning", ""), + limit=32000, + ) + if reasoning: + normalized_turn["reasoning"] = reasoning + for source_key, target_key in ( ("user_created_at", "user_created_at"), ("jin_created_at", "jin_created_at"), @@ -375,6 +386,19 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: ) context.runtime_previous_reasoning_loop_contents = [] + recent_turns = getattr( + context, + "runtime_recent_turns", + [], + ) + if ( + isinstance(recent_turns, list) + and recent_turns + and isinstance(recent_turns[-1], dict) + and not str(recent_turns[-1].get("reasoning", "") or "").strip() + ): + recent_turns[-1]["reasoning"] = previous_reasoning + session_actions = message_data.get( "session_actions", [], @@ -1521,6 +1545,9 @@ def get_or_create_connection_context( context, anonymous_mode_enabled, ) + restore_pending_l1_update( + context + ) store[client_id] = context @@ -2280,6 +2307,18 @@ def apply_runtime_resume( return True +def _bootstrap_iso_timestamp(value) -> float: + text = str(value or "").strip() + if not text: + return 0.0 + try: + return datetime.fromisoformat( + text.replace("Z", "+00:00") + ).timestamp() + except (TypeError, ValueError): + return 0.0 + + def enrich_session_bootstrap_from_archive( message_data: dict, ) -> dict: @@ -2325,26 +2364,31 @@ def enrich_session_bootstrap_from_archive( limit=80, ) archive_messages = archived.get("messages", []) + archive_tail_at = clean_bootstrap_memory( + archived.get("archive_tail_at", ""), + limit=80, + ) + if ( + not archive_tail_at + and isinstance(archive_messages, list) + and archive_messages + and isinstance(archive_messages[-1], dict) + ): + archive_tail_at = clean_bootstrap_memory( + archive_messages[-1].get("ts", ""), + limit=80, + ) + + if saved_at and archive_tail_at: + checkpoint_at = _bootstrap_iso_timestamp(saved_at) + archive_tail_timestamp = _bootstrap_iso_timestamp(archive_tail_at) + if checkpoint_at and archive_tail_timestamp: + # Keep the pre-existing whole-second tolerance: browser checkpoints + # often carry sub-second precision while JSONL rows are second-only. + archive_reaches_saved_checkpoint = ( + int(archive_tail_timestamp) >= int(checkpoint_at) + ) - if saved_at and isinstance(archive_messages, list) and archive_messages: - archive_tail_at = str( - archive_messages[-1].get("ts", "") - if isinstance(archive_messages[-1], dict) - else "" - ).strip() - if archive_tail_at: - try: - checkpoint_at = datetime.fromisoformat( - saved_at.replace("Z", "+00:00") - ).replace(microsecond=0) - archive_tail_at = datetime.fromisoformat( - archive_tail_at.replace("Z", "+00:00") - ).replace(microsecond=0) - archive_reaches_saved_checkpoint = ( - archive_tail_at >= checkpoint_at - ) - except (TypeError, ValueError): - pass # Never mix a newer browser save with an older raw log tail. if archive_reaches_saved_checkpoint: @@ -2402,6 +2446,59 @@ def enrich_session_bootstrap_from_archive( if value not in (None, "", [], {}): enriched[field] = value + # The browser checkpoint is authoritative for ordinary tool results, but + # an UPDATE_L4_FACTS response can finish after that checkpoint was written. + # Replay only those newer append-only L4 results. A CLEAN_TOOL_RESULTS + # timestamp remains a hard lower bound so cleared results never resurrect. + browser_tool_results = message_data.get("tool_results") + if isinstance(browser_tool_results, list): + saved_at_timestamp = _bootstrap_iso_timestamp(saved_at) + cleared_at_timestamp = _bootstrap_iso_timestamp( + message_data.get("tool_results_cleared_at", "") + ) + tool_result_cutoff = max( + saved_at_timestamp, + cleared_at_timestamp, + ) + archived_tool_results = archived.get("tool_results", []) + if tool_result_cutoff > 0 and isinstance(archived_tool_results, list): + late_l4_results = [] + for item in archived_tool_results: + if not isinstance(item, dict): + continue + if str(item.get("kind", "") or "").strip().casefold() != "l4": + continue + try: + created_at = float(item.get("created_at", 0) or 0) + except (TypeError, ValueError): + created_at = 0.0 + if created_at <= tool_result_cutoff: + continue + late_l4_results.append(item) + + if late_l4_results: + merged_tool_results = list(browser_tool_results) + keyed_indexes = {} + for index, item in enumerate(merged_tool_results): + if not isinstance(item, dict): + continue + kind = str(item.get("kind", "") or "").strip().casefold() + result_id = str(item.get("id", "") or "").strip() + if kind and result_id: + keyed_indexes[(kind, result_id)] = index + + for item in late_l4_results: + result_id = str(item.get("id", "") or "").strip() + key = ("l4", result_id) if result_id else None + if key is not None and key in keyed_indexes: + merged_tool_results[keyed_indexes[key]] = item + else: + if key is not None: + keyed_indexes[key] = len(merged_tool_results) + merged_tool_results.append(item) + + enriched["tool_results"] = merged_tool_results[-50:] + enriched["source_session_id"] = source_session_id if archived.get("source_session_date"): enriched["source_session_date"] = archived["source_session_date"] @@ -2412,6 +2509,74 @@ def enrich_session_bootstrap_from_archive( return enriched +def build_session_bootstrap_chat_tail( + context, +) -> list[dict]: + + turns = getattr( + context, + "runtime_recent_turns", + [], + ) + if not isinstance(turns, list): + return [] + + complete_turns = [] + for turn in turns: + if not isinstance(turn, dict): + continue + + user_text = clean_bootstrap_memory( + turn.get("user", ""), + limit=12000, + ) + attachment_context_marker = "\n\nAttached context:\n" + if attachment_context_marker in user_text: + user_text = user_text.split( + attachment_context_marker, + 1, + )[0].rstrip() + + jin_text = clean_bootstrap_memory( + turn.get("jin", ""), + limit=12000, + ) + if not user_text or not jin_text: + continue + + item = { + "user": user_text, + "jin": jin_text, + } + reasoning = clean_bootstrap_memory( + turn.get("reasoning", ""), + limit=32000, + ) + reasoning_marker = "--- REASONING ---" + if reasoning_marker in reasoning: + reasoning = reasoning.split( + reasoning_marker, + 1, + )[1].strip() + if reasoning: + item["reasoning"] = reasoning + + for key in ( + "user_created_at", + "jin_created_at", + ): + try: + created_at = float(turn.get(key, 0) or 0) + except (TypeError, ValueError): + created_at = 0.0 + if created_at > 0: + item[key] = created_at + + complete_turns.append(item) + + return complete_turns[-RECENT_MESSAGES_MAX_PAIRS:] + + def apply_session_bootstrap( context, message_data: dict, diff --git a/websocket/messages.py b/websocket/messages.py index 3d6a2c5c..0665e441 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -37,6 +37,7 @@ ) from utils.chat_log import ( append_chat_log_entry, + replace_latest_chat_log_entry, save_turn_reasoning, ) from utils.delayed_memory_triggers import ( @@ -798,11 +799,91 @@ def apply_runtime_avatar_context( context.runtime_avatar_move_speed = speed +def build_user_retry_request( + context, + message_data: dict | None = None, +) -> dict | None: + """Rebuild the latest real user request without creating a new user turn.""" + + source = getattr( + context, + "runtime_last_retryable_request", + {}, + ) + if not isinstance(source, dict) or not source: + return None + + recent_turns = getattr( + context, + "runtime_recent_turns", + [], + ) + if ( + not isinstance(recent_turns, list) + or not recent_turns + or not str((recent_turns[-1] or {}).get("jin") or "").strip() + ): + return None + + text = str(source.get("text") or "") + attachments = deepcopy(source.get("attachments") or []) + if not text.strip() and not attachments: + return None + + retry_request = { + "type": "retry_last_response", + "text": text, + } + if attachments: + retry_request["attachments"] = attachments + + live_request = message_data if isinstance(message_data, dict) else {} + # Runtime geometry and visible active-memory state are live UI state, not + # part of the discarded answer. Refresh only those fields on retry. + for field_name in ( + "runtime_avatar", + "active_memory_records", + ): + if field_name in live_request: + retry_request[field_name] = deepcopy(live_request[field_name]) + + return retry_request + + +def discard_latest_visible_turn_for_user_retry( + context, +) -> dict: + """Remove the answer being replaced from rolling prompt-side history.""" + + previous_turn = {} + recent_turns = getattr(context, "runtime_recent_turns", None) + if isinstance(recent_turns, list) and recent_turns: + candidate = recent_turns.pop() + if isinstance(candidate, dict): + previous_turn = candidate + + metabolism_turns = getattr( + context, + "runtime_metabolism_recent_turns", + None, + ) + if isinstance(metabolism_turns, list) and metabolism_turns: + metabolism_turns.pop() + + # The previous reasoning belongs to the discarded answer and must not be + # re-injected beside the explicit retry marker. + context.runtime_previous_reasoning_content = "" + context.runtime_previous_reasoning_loop_contents = [] + + return previous_turn + + def append_runtime_recent_turn( context, *, user_message: str, assistant_message: str, + reasoning: str = "", user_created_at: float | None = None, assistant_created_at: float | None = None, ) -> None: @@ -824,6 +905,10 @@ def append_runtime_recent_turn( assistant_message or "" ).strip() + reasoning = str( + reasoning + or "" + ).strip() if not user_message and not assistant_message: return @@ -833,6 +918,9 @@ def append_runtime_recent_turn( "jin": assistant_message, } + if reasoning: + turn["reasoning"] = reasoning + if isinstance( user_created_at, (int, float), @@ -869,107 +957,6 @@ def append_runtime_recent_turn( ] -def merge_runtime_idle_followup_turn( - context, - *, - origin_user_request: str, - assistant_message: str, - assistant_created_at: float | None = None, - idle_followup_id: str = "", -) -> None: - - if context is None: - return - - origin_user_request = str( - origin_user_request - or "" - ).strip() - assistant_message = str( - assistant_message - or "" - ).strip() - - if not assistant_message: - return - - recent_turns = getattr( - context, - "runtime_recent_turns", - None, - ) - if not isinstance( - recent_turns, - list, - ): - recent_turns = [] - context.runtime_recent_turns = recent_turns - - target_turn = None - for turn in reversed( - recent_turns - ): - if not isinstance( - turn, - dict, - ): - continue - - turn_user = str( - turn.get( - "user", - "", - ) - or "" - ).strip() - turn_origin = str( - turn.get( - "idle_origin_user_request", - "", - ) - or "" - ).strip() - - if origin_user_request and ( - turn_user == origin_user_request - or turn_origin == origin_user_request - ): - target_turn = turn - break - - if target_turn is None: - target_turn = { - "user": origin_user_request, - "jin": "", - "idle_origin_user_request": origin_user_request, - } - recent_turns.append( - target_turn - ) - - target_turn["jin"] = assistant_message - target_turn["idle_origin_user_request"] = origin_user_request - - normalized_idle_followup_id = str( - idle_followup_id - or "" - ).strip() - if normalized_idle_followup_id: - target_turn["idle_followup_id"] = normalized_idle_followup_id - - if isinstance( - assistant_created_at, - (int, float), - ): - target_turn["jin_created_at"] = float( - assistant_created_at - ) - - context.runtime_recent_turns = recent_turns[ - -RECENT_MESSAGES_MAX_PAIRS: - ] - - def format_runtime_memory_user_message( context, user_text: str, @@ -984,9 +971,25 @@ def format_runtime_memory_user_message( ) if repeated < 2: - return user_text + formatted = user_text + else: + formatted = ( + f"{json.dumps(user_text, ensure_ascii=False)} " + f"[ repeated: {repeated} ]" + ) - return f"{json.dumps(user_text, ensure_ascii=False)} [ repeated: {repeated} ]" + if getattr( + context, + "runtime_user_retry_active", + False, + ): + return ( + f"{formatted} " + "[ user_retry: true; previous_jin_answer_discarded: true; " + "replace_previous_turn: true ]" + ).strip() + + return formatted async def process_message( @@ -997,17 +1000,13 @@ async def process_message( logger = context.logger action_guard_retry = {} is_action_guard_retry = False + is_user_retry = False + user_retry_replaced_turn = {} + retry_source_candidate = {} retry_terminal_emitted = False try: - idle_followup = message_data.get( - "idle_followup", - {}, - ) - if not isinstance(idle_followup, dict): - idle_followup = {} - is_idle_followup = bool(idle_followup) is_session_restore_resume = bool( message_data.get("type") == "archived_session_resume" and getattr( @@ -1016,6 +1015,43 @@ async def process_message( False, ) ) + is_user_retry = bool( + message_data.get("type") == "retry_last_response" + ) + context.runtime_user_retry_active = is_user_retry + if is_user_retry: + context.runtime_user_retry_count = int( + getattr(context, "runtime_user_retry_count", 0) + or 0 + ) + 1 + retry_source_candidate = deepcopy( + getattr( + context, + "runtime_last_retryable_request", + {}, + ) + or {} + ) + # Retry consumes the previous completed-answer capability. It is + # restored only if the replacement itself completes successfully. + context.runtime_last_retryable_request = {} + user_retry_replaced_turn = ( + discard_latest_visible_turn_for_user_retry( + context + ) + ) + elif message_data.get("type", "message") == "message": + context.runtime_user_retry_count = 0 + retry_source_candidate = { + "text": get_message_user_text(message_data), + "attachments": deepcopy( + message_data.get("attachments") or [] + ), + } + # The previous answer stops being retryable as soon as a new real + # user turn starts. The current request is promoted only after its + # JIN response completes successfully. + context.runtime_last_retryable_request = {} action_guard_retry = normalize_runtime_action_guard_retry( message_data.get( "runtime_action_guard_retry", @@ -1038,7 +1074,7 @@ async def process_message( ) ) - if not is_idle_followup and not is_action_guard_retry: + if not is_action_guard_retry: # A real foreground turn always wins over the previous background # metabolism estimate, especially when SERVICE is also the brain. cancel_metabolism_update( @@ -1055,7 +1091,7 @@ async def process_message( # Keep restored file IDs pinned for subsequent turns, but do not # feed any file payload/image bytes into the hidden restore tick. active_attachment_ids = [] - elif is_idle_followup or is_action_guard_retry: + elif is_action_guard_retry: active_attachment_ids = list( getattr( context, @@ -1080,18 +1116,8 @@ async def process_message( user_text = ( "" if is_session_restore_resume - else ( - str( - idle_followup.get( - "origin_user_request", - "", - ) - or "" - ) - if is_idle_followup - else build_user_text_with_attachments( - message_data, - ) + else build_user_text_with_attachments( + message_data, ) ) @@ -1106,52 +1132,21 @@ async def process_message( + 1 ) context.runtime_current_turn_id = ( - f"idle_{context.runtime_turn_counter:06d}" - if is_idle_followup + f"retry_{context.runtime_turn_counter:06d}" + if is_action_guard_retry else ( - f"retry_{context.runtime_turn_counter:06d}" - if is_action_guard_retry + f"user_retry_{context.runtime_turn_counter:06d}" + if is_user_retry else f"turn_{context.runtime_turn_counter:06d}" ) ) - - if is_idle_followup: - context.runtime_current_sequence_turn_id = str( - idle_followup.get( - "sequence_turn_id", - "", - ) - or context.runtime_current_turn_id - ).strip() - sequence_started_at = idle_followup.get( - "sequence_started_at" - ) - if not isinstance( - sequence_started_at, - (int, float), - ) or sequence_started_at <= 0: - sequence_started_at = context.runtime_turn_started_at - context.runtime_current_sequence_started_at = float( - sequence_started_at - ) - else: - context.runtime_current_sequence_turn_id = ( - context.runtime_current_turn_id - ) - context.runtime_current_sequence_started_at = ( - context.runtime_turn_started_at - ) - if is_idle_followup: - context.runtime_turn_attachments = deepcopy( - hydrated_active_attachments - ) - context.runtime_current_sequence_attachments = deepcopy( - hydrated_active_attachments - ) - context.runtime_current_sequence_attachments_turn_id = ( - context.runtime_current_sequence_turn_id - ) - elif is_action_guard_retry: + context.runtime_current_sequence_turn_id = ( + context.runtime_current_turn_id + ) + context.runtime_current_sequence_started_at = ( + context.runtime_turn_started_at + ) + if is_action_guard_retry: context.runtime_turn_attachments = deepcopy( hydrated_active_attachments ) @@ -1187,36 +1182,41 @@ async def process_message( context.runtime_context_limit_kind = "" context.runtime_context_limit_finish_reason = "" if ( - not is_idle_followup - and not is_action_guard_retry + not is_action_guard_retry and not is_session_restore_resume ): - apply_user_idle_context( - context, - message_data, - ) + if not is_user_retry: + apply_user_idle_context( + context, + message_data, + ) + apply_active_memory_records( context, message_data, ) - apply_runtime_pattern_context( - context, - message_data, - ) + + if not is_user_retry: + apply_runtime_pattern_context( + context, + message_data, + ) + apply_runtime_avatar_context( context, message_data, ) - # Homeostat first: the current user event must be able to affect - # this very generation, not only the next one. No bubble/event - # card is emitted; only the existing ambient metabolism signal. - await prepare_metabolism_for_turn( - context, - get_message_user_text( - message_data - ), - ) + if not is_user_retry: + # Homeostat first: the current user event must be able to affect + # this very generation, not only the next one. No bubble/event + # card is emitted; only the existing ambient metabolism signal. + await prepare_metabolism_for_turn( + context, + get_message_user_text( + message_data + ), + ) context.runtime_turn_memory_user_message = ( format_runtime_memory_user_message( @@ -1224,18 +1224,24 @@ async def process_message( user_text, ) ) - context.user_message_count += 1 - # Tag auto-load is driven only by the text the user typed. - # Attachment text still stays in ``user_text`` and reaches JIN as - # context, but it must never behave like a tag command. - await load_delayed_memory_by_tags( - context, - get_message_user_text( - message_data - ), - ) - if not is_action_guard_retry and not is_session_restore_resume: + if not is_user_retry: + context.user_message_count += 1 + # Tag auto-load is driven only by the text the user typed. + # Attachment text still stays in ``user_text`` and reaches JIN as + # context, but it must never behave like a tag command. + await load_delayed_memory_by_tags( + context, + get_message_user_text( + message_data + ), + ) + + if ( + not is_action_guard_retry + and not is_session_restore_resume + and not is_user_retry + ): try: append_chat_log_entry( context, @@ -1251,10 +1257,10 @@ async def process_message( state = AgentState( user_input=user_text ) - if is_idle_followup: - state.metadata["idle_followup"] = idle_followup if is_session_restore_resume: state.metadata["session_restore_resume"] = True + if is_user_retry: + state.metadata["user_retry"] = True if hasattr( context, @@ -1269,6 +1275,16 @@ async def process_message( await websocket.send_json({ "type": "agent_runtime_start", + # This is only candidate eligibility. The client waits for + # agent_runtime_end before making the bubble long-tap retryable. + "retryable_response": bool( + not is_action_guard_retry + and not is_session_restore_resume + and ( + is_user_retry + or message_data.get("type", "message") == "message" + ) + ), }) await runtime.run( @@ -1328,25 +1344,48 @@ async def process_message( ), ) + assistant_message = ( + state.brain_response + or context.runtime_turn_assistant_response + ) + retryable_response = bool( + not is_action_guard_retry + and not is_session_restore_resume + and not getattr( + context, + "runtime_turn_interrupted", + False, + ) + and str(assistant_message or "").strip() + ) + + if retryable_response: + context.runtime_last_retryable_request = deepcopy( + retry_source_candidate + ) + await logger.log_system( "[WS] agent runtime end" ) await websocket.send_json({ "type": "agent_runtime_end", + "retryable_response": retryable_response, }) - - assistant_message = ( - state.brain_response - or context.runtime_turn_assistant_response - ) if not is_action_guard_retry: try: - append_chat_log_entry( - context, - role="jin", - text=assistant_message, - ) + if is_user_retry: + replace_latest_chat_log_entry( + context, + role="jin", + text=assistant_message, + ) + else: + append_chat_log_entry( + context, + role="jin", + text=assistant_message, + ) except Exception as error: await logger.log_system( "[CHAT_LOG] local JIN message save failed: " @@ -1354,29 +1393,25 @@ async def process_message( ) assistant_created_at = time.time() - if is_idle_followup: - merge_runtime_idle_followup_turn( - context, - origin_user_request=user_text, - assistant_message=assistant_message, - assistant_created_at=assistant_created_at, - idle_followup_id=str( - idle_followup.get( - "id", - "", - ) - or "" - ), - ) - elif not is_action_guard_retry: + if not is_action_guard_retry: append_runtime_recent_turn( context, user_message=user_text, assistant_message=assistant_message, - user_created_at=getattr( + reasoning=getattr( context, - "runtime_turn_started_at", - None, + "runtime_turn_reasoning_content", + "", + ), + user_created_at=( + user_retry_replaced_turn.get("user_created_at") + if is_user_retry + and isinstance(user_retry_replaced_turn, dict) + else getattr( + context, + "runtime_turn_started_at", + None, + ) ), assistant_created_at=assistant_created_at, ) @@ -1413,10 +1448,7 @@ async def process_message( context.runtime_session_restore_delayed_memory_metadata = [] context.runtime_session_restore_attached_file_metadata = [] - if ( - is_idle_followup - or is_action_guard_retry - ): + if is_action_guard_retry: memory_update_task = None elif getattr( context, @@ -1457,12 +1489,9 @@ async def process_message( assistant_message=assistant_message, ) - if not is_action_guard_retry: + if not is_action_guard_retry and not is_user_retry: context.assistant_message_count += 1 - if ( - not is_idle_followup - and not is_action_guard_retry - ): + if not is_action_guard_retry and not is_user_retry: context.turn_number += 1 # Background fact-checking is intentionally not armed here. @@ -1522,6 +1551,9 @@ async def process_message( finally: + if is_user_retry: + context.runtime_user_retry_active = False + if is_action_guard_retry: context.runtime_suppress_chat_content = False context.runtime_action_guard_retry = {} diff --git a/websocket/tasks.py b/websocket/tasks.py index f5d44b99..e11edcc6 100644 --- a/websocket/tasks.py +++ b/websocket/tasks.py @@ -1,6 +1,5 @@ import asyncio import contextlib -from collections import deque from .logger import WebSocketLogger @@ -8,48 +7,6 @@ from runtime.L1_memory import schedule_interrupted_runtime_memory_update from utils.runtime_action_abort import abort_active_runtime_actions - -class PendingRequestQueue(asyncio.Queue): - - def _init(self, maxsize): - self._idle_followups = deque() - self._regular_requests = deque() - - @staticmethod - def _is_idle_followup(item) -> bool: - return ( - isinstance(item, dict) - and item.get("type") == "idle_followup" - and isinstance( - item.get("idle_followup"), - dict, - ) - ) - - def qsize(self) -> int: - return ( - len(self._idle_followups) - + len(self._regular_requests) - ) - - def empty(self) -> bool: - return self.qsize() == 0 - - def _put(self, item) -> None: - target = ( - self._idle_followups - if self._is_idle_followup(item) - else self._regular_requests - ) - target.append(item) - - def _get(self): - if self._idle_followups: - return self._idle_followups.popleft() - - return self._regular_requests.popleft() - - async def cancel_current_task( task: asyncio.Task | None, logger: WebSocketLogger, From f4a46d48c47024a3b790c114cc681dd443b1760e Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:50:28 +0300 Subject: [PATCH 096/168] chore: ui - fix for L-T fact key override after update and naming semantics - fix for latest session bootstrap - fix reasoning collapse jitter - fix expanded reasoning follow --- .gitignore | 5 + logs_anon/.gitkeep | 0 runtime/L4_memory_rules.py | 40 +- runtime/L4_memory_utils.py | 16 +- runtime/stream.py | 126 ++++++- tests/test_archived_session_restore.py | 16 +- ...test_bootstrap_completed_turn_atomicity.py | 107 ++++++ ...test_bootstrap_latest_completed_session.py | 242 ++++++++++++ ...trap_latest_session_fix_client_contract.py | 71 ++++ tests/test_chat_log.py | 44 +++ tests/test_l4_memory.py | 46 +++ tests/test_l4_memory_client_contract.py | 24 ++ tests/test_session_bootstrap_chat_tail.py | 42 +++ tests/test_ui_fix_bundle_contract.py | 18 + ui/static/css/chat-rating.css | 7 +- ui/static/js/chat.js | 343 +++++++++++++++++- ui/static/js/runtime/runtime-session.js | 233 ++++++++++-- ui/static/js/runtime/runtime-storage.js | 145 +++++++- ui/static/js/runtime/runtime.js | 33 +- ui/static/js/socket/event-handlers.js | 52 ++- ui/templates/index.html | 8 +- utils/chat_log.py | 48 ++- utils/session_restore.py | 134 ++++++- utils/stream_handler.py | 17 +- websocket/bootstrap.py | 172 ++++++++- websocket/messages.py | 118 +++--- 26 files changed, 1949 insertions(+), 158 deletions(-) create mode 100644 logs_anon/.gitkeep create mode 100644 tests/test_bootstrap_completed_turn_atomicity.py create mode 100644 tests/test_bootstrap_latest_completed_session.py create mode 100644 tests/test_bootstrap_latest_session_fix_client_contract.py diff --git a/.gitignore b/.gitignore index f9edbf14..afb3a279 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,11 @@ logs/* logs/**/* !logs/**/ !logs/**/.gitkeep +logs_anon/* +!logs_anon/.gitkeep +logs_anon/**/* +!logs_anon/**/ +!logs_anon/**/.gitkeep # Keep empty memory directory structure !memory/**/.gitkeep diff --git a/logs_anon/.gitkeep b/logs_anon/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py index ae5852c9..51fd95b5 100644 --- a/runtime/L4_memory_rules.py +++ b/runtime/L4_memory_rules.py @@ -13,13 +13,13 @@ - likely to remain true or relevant across future sessions; - useful enough that forgetting it could noticeably worsen a future response. -Suitable information includes stable user facts and preferences, durable project -facts, accepted decisions, persistent goals and constraints, recurring habits, -durable environment, and explicit corrections. +Suitable information includes stable user facts and preferences, facts and +accepted decisions about established projects, explicit cross-session goals and +constraints, recurring habits, durable environment, and explicit corrections. -Do not save current tasks, temporary topics, next steps, session state, tool -results, incidental names, one-off examples, isolated behavior, assistant -speculation, or external knowledge unrelated to the user or a durable project. +Save persistent state about the user, their environment, or an established +project. General knowledge, advice, explanations, and the subject matter of a +conversation are not memory state. Distinguish facts from interpretations: - a mention does not establish a relationship or role; @@ -37,13 +37,16 @@ - narrow: no broader than the source; - concrete and independently understandable; - stated without temporary wording such as "currently", "today", or "next"; -- linked to one or more exact input keys in source_keys. +- linked to one or more exact input keys in source_keys; +- keyed by the stored concept itself. Keep an input key when it already names + that concept well; rename it only when the canonical meaning actually changes. + Category is classification, not a key namespace. Return JSON only: { "facts": [ { - "key": "user.preference.response_language", + "key": "response_language", "value": "The user prefers Russian replies.", "category": "user_preference", "source_keys": ["response_language"] @@ -116,7 +119,8 @@ - Presence at a location is not residence. - Unknown details must remain unknown, not be guessed. - Direct user corrections override incompatible existing facts. -- A key must describe the final supported meaning, not preserve an older claim. +- Preserve a candidate or target key when it still describes the final meaning; + rename it only when the meaning changes enough that the old key becomes misleading. - Never invent facts, IDs, relationships, or source metadata. - exact_key_conflicts in the user payload are deterministic runtime hints. A create must not reuse an occupied key. An update may keep its target's key but @@ -133,11 +137,11 @@ Examples: {"operations":[{"action":"ignore","pending_id":"PF3","comment":"Already represented by F12."}]} -{"operations":[{"action":"update","pending_id":"PF8","target_id":"F12","key":"user.preference.response_language","value":"The user prefers Russian replies unless explicitly requesting another language.","category":"user_preference"}]} +{"operations":[{"action":"update","pending_id":"PF8","target_id":"F12","key":"response_language","value":"The user prefers Russian replies unless explicitly requesting another language.","category":"user_preference"}]} -{"operations":[{"action":"merge","pending_id":"PF21","fact_ids":["F12","F44"],"key":"project.operational_modes","value":"JIN uses the consolidated operational-mode model described by the current project definition.","category":"project_fact","comment":"Keep the compatible mode distinctions; retire the two overlapping old formulations."}]} +{"operations":[{"action":"merge","pending_id":"PF21","fact_ids":["F12","F44"],"key":"jin_core_operational_modes","value":"JIN Core uses the consolidated operational-mode model described by the current project definition.","category":"project_fact","comment":"Keep the compatible mode distinctions; retire the two overlapping old formulations."}]} -{"operations":[{"action":"create","pending_id":"PF31","key":"user.preference.response_language","value":"The user prefers Russian replies.","category":"user_preference"}]} +{"operations":[{"action":"create","pending_id":"PF31","key":"response_language","value":"The user prefers Russian replies.","category":"user_preference"}]} Return JSON only. Do not omit pending facts. Do not return multiple operations for the same pending_id. Do not add fields outside this contract. @@ -158,8 +162,10 @@ The note is a trusted edit instruction. Execute requested_action exactly while keeping memory minimal, accurate, atomic, useful, and free of semantic -duplicates. The service may normalize wording, key, and category, but it must -not silently change an update into a merge/create or a merge into a create. +duplicates. The service may normalize wording and category. Keep an existing key +when it still matches the meaning; rename it only when the note changes the +concept represented by that key. Do not silently change an update into a +merge/create or a merge into a create. Rules: - requested_action is authoritative for the selected facts: update edits exactly @@ -204,7 +210,7 @@ "action": "update", "replacement_facts": [ { - "key": "user.relationship.taras", + "key": "relationship_taras", "value": "Taras is both a close friend and an active technical stakeholder.", "category": "user_fact" } @@ -217,7 +223,7 @@ "action": "merge", "replacement_facts": [ { - "key": "user.relationship.taras", + "key": "relationship_taras", "value": "Taras is both a close friend and an active technical stakeholder.", "category": "user_fact" } @@ -231,7 +237,7 @@ "replacement_facts": [], "new_facts": [ { - "key": "user.preference.response_language", + "key": "response_language", "value": "The user prefers Russian replies.", "category": "user_preference" } diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 5999113c..13bddd7d 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -26,6 +26,10 @@ L4_PENDING_FACT_ID_PREFIX = "PF" L4_FACT_ID_RE = re.compile(r"^F([1-9]\d*)$", re.IGNORECASE) L4_PENDING_FACT_ID_RE = re.compile(r"^PF([1-9]\d*)$", re.IGNORECASE) +L4_FACT_REFERENCE_MEMORY_KEY_RE = re.compile( + r"^l4_fact_?f?[1-9]\d*$", + re.IGNORECASE, +) L4_LEGACY_FACT_ID_RE = re.compile(r"^l4_[a-z0-9_-]+$", re.IGNORECASE) L4_LEGACY_PENDING_FACT_ID_RE = re.compile(r"^l4p_[a-z0-9_-]+$", re.IGNORECASE) L4_FIELD_STATUS_PENDING = "pending" @@ -1013,6 +1017,14 @@ def normalize_l4_key(value) -> str: return key.strip("._-") +def is_l4_fact_reference_memory_key(value) -> bool: + return bool( + L4_FACT_REFERENCE_MEMORY_KEY_RE.fullmatch( + normalize_l4_key(value) + ) + ) + + def normalize_l4_category(value) -> str: category = normalize_l4_key(value) return category if category in L4_ALLOWED_CATEGORIES else "other" @@ -1999,6 +2011,8 @@ def normalize_facts_memory_records(value) -> list[dict]: signals = {} for key, field in raw_signals.items(): normalized_key = normalize_l4_key(key) + if is_l4_fact_reference_memory_key(normalized_key): + continue normalized_field = normalize_facts_memory_field( key=normalized_key, field=field, @@ -2126,7 +2140,7 @@ def normalize_l4_candidates( if not isinstance(field, dict): continue key = normalize_l4_key(field.get("key")) - if key: + if key and not is_l4_fact_reference_memory_key(key): fields_by_key.setdefault(key, []).append(field) candidates = [] diff --git a/runtime/stream.py b/runtime/stream.py index 8fe769e5..75cb867c 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -2,6 +2,7 @@ import contextlib import traceback import uuid +import time import httpx @@ -11,6 +12,14 @@ refresh_runtime_state, ) +from runtime.L1_memory_utils import ( + build_runtime_session_checkpoint, +) +from runtime.runtime_context import ( + RECENT_MESSAGES_MAX_PAIRS, +) + + from runtime.client import ( LMStudioAPIError, ) @@ -698,6 +707,116 @@ def capture_runtime_turn_response(self): self.stream.response ) + def build_message_end_checkpoint_payload(self) -> dict: + if not self.is_brain_context(): + return {} + + user_message = str( + getattr( + self.context, + "runtime_turn_user_message", + "", + ) + or "" + ).strip() + assistant_message = str( + getattr( + self.stream, + "response", + "", + ) + or "" + ).strip() + + # An action-only/internal stream has no visible completed USER/JIN pair. + # Its later follow-up stream will carry the actual visible checkpoint. + if not user_message or not assistant_message: + return {} + + session_snapshot = build_runtime_session_checkpoint( + self.context + ) + recent_turns = [ + dict(turn) + for turn in session_snapshot.get( + "recent_turns", + [], + ) + if isinstance(turn, dict) + ] + reasoning = str( + getattr( + self.stream, + "reasoning", + "", + ) + or "" + ).strip() + now = time.time() + current_turn = { + "user": user_message, + "jin": assistant_message, + "user_created_at": float( + getattr( + self.context, + "runtime_turn_started_at", + now, + ) + or now + ), + "jin_created_at": now, + } + if reasoning: + current_turn["reasoning"] = reasoning + + session_snapshot["recent_turns"] = ( + recent_turns + [current_turn] + )[-RECENT_MESSAGES_MAX_PAIRS:] + session_snapshot["previous_reasoning"] = reasoning + + if not getattr( + self.context, + "runtime_user_retry_active", + False, + ): + session_snapshot["assistant_message_count"] = ( + int( + session_snapshot.get( + "assistant_message_count", + 0, + ) + or 0 + ) + + 1 + ) + session_snapshot["turn_number"] = ( + int( + session_snapshot.get( + "turn_number", + 0, + ) + or 0 + ) + + 1 + ) + + return { + "session_snapshot": session_snapshot, + "completed_turn_commit": not bool( + getattr( + self.context, + "runtime_turn_interrupted", + False, + ) + or getattr( + self.context, + "runtime_turn_discard_requested", + False, + ) + ), + } + + def detect_context_limit_stage(self) -> str: if self.stream.response.strip(): @@ -3224,7 +3343,12 @@ async def run( await self.fail_unfinished_delayed_memory_actions() await self.stream.finish( - emit=self.emit_to_chat + emit=self.emit_to_chat, + end_payload_builder=( + self.build_message_end_checkpoint_payload + if self.emit_to_chat + else None + ), ) await self.refresh_token_usage() diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index 27814f8d..f7da7c0b 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -641,7 +641,7 @@ def test_browser_session_bootstrap_is_enriched_from_raw_archive(self): self.assertEqual(context.runtime_attached_file_ids, []) - def test_browser_checkpoint_does_not_mix_in_older_raw_dialogue(self): + def test_runtime_saved_at_does_not_block_missing_browser_dialogue_tail(self): archived = { "source_session_id": "archive-session", "messages": [ @@ -687,10 +687,16 @@ def test_browser_checkpoint_does_not_mix_in_older_raw_dialogue(self): enriched["runtime_memory"], "active_topic: Cooking instructions for macaroni and sausages", ) - self.assertNotIn("dialog_context", enriched) - self.assertNotIn("recent_turns", enriched) - self.assertNotIn("previous_reasoning", enriched) - self.assertNotIn("restore_reasoning_dump", enriched) + # Runtime saved_at is newer than the raw log, but it contains no + # dialogue tail. The archive must still supply the source session's + # latest conversation instead of pinning bootstrap to an older clone. + self.assertIn("Enderman", enriched["dialog_context"]) + self.assertEqual(enriched["recent_turns"], archived["recent_turns"]) + self.assertEqual( + enriched["previous_reasoning"], + "reasoning about Enderman", + ) + self.assertIn("Enderman", enriched["restore_reasoning_dump"]) self.assertTrue(enriched["archived_session_restore"]) def test_browser_checkpoint_explicit_empty_tool_results_stays_empty(self): diff --git a/tests/test_bootstrap_completed_turn_atomicity.py b/tests/test_bootstrap_completed_turn_atomicity.py new file mode 100644 index 00000000..823ecfff --- /dev/null +++ b/tests/test_bootstrap_completed_turn_atomicity.py @@ -0,0 +1,107 @@ +from pathlib import Path +from unittest.mock import patch + +from websocket.bootstrap import enrich_session_bootstrap_from_archive + + +ROOT = Path(__file__).resolve().parents[1] +MESSAGES_PY = ROOT / "websocket" / "messages.py" +STREAM_PY = ROOT / "runtime" / "stream.py" +HANDLERS_JS = ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" + + +def test_visible_message_end_carries_current_turn_bootstrap_preview(): + stream_source = STREAM_PY.read_text(encoding="utf-8") + + assert "def build_message_end_checkpoint_payload" in stream_source + assert '"session_snapshot": session_snapshot' in stream_source + assert "end_payload_builder=(" in stream_source + assert "self.build_message_end_checkpoint_payload" in stream_source + + source = MESSAGES_PY.read_text(encoding="utf-8") + start = source.index("async def process_message(") + block = source[start:] + recent_index = block.index("append_runtime_recent_turn(") + agent_end_index = block.index('"type": "agent_runtime_end"') + assert recent_index < agent_end_index + assert '"session_snapshot": completed_session_snapshot' in block + + +def test_message_end_persists_bootstrap_checkpoint_before_finishing_bubble(): + source = HANDLERS_JS.read_text(encoding="utf-8") + start = source.index("function handleMessageEnd(") + end = source.index("function handleMessageError(", start) + block = source[start:end] + + persist_index = block.index("persistLiveSessionCheckpoint") + finish_index = block.index("finishStreamMessage(") + + assert "data.session_snapshot" in block + assert persist_index < finish_index + + +def test_archive_dialogue_freshness_is_not_blocked_by_newer_runtime_saved_at(): + archived = { + "source_session_id": "source-session", + "archive_tail_at": "2026-08-24T18:20:00+03:00", + "recent_turns": [ + { + "user": "new user", + "jin": "new jin", + "user_created_at": 200.0, + "jin_created_at": 201.0, + }, + ], + "dialog_context": "new", + "previous_reasoning": "new reasoning", + "session_actions": [{"id": "archive-action"}], + } + + with patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=archived, + ): + enriched = enrich_session_bootstrap_from_archive( + { + "type": "session_bootstrap", + "source_session_id": "source-session", + # This runtime snapshot is newer than the raw archive tail, but + # its copied dialogue is older. Dialogue must still advance. + "saved_at": "2026-08-24T18:30:00+03:00", + "recent_turns": [ + { + "user": "old user", + "jin": "old jin", + "user_created_at": 100.0, + "jin_created_at": 101.0, + }, + ], + "session_actions": [{"id": "browser-action"}], + "runtime_memory": "current runtime", + } + ) + + assert enriched["recent_turns"] == archived["recent_turns"] + assert enriched["previous_reasoning"] == "new reasoning" + # Runtime/resource state still obeys runtime checkpoint freshness. + assert enriched["session_actions"] == [{"id": "browser-action"}] + + +def test_agent_runtime_end_commits_user_only_completed_turn_checkpoint(): + source = MESSAGES_PY.read_text(encoding="utf-8") + start = source.index("async def process_message(") + block = source[start:] + agent_end_index = block.index('"type": "agent_runtime_end"') + preceding = block[:agent_end_index] + + assert "completed_turn_commit = bool(" in preceding + assert 'and str(user_text or "").strip()' in preceding + assert '"completed_turn_commit": completed_turn_commit' in block[agent_end_index:] + + handler_source = HANDLERS_JS.read_text(encoding="utf-8") + start = handler_source.index("function handleAgentRuntimeEnd(data)") + end = handler_source.index("function handleMessageStart(", start) + handler = handler_source[start:end] + + assert "completed_turn_commit: Boolean(" in handler + assert "data.completed_turn_commit === true" in handler diff --git a/tests/test_bootstrap_latest_completed_session.py b/tests/test_bootstrap_latest_completed_session.py new file mode 100644 index 00000000..01d57ac6 --- /dev/null +++ b/tests/test_bootstrap_latest_completed_session.py @@ -0,0 +1,242 @@ +import json +from pathlib import Path +from unittest.mock import patch + +from utils.session_restore import find_latest_completed_session_restore_payload +from websocket.bootstrap import enrich_session_bootstrap_from_archive + + +def _write_dialog(root: Path, session_id: str, name: str, rows: list[dict]) -> None: + directory = root / "2026-08-24" / session_id + directory.mkdir(parents=True, exist_ok=True) + (directory / f"{name}.jsonl").write_text( + "\n".join(json.dumps(row, ensure_ascii=False) for row in rows), + encoding="utf-8", + ) + + +def test_latest_completed_selector_ignores_newer_blank_boot_session(tmp_path): + old_session = "old-session" + fresh_session = "fresh-session" + blank_session = "blank-session" + + _write_dialog(tmp_path, old_session, "190000", [ + {"ts": "2026-08-24T19:00:00+03:00", "turn": 10, "role": "user", "text": "old user"}, + {"ts": "2026-08-24T19:00:10+03:00", "turn": 10, "role": "jin", "text": "old jin"}, + ]) + _write_dialog(tmp_path, fresh_session, "192134", [ + {"ts": "2026-08-24T19:21:56+03:00", "turn": 265, "role": "user", "text": "напиши быстро тест"}, + {"ts": "2026-08-24T19:22:53+03:00", "turn": 265, "role": "jin", "text": "Тест пройден"}, + ]) + _write_dialog(tmp_path, blank_session, "192328", [ + {"ts": "2026-08-24T19:23:35+03:00", "turn": 264, "role": "jin", "text": ""}, + ]) + + payload = find_latest_completed_session_restore_payload(root=tmp_path) + + assert payload is not None + assert payload["source_session_id"] == fresh_session + assert payload["recent_turns"][-1]["user"] == "напиши быстро тест" + assert payload["recent_turns"][-1]["jin"] == "Тест пройден" + + +def test_mode_specific_selector_uses_separate_log_roots(tmp_path): + normal_root = tmp_path / "logs" + anonymous_root = tmp_path / "logs_anon" + normal_session = "normal-session" + anonymous_session = "anonymous-session" + + _write_dialog(normal_root, normal_session, "195000", [ + {"ts": "2026-08-24T19:50:00+03:00", "turn": 1, "role": "user", "text": "normal user"}, + {"ts": "2026-08-24T19:50:10+03:00", "turn": 1, "role": "jin", "text": "normal jin"}, + ]) + _write_dialog(anonymous_root, anonymous_session, "195500", [ + {"ts": "2026-08-24T19:55:00+03:00", "turn": 2, "role": "user", "text": "anonymous user"}, + {"ts": "2026-08-24T19:55:10+03:00", "turn": 2, "role": "jin", "text": "anonymous jin"}, + ]) + + with ( + patch("utils.chat_log.CHAT_LOG_ROOT", normal_root), + patch("utils.chat_log.CHAT_LOG_ANON_ROOT", anonymous_root), + ): + normal_payload = find_latest_completed_session_restore_payload( + anonymous_mode=False, + ) + anonymous_payload = find_latest_completed_session_restore_payload( + anonymous_mode=True, + ) + + assert normal_payload is not None + assert normal_payload["source_session_id"] == normal_session + assert normal_payload["recent_turns"][-1]["user"] == "normal user" + assert anonymous_payload is not None + assert anonymous_payload["source_session_id"] == anonymous_session + assert anonymous_payload["recent_turns"][-1]["user"] == "anonymous user" + + +def test_normal_bootstrap_never_reads_newer_anonymous_log_root(tmp_path): + normal_root = tmp_path / "logs" + anonymous_root = tmp_path / "logs_anon" + normal_session = "normal-source" + anonymous_session = "anonymous-newer" + + _write_dialog(normal_root, normal_session, "195000", [ + {"ts": "2026-08-24T19:50:00+03:00", "turn": 1, "role": "user", "text": "normal user"}, + {"ts": "2026-08-24T19:50:10+03:00", "turn": 1, "role": "jin", "text": "normal jin"}, + ]) + _write_dialog(anonymous_root, anonymous_session, "195500", [ + {"ts": "2026-08-24T19:55:00+03:00", "turn": 2, "role": "user", "text": "anonymous user"}, + {"ts": "2026-08-24T19:55:10+03:00", "turn": 2, "role": "jin", "text": "anonymous jin"}, + ]) + + with ( + patch("utils.chat_log.CHAT_LOG_ROOT", normal_root), + patch("utils.chat_log.CHAT_LOG_ANON_ROOT", anonymous_root), + ): + enriched = enrich_session_bootstrap_from_archive( + { + "type": "session_bootstrap", + "source_session_id": normal_session, + "runtime_memory": "browser normal runtime", + }, + anonymous_mode=False, + ) + + assert enriched["source_session_id"] == normal_session + assert enriched["recent_turns"][-1]["user"] == "normal user" + assert enriched["recent_turns"][-1]["jin"] == "normal jin" + + +def test_bootstrap_replaces_stale_source_with_newer_complete_raw_session(): + stale = { + "source_session_id": "stale-session", + "recent_turns": [{ + "user": "old user", + "jin": "old jin", + "user_created_at": 100.0, + "jin_created_at": 101.0, + }], + "dialog_context": "old", + } + fresh = { + "source_session_id": "fresh-session", + "recent_turns": [{ + "user": "напиши быстро тест", + "jin": "Тест пройден", + "user_created_at": 200.0, + "jin_created_at": 201.0, + }], + "dialog_context": "fresh", + } + + with ( + patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=stale, + ), + patch( + "utils.session_restore.find_latest_completed_session_restore_payload", + return_value=fresh, + ), + ): + enriched = enrich_session_bootstrap_from_archive({ + "type": "session_bootstrap", + "source_session_id": "stale-session", + "recent_turns": stale["recent_turns"], + "runtime_memory": "browser runtime", + }) + + assert enriched["source_session_id"] == "fresh-session" + assert enriched["recent_turns"] == fresh["recent_turns"] + assert enriched["dialog_context"] == fresh["dialog_context"] + + +def test_bootstrap_does_not_override_browser_dialogue_that_is_newer_than_raw_log(): + requested = { + "source_session_id": "requested-session", + "recent_turns": [{ + "user": "archive user", + "jin": "archive jin", + "user_created_at": 100.0, + "jin_created_at": 101.0, + }], + } + latest_raw = { + "source_session_id": "other-session", + "recent_turns": [{ + "user": "other user", + "jin": "other jin", + "user_created_at": 200.0, + "jin_created_at": 201.0, + }], + } + browser_turns = [{ + "user": "browser user", + "jin": "browser jin", + "user_created_at": 300.0, + "jin_created_at": 301.0, + }] + + with ( + patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=requested, + ), + patch( + "utils.session_restore.find_latest_completed_session_restore_payload", + return_value=latest_raw, + ), + ): + enriched = enrich_session_bootstrap_from_archive({ + "type": "session_bootstrap", + "source_session_id": "requested-session", + "recent_turns": browser_turns, + "runtime_memory": "browser runtime", + }) + + assert enriched["source_session_id"] == "requested-session" + assert enriched["recent_turns"] == browser_turns + + +def test_latest_selector_accepts_committed_user_only_action_turn(tmp_path): + previous_session = "previous-complete" + action_session = "action-only" + + _write_dialog(tmp_path, previous_session, "200000", [ + {"ts": "2026-08-24T20:00:00+03:00", "turn": 1, "role": "user", "text": "напиши быстро тест"}, + {"ts": "2026-08-24T20:00:10+03:00", "turn": 1, "role": "jin", "text": "Тест готов"}, + ]) + _write_dialog(tmp_path, action_session, "200500", [ + {"ts": "2026-08-24T20:05:00+03:00", "turn": 2, "role": "user", "text": "поставь себе вечерний цвет и громадный размер"}, + # Marker/action response was consumed by runtime and leaves no visible + # answer, but this row proves runtime.run completed the turn. + {"ts": "2026-08-24T20:05:05+03:00", "turn": 2, "role": "jin", "text": ""}, + ]) + + payload = find_latest_completed_session_restore_payload(root=tmp_path) + + assert payload is not None + assert payload["source_session_id"] == action_session + assert payload["messages"][-1]["role"] == "user" + assert payload["messages"][-1]["text"] == "поставь себе вечерний цвет и громадный размер" + assert payload["recent_turns"][-1]["user"] == "поставь себе вечерний цвет и громадный размер" + assert payload["recent_turns"][-1]["jin"] == "" + + +def test_latest_selector_ignores_unfinished_bare_user_turn(tmp_path): + complete_session = "complete" + interrupted_session = "interrupted" + + _write_dialog(tmp_path, complete_session, "201000", [ + {"ts": "2026-08-24T20:10:00+03:00", "turn": 1, "role": "user", "text": "complete user"}, + {"ts": "2026-08-24T20:10:05+03:00", "turn": 1, "role": "jin", "text": "complete jin"}, + ]) + _write_dialog(tmp_path, interrupted_session, "201500", [ + {"ts": "2026-08-24T20:15:00+03:00", "turn": 2, "role": "user", "text": "in flight"}, + ]) + + payload = find_latest_completed_session_restore_payload(root=tmp_path) + + assert payload is not None + assert payload["source_session_id"] == complete_session + assert payload["recent_turns"][-1]["jin"] == "complete jin" diff --git a/tests/test_bootstrap_latest_session_fix_client_contract.py b/tests/test_bootstrap_latest_session_fix_client_contract.py new file mode 100644 index 00000000..df1f1a4f --- /dev/null +++ b/tests/test_bootstrap_latest_session_fix_client_contract.py @@ -0,0 +1,71 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_SESSION_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-session.js" +) + + +class BootstrapLatestSessionFixClientContractTests(unittest.TestCase): + + def test_bootstrap_prefers_newest_per_session_runtime_over_global_pointer(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + start = source.index("function getPersistedSessionBootstrap()") + end = source.index("function clearPersistedSessionBootstrap()", start) + block = source[start:end] + + selection_start = block.index("let runtimeMemory =") + selection_end = block.index("if (!runtimeMemory)", selection_start) + selection = block[selection_start:selection_end] + + self.assertLess( + selection.index("browserLatestRuntimeMemory"), + selection.index("browserLatestSavedRuntimeMemory"), + ) + self.assertIn( + "compatibility fallback when no per-session predecessor exists", + block, + ) + + def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + helper_start = source.index( + "function isCurrentRuntimeCheckpointFreshest(" + ) + helper_end = source.index( + "function prefersReducedSceneTintMotion()", + helper_start, + ) + helper = source[helper_start:helper_end] + + self.assertIn("collectOtherLatestRuntimeMemorySnapshots()", helper) + self.assertIn("return otherSavedAt > currentSavedAt", helper) + + persist_start = source.index("function persistLiveSessionCheckpoint(data)") + persist_end = source.index( + "function clearPersistedToolResultsCheckpoint()", + persist_start, + ) + persist = source[persist_start:persist_end] + + guard = "if (!isCurrentRuntimeCheckpointFreshest(currentRuntime))" + self.assertIn(guard, persist) + self.assertLess( + persist.index(guard), + persist.index("writeLatestSavedRuntimeMemory({"), + ) + self.assertLess( + persist.index(guard), + persist.index("writeLatestSavedSessionSnapshot({"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_chat_log.py b/tests/test_chat_log.py index d8075a48..334381bf 100644 --- a/tests/test_chat_log.py +++ b/tests/test_chat_log.py @@ -104,6 +104,50 @@ def test_summarize_attachments_keeps_only_metadata(self): ], ) + def test_anonymous_chat_log_uses_separate_root_without_row_flag(self): + + context = SimpleNamespace( + session_id="anon-tab", + runtime_anonymous_mode=True, + runtime_turn_counter=1, + runtime_current_turn_id="turn_000001", + runtime_turn_attachments=[], + active_memory_records=[], + runtime_loaded_delayed_memory_ids=[], + ) + now = datetime( + 2026, + 8, + 24, + 20, + 0, + 0, + tzinfo=timezone.utc, + ) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_root = Path(temp_dir) + normal_root = temp_root / "logs" + anonymous_root = temp_root / "logs_anon" + from unittest.mock import patch + + with ( + patch("utils.chat_log.CHAT_LOG_ROOT", normal_root), + patch("utils.chat_log.CHAT_LOG_ANON_ROOT", anonymous_root), + ): + path = append_chat_log_entry( + context, + role="user", + text="anonymous hello", + now=now, + ) + + self.assertTrue(path.is_relative_to(anonymous_root)) + self.assertFalse(normal_root.exists()) + row = json.loads(path.read_text(encoding="utf-8").strip()) + self.assertNotIn("anonymous_mode", row) + self.assertEqual(row["text"], "anonymous hello") + def test_append_chat_log_entry_uses_date_session_directory(self): context = SimpleNamespace( diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index ab3593f1..b7d50d96 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -113,6 +113,52 @@ def test_facts_memory_normalization_adds_session_and_pending_status(self): self.assertEqual(field["l4_status"], "pending") self.assertTrue(field["l4_content_hash"]) + + def test_facts_memory_drops_l4_fact_reference_bookkeeping_keys(self): + records = normalize_facts_memory_records([ + { + "session_id": "session-a", + "signals": { + "L4 fact #305": { + "content": ( + "The description now excludes white bonfire." + ), + }, + "environment_physical_setup": { + "content": "The setup includes coffee, bong, and bricks.", + }, + }, + }, + ]) + + self.assertEqual(len(records), 1) + self.assertEqual( + list(records[0]["signals"]), + ["environment_physical_setup"], + ) + self.assertEqual( + [field["key"] for field in collect_pending_facts_memory_fields(records)], + ["environment_physical_setup"], + ) + self.assertEqual( + normalize_l4_candidates( + { + "facts": [{ + "key": "l4_fact_305", + "value": "Bookkeeping about the L4 update.", + "category": "other", + "source_keys": ["l4_fact_305"], + }], + }, + source_fields=[{ + "key": "l4_fact_305", + "content": "The description now excludes white bonfire.", + "session_id": "session-a", + }], + ), + [], + ) + def test_mark_analyzed_only_marks_matching_content_hash(self): records = normalize_facts_memory_records([ { diff --git a/tests/test_l4_memory_client_contract.py b/tests/test_l4_memory_client_contract.py index 4130742b..d4a1549c 100644 --- a/tests/test_l4_memory_client_contract.py +++ b/tests/test_l4_memory_client_contract.py @@ -56,6 +56,30 @@ def test_idle_l4_scheduler_is_not_driven_by_browser_javascript(self): ) self.assertIn("window.syncFactsMemoryToRuntime();", runtime_source) + + def test_l4_fact_reference_runtime_keys_do_not_feed_facts_memory(self): + source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime.js" + ).read_text(encoding="utf-8") + + self.assertIn( + "FACTS_MEMORY_EXCLUDED_KEY_PATTERNS", + source, + ) + self.assertIn( + r"/^l4_fact_?f?[1-9]\d*$/i", + source, + ) + self.assertIn( + "delete fields[key];", + source, + ) + def test_equal_revision_update_cannot_replace_larger_local_snapshot(self): source = ( ROOT diff --git a/tests/test_session_bootstrap_chat_tail.py b/tests/test_session_bootstrap_chat_tail.py index 0b8a4154..4764f4d0 100644 --- a/tests/test_session_bootstrap_chat_tail.py +++ b/tests/test_session_bootstrap_chat_tail.py @@ -102,6 +102,26 @@ def test_bootstrap_hydration_preserves_turn_reasoning_and_ui_tail(self): ], ) + def test_bootstrap_tail_keeps_committed_user_only_action_turn(self): + context = SimpleNamespace( + runtime_recent_turns=[{ + "user": "поставь себе цвет ff0000", + "jin": "", + "user_created_at": 100.0, + "jin_created_at": 101.0, + }], + ) + + self.assertEqual( + build_session_bootstrap_chat_tail(context), + [{ + "user": "поставь себе цвет ff0000", + "jin": "", + "user_created_at": 100.0, + "jin_created_at": 101.0, + }], + ) + def test_live_recent_turn_persists_reasoning(self): context = SimpleNamespace( runtime_recent_turns=[], @@ -132,6 +152,28 @@ def test_client_renders_bootstrap_tail_with_existing_chat_primitives(self): self.assertIn("window.jinArchivedSessionRestorePayload", source) self.assertIn(".slice(-3)", source) + def test_client_keeps_user_only_turn_without_blank_br_bubble(self): + source = SOCKET_HANDLERS_JS.read_text(encoding="utf-8") + handler_start = source.index( + "function handleSessionBootstrapChatTail" + ) + handler_end = source.index( + "function handleSessionActionsUpdate", + handler_start, + ) + handler_source = source[handler_start:handler_end] + + self.assertIn( + '&& String(turn.user || "").trim()', + handler_source, + ) + self.assertNotIn( + '&& String(turn.jin || "").trim()', + handler_source, + ) + self.assertIn("if (!jinText)", handler_source) + self.assertIn("appendSessionBootstrapBoundary", handler_source) + def test_client_places_current_session_boundary_above_new_response(self): source = SOCKET_HANDLERS_JS.read_text(encoding="utf-8") diff --git a/tests/test_ui_fix_bundle_contract.py b/tests/test_ui_fix_bundle_contract.py index 89dcf43c..baff9f7b 100644 --- a/tests/test_ui_fix_bundle_contract.py +++ b/tests/test_ui_fix_bundle_contract.py @@ -29,6 +29,23 @@ def test_live_turn_releases_top_lock_and_autoscrolls_on_overflow(self): self.assertIn("releaseLiveUserTurnTopLock();", source) self.assertIn("liveTurnOverflowAutoscroll =", source) + def test_reasoning_collapse_keeps_live_turn_viewport_stable(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn( + "function syncLiveUserTurnViewportForLayoutChange()", + source, + ) + self.assertIn( + "syncLiveUserTurnViewportForLayoutChange();", + source, + ) + self.assertIn( + "thinkContent.scrollTop =\n thinkContent.scrollHeight;", + source, + ) + self.assertNotIn('behavior: "smooth"', source) + def test_input_focus_and_form_padding_click(self): socket_source = SOCKET_JS.read_text(encoding="utf-8") input_source = INPUT_JS.read_text(encoding="utf-8") @@ -60,6 +77,7 @@ def test_cache_busters_cover_updated_ui_files(self): "&reasoning-state=1&live-turn-overflow-scroll=2", source, ) + self.assertIn("&reasoning-collapse-stability=1", source) self.assertIn("&input-focus=2", source) self.assertIn("&input-focus=3", source) diff --git a/ui/static/css/chat-rating.css b/ui/static/css/chat-rating.css index daa4fe64..561e9490 100644 --- a/ui/static/css/chat-rating.css +++ b/ui/static/css/chat-rating.css @@ -471,13 +471,14 @@ body:not(.jin-answer-rating-enabled) .jin-chat-bubble-brain .jin-chat-pre { /* Reuse the old neutral rating glow for the whole bubble. No red/green edge, skew or directional motion survives while rating is disabled. */ body:not(.jin-answer-rating-enabled) .jin-bubble-utility-enabled:hover { + /* Keep the neutral hover as an outer lift. The old 24/30px inset blurs + overlap on one-line bubbles and turn the whole interior into a muddy + band, which looks like a broken inner shadow. */ box-shadow: 0 14px 42px rgba(0, 0, 0, 0.24), 0 0 30px rgba(226, 232, 240, 0.070), 0 0 28px rgba(45, 212, 191, 0.040), - inset 0 1px 0 rgba(248, 250, 252, 0.060), - inset 0 0 24px rgba(226, 232, 240, 0.035), - inset 0 0 30px rgba(103, 232, 249, 0.010); + inset 0 1px 0 rgba(248, 250, 252, 0.060); filter: brightness(1.03); } diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index 3e3a04a1..f1a70d75 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -23,6 +23,8 @@ const MEMORY_REFERENCE_HIGHLIGHT_EVENT = let liveUserTurnAnchor = null; let keepLiveUserTurnAtTop = false; +let expandedReasoningFollowStream = null; +let expandedReasoningFollowFrame = null; let jinThinkCollapsedPreference = true; @@ -604,8 +606,206 @@ function scrollLiveUserTurnToTop() { } +function stopExpandedReasoningFollow( + stream = null +) { + + if ( + stream + && expandedReasoningFollowStream !== stream + ) { + return; + } + + expandedReasoningFollowStream = null; + + if (expandedReasoningFollowFrame) { + cancelAnimationFrame( + expandedReasoningFollowFrame + ); + expandedReasoningFollowFrame = null; + } + +} + + +function canFollowExpandedReasoning( + stream +) { + + return Boolean( + chatHistory + && stream + && stream.group + && stream.runtimeAvatarReasoningActive + && stream.group.createdThinking + && !stream.group.createdAnswer + && stream.group.thinkContent + && stream.group.thinkContent.isConnected + && !stream.group.thinkContent.classList.contains( + "is-collapsed" + ) + && stream.group.avatarSlot + && stream.group.avatarSlot.isConnected + ); + +} + + +function getExpandedReasoningFollowTarget( + stream +) { + + if (!canFollowExpandedReasoning(stream)) { + return null; + } + + const historyRect = + chatHistory.getBoundingClientRect(); + const avatarRect = + stream.group.avatarSlot.getBoundingClientRect(); + const visibleBottom = + historyRect.bottom + - getChatInputOverlaySpace() + - getChatHistoryTopGap(); + const overflow = + avatarRect.bottom - visibleBottom; + + if (overflow <= 0.5) { + return chatHistory.scrollTop; + } + + const maxScrollTop = + Math.max( + 0, + chatHistory.scrollHeight + - chatHistory.clientHeight + ); + + return Math.min( + maxScrollTop, + chatHistory.scrollTop + overflow + ); + +} + + +function runExpandedReasoningFollowFrame() { + + expandedReasoningFollowFrame = null; + + const stream = + expandedReasoningFollowStream; + + if (!canFollowExpandedReasoning(stream)) { + stopExpandedReasoningFollow( + stream + ); + return; + } + + updateLiveUserTurnBottomSpace(); + + const targetTop = + getExpandedReasoningFollowTarget( + stream + ); + + if (targetTop === null) { + stopExpandedReasoningFollow( + stream + ); + return; + } + + const delta = + targetTop - chatHistory.scrollTop; + + if (delta <= 0.5) { + return; + } + + chatHistory.scrollTop = + Math.min( + targetTop, + chatHistory.scrollTop + + Math.max( + 1, + delta * 0.24 + ) + ); + + if ( + targetTop - chatHistory.scrollTop + > 0.5 + ) { + expandedReasoningFollowFrame = + requestAnimationFrame( + runExpandedReasoningFollowFrame + ); + } + +} + + +function queueExpandedReasoningFollow() { + + if ( + expandedReasoningFollowFrame + || !canFollowExpandedReasoning( + expandedReasoningFollowStream + ) + ) { + return; + } + + expandedReasoningFollowFrame = + requestAnimationFrame( + runExpandedReasoningFollowFrame + ); + +} + + +function startExpandedReasoningFollow( + stream +) { + + if (!canFollowExpandedReasoning(stream)) { + return; + } + + if ( + expandedReasoningFollowStream + && expandedReasoningFollowStream !== stream + ) { + stopExpandedReasoningFollow(); + } + + expandedReasoningFollowStream = + stream; + + // Manual expansion hands scroll ownership from the pinned USER row to the + // live reasoning tail. From here the viewport follows the moving avatar + // only when it reaches the usable bottom edge of the chat. + keepLiveUserTurnAtTop = false; + updateLiveUserTurnBottomSpace(); + queueExpandedReasoningFollow(); + +} + + +function releaseLiveUserTurnViewportControl() { + + releaseLiveUserTurnTopLock(); + stopExpandedReasoningFollow(); + +} + + function prepareLiveUserTurnViewport() { + stopExpandedReasoningFollow(); liveUserTurnAnchor = null; keepLiveUserTurnAtTop = false; @@ -657,6 +857,32 @@ function releaseLiveUserTurnTopLock() { } +function syncLiveUserTurnViewportForLayoutChange() { + + if ( + !liveUserTurnAnchor + || !liveUserTurnAnchor.isConnected + ) { + return; + } + + // A reasoning max-height transition changes the visible tail height without + // producing a stream frame. Keep the compensating bottom spacer in lockstep + // so the browser never has to clamp chatHistory.scrollTop mid-collapse. + if (keepLiveUserTurnAtTop) { + scrollLiveUserTurnToTop(); + return; + } + + updateLiveUserTurnBottomSpace(); + + if (expandedReasoningFollowStream) { + queueExpandedReasoningFollow(); + } + +} + + function scrollChatHistoryAfterAppend() { if (!chatHistory) { @@ -670,6 +896,11 @@ function scrollChatHistoryAfterAppend() { return; } + if (expandedReasoningFollowStream) { + queueExpandedReasoningFollow(); + return; + } + chatHistory.scrollTop = chatHistory.scrollHeight; @@ -702,13 +933,13 @@ function shouldAutoScroll() { if (chatHistory) { chatHistory.addEventListener( "wheel", - releaseLiveUserTurnTopLock, + releaseLiveUserTurnViewportControl, { passive: true } ); chatHistory.addEventListener( "touchstart", - releaseLiveUserTurnTopLock, + releaseLiveUserTurnViewportControl, { passive: true } ); } @@ -720,7 +951,7 @@ window.addEventListener( event.detail && event.detail.active === false ) { - releaseLiveUserTurnTopLock(); + releaseLiveUserTurnViewportControl(); } } ); @@ -733,6 +964,8 @@ window.addEventListener( if (keepLiveUserTurnAtTop) { scrollLiveUserTurnToTop(); + } else if (expandedReasoningFollowStream) { + queueExpandedReasoningFollow(); } } ); @@ -887,6 +1120,10 @@ function flushStreamFrame() { !stream.group.createdAnswer ) { + stopExpandedReasoningFollow( + stream + ); + stream.group.wrapper.classList.remove( "is-awaiting-model" ); @@ -930,7 +1167,10 @@ function flushStreamFrame() { let liveTurnOverflowAutoscroll = false; - if (liveUserTurnReachedViewportBottom()) { + if ( + !expandedReasoningFollowStream + && liveUserTurnReachedViewportBottom() + ) { releaseLiveUserTurnTopLock(); liveTurnOverflowAutoscroll = true; @@ -938,6 +1178,8 @@ function flushStreamFrame() { if (keepLiveUserTurnAtTop) { scrollLiveUserTurnToTop(); + } else if (expandedReasoningFollowStream) { + queueExpandedReasoningFollow(); } else if ( ( autoscroll @@ -1525,12 +1767,22 @@ function trackStreamAvatarLayoutTransition( if ( !stream || !stream.group - || !stream.group.avatarSlot ) { return; } const group = stream.group; + + if ( + !group.avatarSlot + && !( + liveUserTurnAnchor + && liveUserTurnAnchor.isConnected + ) + ) { + return; + } + const trackId = (group.avatarLayoutTrackId || 0) + 1; const startedAt = nowMs(); @@ -1542,15 +1794,35 @@ function trackStreamAvatarLayoutTransition( if ( group.avatarLayoutTrackId !== trackId - || !group.avatarSlot - || !group.avatarSlot.isConnected ) { return; } - syncStreamAvatarPosition( - stream - ); + const avatarConnected = + Boolean( + group.avatarSlot + && group.avatarSlot.isConnected + ); + const liveTurnConnected = + Boolean( + liveUserTurnAnchor + && liveUserTurnAnchor.isConnected + ); + + if ( + !avatarConnected + && !liveTurnConnected + ) { + return; + } + + if (avatarConnected) { + syncStreamAvatarPosition( + stream + ); + } + + syncLiveUserTurnViewportForLayoutChange(); if ( nowMs() - startedAt @@ -1562,9 +1834,13 @@ function trackStreamAvatarLayoutTransition( return; } - syncStreamAvatarPosition( - stream - ); + if (avatarConnected) { + syncStreamAvatarPosition( + stream + ); + } + + syncLiveUserTurnViewportForLayoutChange(); }; @@ -1574,6 +1850,7 @@ function trackStreamAvatarLayoutTransition( } + function installStreamThinkResizeObserver( stream ) { @@ -1603,6 +1880,12 @@ function installStreamThinkResizeObserver( queueStreamAvatarPositionSync( stream ); + + if ( + expandedReasoningFollowStream === stream + ) { + queueExpandedReasoningFollow(); + } } ); @@ -1799,10 +2082,11 @@ function scrollCollapsedThinkToLatest( return; } - thinkContent.scrollTo({ - top: thinkContent.scrollHeight, - behavior: "smooth", - }); + // The collapsed preview must land on the latest reasoning immediately. + // A smooth inner scroll runs at the same time as the max-height collapse + // and makes the whole interaction look like the chat is still moving. + thinkContent.scrollTop = + thinkContent.scrollHeight; } @@ -1963,6 +2247,17 @@ function createStreamGroup( : "true" ); + if ( + typeof thinkContent.__jinExpandedReasoningFollow + === "function" + ) { + thinkContent.__jinExpandedReasoningFollow( + !collapsed + ); + } + + syncLiveUserTurnViewportForLayoutChange(); + if (collapsed) { requestAnimationFrame( () => { @@ -2193,6 +2488,20 @@ function ensureStreamGroup( ); }; + stream.group.thinkContent.__jinExpandedReasoningFollow = + (expanded) => { + if (expanded) { + startExpandedReasoningFollow( + stream + ); + return; + } + + stopExpandedReasoningFollow( + stream + ); + }; + installStreamThinkResizeObserver( stream ); diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 6e67b967..21b09a59 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -58,6 +58,7 @@ keys: runtimeStorageKeys, removeBrowserMemory, readLatestRuntimeMemory, + writeLatestRuntimeMemory, writeLatestSavedSessionSnapshot, readLatestSavedSessionSnapshot, writeLatestSavedRuntimeMemory, @@ -66,6 +67,7 @@ setBootSourceRuntimeSessionId, cloneRuntimeMemoryToCurrentSession, readLatestPreviousRuntimeMemory, + readLatestCompletedConversationRuntimeMemory, collectOtherLatestRuntimeMemorySnapshots, clearOtherLatestRuntimeMemorySnapshots, getSavedRuntimeMemoryFallback, @@ -541,6 +543,23 @@ && String(previousCheckpoint.session_id || "").trim() === currentSessionId ); + const completedTurnCommit = Boolean( + data + && data.completed_turn_commit === true + ); + + // The global checkpoint represents the latest completed conversation, + // not the most recently opened tab. A boot-only/stale tab may keep its + // own per-session L1 cache, but it must not steal bootstrap ownership + // through a reconnect/duplicate/background L1 echo. Ownership moves to + // another tab only when that tab finishes a visible USER -> JIN turn. + if ( + previousCheckpoint + && !sameSession + && !completedTurnCommit + ) { + return false; + } const previousSessionId = String( currentRuntime.previous_session_id @@ -566,6 +585,39 @@ data || {}, previousSessionSnapshot ); + const previousConversationCommittedAt = + String( + currentRuntime.conversation_committed_at + || ( + sameSession + && previousCheckpoint + && previousCheckpoint.conversation_committed_at + ) + || "" + ).trim(); + const conversationCommittedAt = + completedTurnCommit + ? savedAt + : previousConversationCommittedAt; + + // Persist the conversation owner on this session's own record. Unlike + // saved_at (which advances on L1 updates/reconnects), this timestamp + // advances only after a visible USER -> JIN turn has completed. A blank + // tab or a late L1 echo therefore cannot become the next bootstrap owner. + if (completedTurnCommit) { + writeLatestRuntimeMemory({ + ...currentRuntime, + version: currentRuntime.version || 1, + session_id: currentSessionId, + previous_session_id: previousSessionId, + conversation_committed_at: conversationCommittedAt, + session_snapshot: sessionSnapshot, + runtime_snapshot: + buildCheckpointRuntimeSnapshot( + currentRuntime.runtime_snapshot + ), + }); + } writeLatestSavedRuntimeMemory({ ...currentRuntime, @@ -573,6 +625,8 @@ session_id: currentSessionId, previous_session_id: previousSessionId, saved_at: savedAt, + conversation_committed_at: conversationCommittedAt, + session_snapshot: sessionSnapshot, runtime_snapshot: buildCheckpointRuntimeSnapshot( currentRuntime.runtime_snapshot @@ -584,6 +638,7 @@ session_id: currentSessionId, previous_session_id: previousSessionId, saved_at: savedAt, + conversation_committed_at: conversationCommittedAt, loaded_memory_ids: sessionSnapshot.loaded_memory_ids, session_snapshot: sessionSnapshot, @@ -1322,12 +1377,9 @@ bootstrap.runtime_snapshot || null, }); - // The fresh tab immediately becomes the new live checkpoint, even - // before the user sends another message. This keeps last-saved lineage - // one hop wide instead of leaving it pointed at the older source tab. - persistLiveSessionCheckpoint( - bootstrap - ); + // Do not advance the global conversation checkpoint on page load. + // This tab becomes the next bootstrap predecessor only after it + // completes a visible USER -> JIN turn. } if ( @@ -1438,27 +1490,158 @@ readLatestPreviousRuntimeMemory ? readLatestPreviousRuntimeMemory() : null; + const latestCompletedConversationRuntime = + readLatestCompletedConversationRuntimeMemory + ? readLatestCompletedConversationRuntimeMemory() + : null; - let runtimeMemory = + const parseCommitTimestamp = value => { + const parsed = Date.parse( + String(value || "").trim() + ); + return Number.isFinite(parsed) + ? parsed + : 0; + }; + const checkpointCommittedAt = + parseCommitTimestamp( + browserCheckpoint + && browserCheckpoint.conversation_committed_at + ); + const perSessionCommittedAt = + parseCommitTimestamp( + latestCompletedConversationRuntime + && latestCompletedConversationRuntime.conversation_committed_at + ); + const perSessionConversationWins = Boolean( + latestCompletedConversationRuntime + && perSessionCommittedAt + && ( + !checkpointCommittedAt + || perSessionCommittedAt > checkpointCommittedAt + ) + ); + + // Dialogue ownership comes from an actual completed turn, never from the + // most recently touched L1 record. This heals stale global checkpoints + // left by older builds while keeping empty/reconnected tabs ineligible. + const conversationCheckpoint = + perSessionConversationWins + ? { + version: 1, + session_id: + latestCompletedConversationRuntime.session_id, + previous_session_id: + latestCompletedConversationRuntime.previous_session_id + || latestCompletedConversationRuntime.booted_from_session_id + || null, + saved_at: + latestCompletedConversationRuntime.saved_at + || latestCompletedConversationRuntime.conversation_committed_at + || "", + conversation_committed_at: + latestCompletedConversationRuntime.conversation_committed_at, + loaded_memory_ids: + ( + latestCompletedConversationRuntime.session_snapshot + && Array.isArray( + latestCompletedConversationRuntime.session_snapshot.loaded_memory_ids + ) + ) + ? latestCompletedConversationRuntime.session_snapshot.loaded_memory_ids + : [], + session_snapshot: + ( + latestCompletedConversationRuntime.session_snapshot + && typeof latestCompletedConversationRuntime.session_snapshot === "object" + && !Array.isArray( + latestCompletedConversationRuntime.session_snapshot + ) + ) + ? { + ...latestCompletedConversationRuntime.session_snapshot, + } + : {}, + } + : browserCheckpoint; + + const checkpointSessionId = + String( + conversationCheckpoint + && conversationCheckpoint.session_id + || "" + ).trim(); + const savedRuntimeSessionId = + String( + browserLatestSavedRuntimeMemory + && browserLatestSavedRuntimeMemory.session_id + || "" + ).trim(); + const pairedSavedRuntime = Boolean( + checkpointSessionId + && browserLatestSavedRuntimeMemory + && typeof browserLatestSavedRuntimeMemory === "object" + && !Array.isArray(browserLatestSavedRuntimeMemory) + && savedRuntimeSessionId === checkpointSessionId + && String( + browserLatestSavedRuntimeMemory.runtime_memory + || "" + ).trim() + ) + ? browserLatestSavedRuntimeMemory + : null; + const checkpointRuntime = ( + checkpointSessionId + && collectOtherLatestRuntimeMemorySnapshots + ) + ? collectOtherLatestRuntimeMemorySnapshots() + .find(snapshot => ( + snapshot + && String(snapshot.session_id || "").trim() + === checkpointSessionId + && String(snapshot.runtime_memory || "").trim() + )) + : null; + const completedConversationRuntime = Boolean( + checkpointSessionId + && latestCompletedConversationRuntime + && String( + latestCompletedConversationRuntime.session_id + || "" + ).trim() === checkpointSessionId + && String( + latestCompletedConversationRuntime.runtime_memory + || "" + ).trim() + ) + ? latestCompletedConversationRuntime + : null; + + let runtimeMemory = + completedConversationRuntime + || pairedSavedRuntime + || checkpointRuntime + || ( + browserLatestRuntimeMemory + && typeof browserLatestRuntimeMemory === "object" + && !Array.isArray(browserLatestRuntimeMemory) + && String( + browserLatestRuntimeMemory.runtime_memory + || "" + ).trim() + ? browserLatestRuntimeMemory + : null + ) + || ( browserLatestSavedRuntimeMemory - && typeof browserLatestSavedRuntimeMemory === "object" - && !Array.isArray(browserLatestSavedRuntimeMemory) && String( browserLatestSavedRuntimeMemory.runtime_memory || "" ).trim() - ) - ? browserLatestSavedRuntimeMemory - : ( - browserLatestRuntimeMemory - && String( - browserLatestRuntimeMemory.runtime_memory - || "" - ).trim() - ? browserLatestRuntimeMemory - : null - ); + ? browserLatestSavedRuntimeMemory + : null + ); if (!runtimeMemory) { const savedRuntimeFallback = @@ -1497,18 +1680,18 @@ ).trim(); const checkpointMatchesSource = Boolean( - browserCheckpoint - && typeof browserCheckpoint === "object" - && !Array.isArray(browserCheckpoint) + conversationCheckpoint + && typeof conversationCheckpoint === "object" + && !Array.isArray(conversationCheckpoint) && ( !sourceSessionId - || String(browserCheckpoint.session_id || "").trim() + || String(conversationCheckpoint.session_id || "").trim() === sourceSessionId ) ); const checkpoint = checkpointMatchesSource - ? browserCheckpoint + ? conversationCheckpoint : null; if (!sourceSessionId && checkpoint) { diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 4c723dc1..0384b338 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -634,8 +634,56 @@ value ) { + const previousValue = + sanitizeRuntimeMemoryRecord( + readRuntimeSessionMemory( + latestRuntimeMemoryStorageKey + ) + ); + value = sanitizeRuntimeMemoryRecord(value); + if ( + value + && typeof value === "object" + && !Array.isArray(value) + && previousValue + && typeof previousValue === "object" + && !Array.isArray(previousValue) + ) { + const previousCommittedAt = + String( + previousValue.conversation_committed_at + || "" + ).trim(); + + if ( + previousCommittedAt + && !String( + value.conversation_committed_at + || "" + ).trim() + ) { + value.conversation_committed_at = + previousCommittedAt; + } + + if ( + previousValue.session_snapshot + && typeof previousValue.session_snapshot === "object" + && !Array.isArray(previousValue.session_snapshot) + && !( + value.session_snapshot + && typeof value.session_snapshot === "object" + && !Array.isArray(value.session_snapshot) + ) + ) { + value.session_snapshot = { + ...previousValue.session_snapshot, + }; + } + } + const normalizedValue = ( value @@ -713,6 +761,8 @@ String(value.previous_session_id || "").trim() || null, saved_at: String(value.saved_at || "").trim(), + conversation_committed_at: + String(value.conversation_committed_at || "").trim(), loaded_memory_ids: Array.isArray(value.loaded_memory_ids) ? value.loaded_memory_ids @@ -2059,11 +2109,12 @@ writeLatestRuntimeMemory({ version: runtimeMemory.version || 1, - // This is a snapshot of the new session at boot time, not a copy of - // the predecessor timestamp. That guarantees newest-by-saved_at - // resolves to the direct previous session on the following boot. + // Opening a tab is not conversation activity. Keep the predecessor's + // causal timestamp until this tab produces a real L1 update / completed + // turn; otherwise empty tabs outrank the session the user actually used. saved_at: - new Date().toISOString(), + String(runtimeMemory.saved_at || "").trim() + || new Date().toISOString(), runtime_memory: runtimeMemory.runtime_memory || "", runtime_memory_updates: @@ -2176,6 +2227,23 @@ && value.saved_at ) || null, + conversation_committed_at: + ( + value + && value.conversation_committed_at + ) + || null, + session_snapshot: + ( + value + && value.session_snapshot + && typeof value.session_snapshot === "object" + && !Array.isArray(value.session_snapshot) + ) + ? { + ...value.session_snapshot, + } + : null, runtime_memory_updates: ( value @@ -2279,6 +2347,74 @@ } + function readLatestCompletedConversationRuntimeMemory() { + + if (shouldIsolateAnonymousStorage()) { + return null; + } + + const snapshots = + collectOtherLatestRuntimeMemorySnapshots() + .filter(snapshot => ( + snapshot + && String( + snapshot.conversation_committed_at + || "" + ).trim() + && String( + snapshot.runtime_memory + || "" + ).trim() + )) + .sort((left, right) => ( + String( + right.conversation_committed_at + || "" + ).localeCompare( + String( + left.conversation_committed_at + || "" + ) + ) + )); + + if (!snapshots.length) { + return null; + } + + const latest = snapshots[0]; + const value = readBrowserMemory( + latest.key + ); + + if ( + !value + || typeof value !== "object" + || Array.isArray(value) + ) { + return null; + } + + const sourceSessionId = + String( + value.session_id + || latest.key_session_id + || "" + ).trim(); + + if (!sourceSessionId) { + return null; + } + + return { + ...value, + session_id: sourceSessionId, + storage_key: latest.key, + }; + + } + + function clearOtherLatestRuntimeMemorySnapshots() { if (shouldIsolateAnonymousStorage()) { @@ -2583,6 +2719,7 @@ cloneBootRuntimeMemoryIfNeeded, collectOtherLatestRuntimeMemorySnapshots, readLatestPreviousRuntimeMemory, + readLatestCompletedConversationRuntimeMemory, clearOtherLatestRuntimeMemorySnapshots, extractSavedRuntimeConstant, parseSavedRuntimeText, diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 6ad3cc53..38dfa154 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -295,6 +295,30 @@ const FACTS_MEMORY_EXCLUDED_KEYS = new Set([ "user_idle", ]); +const FACTS_MEMORY_EXCLUDED_KEY_PATTERNS = [ + /^l4_fact_?f?[1-9]\d*$/i, +]; + +function isFactsMemoryExcludedKey( + key +) { + + const normalizedKey = + normalizeRuntimeMemoryKey( + key + ); + + return ( + FACTS_MEMORY_EXCLUDED_KEYS.has( + normalizedKey + ) + || FACTS_MEMORY_EXCLUDED_KEY_PATTERNS.some( + pattern => pattern.test(normalizedKey) + ) + ); + +} + const deletedFactsMemoryKeys = new Set(); @@ -341,6 +365,14 @@ function persistRuntimeFactsMemory( line && line.key ); + if ( + key + && isFactsMemoryExcludedKey(key) + ) { + delete fields[key]; + return; + } + const content = String( stripRuntimeMemoryMeta( @@ -354,7 +386,6 @@ function persistRuntimeFactsMemory( || deletedFactsMemoryKeys.has( getFactsMemoryIdentity(key) ) - || FACTS_MEMORY_EXCLUDED_KEYS.has(key) || isJinResponseRuntimeMemoryKey(key) || isActiveMemoryRuntimeMemoryLine(line) ) { diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index e43baa1c..63be260e 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -113,16 +113,26 @@ function handleSessionBootstrapChatTail( turn && typeof turn === "object" && String(turn.user || "").trim() - && String(turn.jin || "").trim() )) .slice(-3); turns.forEach((turn, index) => { + const userText = + String(turn.user || "").trim(); + const jinText = + String(turn.jin || "").trim(); + appendChatMessage( "user", - String(turn.user || "") + userText ); + // Marker/action-only turns have no visible JIN answer. Keep the USER + // bubble above the divider, but never manufacture an empty BR bubble. + if (!jinText) { + return; + } + const messageId = `bootstrap-tail-${String( data.source_session_id @@ -147,7 +157,7 @@ function handleSessionBootstrapChatTail( appendStreamChunk( messageId, - String(turn.jin || "") + jinText ); finishStreamMessage( messageId, @@ -338,6 +348,24 @@ function handleAgentRuntimeStart(data) { function handleAgentRuntimeEnd(data) { + const runtimeSession = + window.JinRuntime + && window.JinRuntime.session; + + if ( + data + && data.session_snapshot + && runtimeSession + && typeof runtimeSession.persistLiveSessionCheckpoint === "function" + ) { + runtimeSession.persistLiveSessionCheckpoint({ + session_snapshot: data.session_snapshot, + completed_turn_commit: Boolean( + data.completed_turn_commit === true + ), + }); + } + if (data && data.retryable_response === true) { if (window.commitJinCompletedAnswerRetryCandidate) { window.commitJinCompletedAnswerRetryCandidate(); @@ -416,6 +444,24 @@ function handleMessageEnd( data ) { + const runtimeSession = + window.JinRuntime + && window.JinRuntime.session; + + if ( + data + && data.session_snapshot + && runtimeSession + && typeof runtimeSession.persistLiveSessionCheckpoint === "function" + ) { + runtimeSession.persistLiveSessionCheckpoint({ + session_snapshot: data.session_snapshot, + completed_turn_commit: Boolean( + data.completed_turn_commit === true + ), + }); + } + clearDelayedMemoryContentFilter( data.message_id ); diff --git a/ui/templates/index.html b/ui/templates/index.html index 8866ed50..2048dbc7 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -505,12 +505,12 @@ - + - + @@ -528,7 +528,7 @@ - + @@ -536,7 +536,7 @@ - + diff --git a/utils/chat_log.py b/utils/chat_log.py index cbab7324..ebd89160 100644 --- a/utils/chat_log.py +++ b/utils/chat_log.py @@ -12,6 +12,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] CHAT_LOG_ROOT = PROJECT_ROOT / "logs" +CHAT_LOG_ANON_ROOT = PROJECT_ROOT / "logs_anon" CHAT_LOG_SESSION_ID_MAX_CHARS = 80 CHAT_LOG_SESSION_ID_RE = re.compile( r"[^a-zA-Z0-9_.-]" @@ -45,6 +46,39 @@ def _now() -> datetime: return datetime.now().astimezone() +def chat_log_root_for_context( + context, + *, + root: Path | str | None = None, +) -> Path: + + if root is not None: + return Path(root) + + return ( + CHAT_LOG_ANON_ROOT + if bool( + getattr( + context, + "runtime_anonymous_mode", + False, + ) + ) + else CHAT_LOG_ROOT + ) + + +def chat_log_root_for_mode( + anonymous_mode: bool, +) -> Path: + + return ( + CHAT_LOG_ANON_ROOT + if bool(anonymous_mode) + else CHAT_LOG_ROOT + ) + + def _clean_session_id( value, ) -> str: @@ -138,10 +172,9 @@ def _existing_session_chat_logs( root: Path | str | None = None, ) -> list[Path]: - root_path = Path( - root - if root is not None - else CHAT_LOG_ROOT + root_path = chat_log_root_for_context( + context, + root=root, ) session_id = _context_session_id( context @@ -315,10 +348,9 @@ def get_chat_log_path( ) timestamp = now or _now() - root_path = Path( - root - if root is not None - else CHAT_LOG_ROOT + root_path = chat_log_root_for_context( + context, + root=root, ) session_id = _context_session_id( context diff --git a/utils/session_restore.py b/utils/session_restore.py index 23d0e21c..08856528 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -10,7 +10,11 @@ SESSION_RESTORE_REASONING_CHAR_LIMIT, SESSION_RESTORE_REASONING_COUNT, ) -from utils.chat_log import CHAT_LOG_ROOT, _clean_session_id +from utils.chat_log import ( + CHAT_LOG_ROOT, + _clean_session_id, + chat_log_root_for_mode, +) from utils.actions import normalize_jin_position_dict, normalize_jin_speed_value @@ -306,6 +310,7 @@ def _build_recent_turns( turn = { "user": "", "jin": "", + "_jin_row_seen": False, } turns[turn_number] = turn ordered_turns.append((turn_number, turn)) @@ -321,6 +326,10 @@ def _build_recent_turns( if timestamp: turn["user_created_at"] = timestamp elif role in {"jin", "assistant", "brain", "service"}: + # A JIN row is appended only after runtime.run returns. Even when + # marker stripping leaves no visible answer text, that empty row + # is the durable commit marker for a real USER-only turn. + turn["_jin_row_seen"] = True turn["jin"] = text reasoning = _extract_reasoning_body( reasoning_by_turn_id.get( @@ -334,12 +343,17 @@ def _build_recent_turns( turn["jin_created_at"] = timestamp ordered_turns.sort(key=lambda item: item[0]) - complete_turns = [ - item - for _, item in ordered_turns - if item.get("user") and item.get("jin") - ] - return complete_turns[-RECENT_MESSAGES_MAX_PAIRS:] + committed_turns = [] + for _, item in ordered_turns: + if not item.get("user") or not item.get("_jin_row_seen"): + continue + committed_turns.append({ + key: value + for key, value in item.items() + if not key.startswith("_") + }) + + return committed_turns[-RECENT_MESSAGES_MAX_PAIRS:] def _read_reasoning(session_directory: Path, entries: list[dict]) -> dict[str, str]: @@ -1046,12 +1060,114 @@ def _parse_jin_size(value: str): } +def find_latest_completed_session_restore_payload( + *, + root: Path | str | None = None, + anonymous_mode: bool | None = None, +) -> dict | None: + """Return the newest raw-log session containing a committed user turn. + + A normal USER/JIN pair qualifies, and so does a USER followed by an empty + JIN row after an action-only response. A bare USER row does not qualify, so + bootstrap-only/in-flight sessions remain ignored. + """ + root_path = Path( + root + if root is not None + else chat_log_root_for_mode( + bool(anonymous_mode) + ) + ) + if not root_path.is_dir(): + return None + + date_directories = sorted( + ( + path + for path in root_path.iterdir() + if path.is_dir() + and re.fullmatch(r"\d{4}-\d{2}-\d{2}", path.name) + ), + key=lambda path: path.name, + reverse=True, + ) + + for date_directory in date_directories: + best_session_id = "" + best_turn_timestamp = 0.0 + + for session_directory in date_directory.iterdir(): + if not session_directory.is_dir(): + continue + + dialog_paths = sorted( + ( + path + for path in session_directory.glob("*.jsonl") + if path.is_file() + ), + key=lambda path: path.name, + ) + if not dialog_paths: + continue + + # Match build_archived_session_restore_payload: the last JSONL is + # the authoritative dialogue file for this runtime session. + entries = _load_dialog(dialog_paths[-1]) + recent_turns = _build_recent_turns(entries) + if not recent_turns: + continue + + latest_turn = recent_turns[-1] + try: + turn_timestamp = float( + latest_turn.get("jin_created_at", 0) + or latest_turn.get("user_created_at", 0) + or 0 + ) + except (TypeError, ValueError): + turn_timestamp = 0.0 + + if turn_timestamp <= best_turn_timestamp: + continue + + best_turn_timestamp = turn_timestamp + best_session_id = _clean_session_id( + session_directory.name + ) + + if not best_session_id: + continue + + payload = build_archived_session_restore_payload( + best_session_id, + root=root_path, + ) + if isinstance(payload, dict): + payload = dict(payload) + payload["latest_completed_turn_at"] = ( + best_turn_timestamp + ) + return payload + + return None + + def build_archived_session_restore_payload( session_id: str, *, root: Path | str | None = None, + anonymous_mode: bool | None = None, ) -> dict | None: - root_path = Path(root if root is not None else CHAT_LOG_ROOT) + root_path = Path( + root + if root is not None + else ( + chat_log_root_for_mode(bool(anonymous_mode)) + if anonymous_mode is not None + else CHAT_LOG_ROOT + ) + ) session_directory = _find_session_directory(session_id, root_path) if session_directory is None: return None @@ -1245,7 +1361,7 @@ def build_archived_session_restore_payload( reasoning_by_turn_id, ), "recent_turns": _build_recent_turns( - visible_entries, + entries, reasoning_by_turn_id, ), "previous_reasoning": latest_reasoning, diff --git a/utils/stream_handler.py b/utils/stream_handler.py index 48709db7..8734215a 100644 --- a/utils/stream_handler.py +++ b/utils/stream_handler.py @@ -376,6 +376,7 @@ async def finish( self, *, emit: bool = True, + end_payload_builder=None, ): await self.flush_validator_tail( @@ -385,9 +386,21 @@ async def finish( if not emit: return - await self.websocket.send_json({ + payload = { "type": "message_end", "message_id": ( self.message_id ), - }) + } + + if callable(end_payload_builder): + try: + extra_payload = end_payload_builder() + except Exception: + extra_payload = None + if isinstance(extra_payload, dict): + payload.update(extra_payload) + + await self.websocket.send_json( + payload + ) diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 1935bd6f..81e90826 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -1532,6 +1532,10 @@ def get_or_create_connection_context( clients=websocket.app.state.clients, session_id=client_id, ) + configure_runtime_anonymous_mode( + context, + anonymous_mode_enabled, + ) resume_chat_log_session( context ) @@ -1541,10 +1545,6 @@ def get_or_create_connection_context( hydrate_attached_files_from_store( context ) - configure_runtime_anonymous_mode( - context, - anonymous_mode_enabled, - ) restore_pending_l1_update( context ) @@ -2319,8 +2319,31 @@ def _bootstrap_iso_timestamp(value) -> float: return 0.0 +def _bootstrap_recent_turns_tail_timestamp(value) -> float: + if not isinstance(value, list): + return 0.0 + + newest = 0.0 + for turn in value: + if not isinstance(turn, dict): + continue + for field in ( + "jin_created_at", + "user_created_at", + ): + raw = turn.get(field) + try: + timestamp = float(raw or 0) + except (TypeError, ValueError): + timestamp = _bootstrap_iso_timestamp(raw) + newest = max(newest, timestamp) + return newest + + def enrich_session_bootstrap_from_archive( message_data: dict, + *, + anonymous_mode: bool | None = None, ) -> dict: if not isinstance(message_data, dict): @@ -2345,13 +2368,66 @@ def enrich_session_bootstrap_from_archive( try: from utils.session_restore import ( build_archived_session_restore_payload, + find_latest_completed_session_restore_payload, ) archived = build_archived_session_restore_payload( - source_session_id + source_session_id, + anonymous_mode=anonymous_mode, + ) + latest_completed_archive = ( + find_latest_completed_session_restore_payload( + anonymous_mode=anonymous_mode, + ) ) except Exception: archived = None + latest_completed_archive = None + + # source_session_id comes from browser persistence and can be stale. The + # raw JSONL dialogue is authoritative: if another session has a strictly + # newer COMPLETE USER/JIN turn, continue from that session instead. Empty + # bootstrap-only sessions never qualify for this override. + if isinstance(latest_completed_archive, dict): + latest_source_session_id = clean_bootstrap_memory( + latest_completed_archive.get( + "source_session_id", + "", + ), + limit=80, + ) + latest_dialog_tail_timestamp = ( + _bootstrap_recent_turns_tail_timestamp( + latest_completed_archive.get( + "recent_turns", + [], + ) + ) + ) + requested_dialog_tail_timestamp = ( + _bootstrap_recent_turns_tail_timestamp( + archived.get("recent_turns", []) + if isinstance(archived, dict) + else [] + ) + ) + browser_dialog_tail_timestamp = ( + _bootstrap_recent_turns_tail_timestamp( + message_data.get("recent_turns", []) + ) + ) + + if ( + latest_source_session_id + and latest_source_session_id != source_session_id + and latest_dialog_tail_timestamp + > max( + requested_dialog_tail_timestamp, + browser_dialog_tail_timestamp, + ) + ): + archived = latest_completed_archive + source_session_id = latest_source_session_id if not isinstance(archived, dict): return message_data @@ -2389,22 +2465,78 @@ def enrich_session_bootstrap_from_archive( int(archive_tail_timestamp) >= int(checkpoint_at) ) + # Dialogue freshness is independent from runtime-snapshot freshness. A fresh + # tab clone advances saved_at even when it contains no newer conversation, + # so comparing a chat-log tail to saved_at can pin bootstrap to an old turn + # forever. Compare chat tail to chat tail instead. + browser_recent_turns = message_data.get("recent_turns", []) + archived_recent_turns = archived.get("recent_turns", []) + browser_dialog_tail_timestamp = ( + _bootstrap_recent_turns_tail_timestamp(browser_recent_turns) + ) + archive_dialog_tail_timestamp = ( + _bootstrap_recent_turns_tail_timestamp(archived_recent_turns) + ) - # Never mix a newer browser save with an older raw log tail. - if archive_reaches_saved_checkpoint: + if not archive_dialog_tail_timestamp and archive_tail_at: + archive_dialog_tail_timestamp = _bootstrap_iso_timestamp( + archive_tail_at + ) + + archive_reaches_dialog_checkpoint = False + browser_has_recent_turns = bool( + isinstance(browser_recent_turns, list) + and browser_recent_turns + ) + archive_has_recent_turns = bool( + isinstance(archived_recent_turns, list) + and archived_recent_turns + ) + + if ( + not browser_has_recent_turns + and ( + archive_has_recent_turns + or archive_dialog_tail_timestamp + or clean_bootstrap_memory( + archived.get("dialog_context", "") + ) + ) + ): + # Legacy browser checkpoints may have no per-turn timestamps at all. + # If the browser has no dialogue tail, the source session's raw log is + # the only authoritative dialogue source regardless of runtime saved_at. + archive_reaches_dialog_checkpoint = True + elif archive_dialog_tail_timestamp and browser_dialog_tail_timestamp: + archive_reaches_dialog_checkpoint = ( + int(archive_dialog_tail_timestamp) + >= int(browser_dialog_tail_timestamp) + ) + + if archive_reaches_dialog_checkpoint: for field in ( "dialog_context", "recent_turns", "previous_reasoning", "restore_reasoning_dump", "restore_l4_fact_ids", - "restore_delayed_memory_metadata", - "restore_attached_file_metadata", - "session_actions", "runtime_turn_counter", "turn_number", "user_message_count", "assistant_message_count", + ): + value = archived.get(field) + if value not in (None, "", [], {}): + enriched[field] = value + + # Runtime/resource checkpoint freshness still uses saved_at. Those fields + # are not safe to replace from an older archive just because its dialogue + # tail is newer than the browser's copied chat tail. + if archive_reaches_saved_checkpoint: + for field in ( + "restore_delayed_memory_metadata", + "restore_attached_file_metadata", + "session_actions", "attached_file_ids", ): value = archived.get(field) @@ -2521,7 +2653,7 @@ def build_session_bootstrap_chat_tail( if not isinstance(turns, list): return [] - complete_turns = [] + committed_turns = [] for turn in turns: if not isinstance(turn, dict): continue @@ -2541,7 +2673,10 @@ def build_session_bootstrap_chat_tail( turn.get("jin", ""), limit=12000, ) - if not user_text or not jin_text: + # runtime_recent_turns contains committed turns. An action-only + # completion can legitimately have no visible JIN text, but its USER + # bubble still belongs to the predecessor chat tail. + if not user_text: continue item = { @@ -2572,9 +2707,9 @@ def build_session_bootstrap_chat_tail( if created_at > 0: item[key] = created_at - complete_turns.append(item) + committed_turns.append(item) - return complete_turns[-RECENT_MESSAGES_MAX_PAIRS:] + return committed_turns[-RECENT_MESSAGES_MAX_PAIRS:] def apply_session_bootstrap( @@ -2583,7 +2718,14 @@ def apply_session_bootstrap( ) -> bool: message_data = enrich_session_bootstrap_from_archive( - message_data + message_data, + anonymous_mode=bool( + getattr( + context, + "runtime_anonymous_mode", + False, + ) + ), ) apply_active_memory_records( diff --git a/websocket/messages.py b/websocket/messages.py index 0665e441..025080f4 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -24,7 +24,10 @@ schedule_interrupted_runtime_memory_update, schedule_runtime_memory_update, ) -from runtime.L1_memory_utils import record_runtime_memory_reasoning_quotes +from runtime.L1_memory_utils import ( + build_runtime_session_checkpoint, + record_runtime_memory_reasoning_quotes, +) from runtime.metabolism import ( append_metabolism_turn, cancel_metabolism_update, @@ -1332,46 +1335,15 @@ async def process_message( ): return - await emit_session_actions_update( - context, - current_sequence=False, - ) - - await logger.log( - "[FLOW TELEMETRY]", - format_token_usage_summary( - context - ), - ) - assistant_message = ( - state.brain_response - or context.runtime_turn_assistant_response - ) - retryable_response = bool( - not is_action_guard_retry - and not is_session_restore_resume - and not getattr( - context, - "runtime_turn_interrupted", - False, - ) - and str(assistant_message or "").strip() - ) - - if retryable_response: - context.runtime_last_retryable_request = deepcopy( - retry_source_candidate - ) - - await logger.log_system( - "[WS] agent runtime end" + state.brain_response + or context.runtime_turn_assistant_response ) - await websocket.send_json({ - "type": "agent_runtime_end", - "retryable_response": retryable_response, - }) + # The last visible message_end already persisted a browser-side preview + # of this turn. Commit the same completed turn to the raw archive and + # runtime history before agent_runtime_end so both bootstrap sources + # converge immediately. if not is_action_guard_retry: try: if is_user_retry: @@ -1426,6 +1398,71 @@ async def process_message( ), feedback=metabolism_turn_feedback, ) + + if not is_action_guard_retry and not is_user_retry: + context.assistant_message_count += 1 + context.turn_number += 1 + + completed_session_snapshot = ( + build_runtime_session_checkpoint(context) + ) + + await emit_session_actions_update( + context, + current_sequence=False, + ) + + await logger.log( + "[FLOW TELEMETRY]", + format_token_usage_summary( + context + ), + ) + + retryable_response = bool( + not is_action_guard_retry + and not is_session_restore_resume + and not getattr( + context, + "runtime_turn_interrupted", + False, + ) + and str(assistant_message or "").strip() + ) + + if retryable_response: + context.runtime_last_retryable_request = deepcopy( + retry_source_candidate + ) + + completed_turn_commit = bool( + not is_action_guard_retry + and not is_session_restore_resume + and not getattr( + context, + "runtime_turn_interrupted", + False, + ) + and not getattr( + context, + "runtime_turn_discard_requested", + False, + ) + and str(user_text or "").strip() + ) + + await logger.log_system( + "[WS] agent runtime end" + ) + + await websocket.send_json({ + "type": "agent_runtime_end", + "retryable_response": retryable_response, + "session_snapshot": completed_session_snapshot, + "completed_turn_commit": completed_turn_commit, + }) + + if not is_action_guard_retry: # Runtime actions/validators leave a small causal trace immediately. # The slower SERVICE integration still runs afterwards and may # correct the reflex from the full turn snapshot. @@ -1489,11 +1526,6 @@ async def process_message( assistant_message=assistant_message, ) - if not is_action_guard_retry and not is_user_retry: - context.assistant_message_count += 1 - if not is_action_guard_retry and not is_user_retry: - context.turn_number += 1 - # Background fact-checking is intentionally not armed here. # Fact-checking runs only from the explicit UI request path. From 21f88adb1307bb8be9e79c62d25fbdd636f2e68b Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:41:24 +0300 Subject: [PATCH 097/168] fix: color bootstrap - fix for bootstrap color action restore --- tests/runtime_actions/test_jin_color.py | 4 + .../test_jin_color_bootstrap_persistence.py | 56 ++++ tests/test_archived_session_restore.py | 126 +++++++++ .../test_bootstrap_color_action_regression.py | 142 ++++++++++ ...test_bootstrap_completed_turn_atomicity.py | 9 +- ...st_jin_color_transition_client_contract.py | 146 ++++++++++ tests/test_live_session_checkpoint.py | 63 +++++ ...ssion_bootstrap_lineage_client_contract.py | 66 +++++ ui/static/css/runtime-avatar.css | 9 +- ui/static/js/logger/logger.js | 162 +++++++++-- ui/static/js/runtime/runtime-avatar.js | 88 +++++- ui/static/js/runtime/runtime-session.js | 265 ++++++++++++++---- ui/static/js/session-restore.js | 135 ++++++++- ui/static/js/socket/event-handlers.js | 51 +++- ui/static/js/socket/runtime-actions.js | 14 +- ui/templates/index.html | 12 +- utils/actions/dispatcher.py | 1 + utils/actions/jin_visual_sequence_actions.py | 44 ++- utils/session_actions_history.py | 2 + utils/session_restore.py | 156 ++++++++++- websocket/__init__.py | 1 + websocket/bootstrap.py | 170 ++++++++++- 22 files changed, 1600 insertions(+), 122 deletions(-) create mode 100644 tests/runtime_actions/test_jin_color_bootstrap_persistence.py create mode 100644 tests/test_bootstrap_color_action_regression.py create mode 100644 tests/test_jin_color_transition_client_contract.py diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index 22f2932c..52771449 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -305,6 +305,10 @@ async def run_case(): "#ff00aa", ], ) + self.assertEqual( + context.jin_color, + "#ff00aa", + ) asyncio.run(run_case()) diff --git a/tests/runtime_actions/test_jin_color_bootstrap_persistence.py b/tests/runtime_actions/test_jin_color_bootstrap_persistence.py new file mode 100644 index 00000000..b921e43b --- /dev/null +++ b/tests/runtime_actions/test_jin_color_bootstrap_persistence.py @@ -0,0 +1,56 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import patch + +from utils.actions import RuntimeActionCall +from utils.actions.jin_visual_sequence_actions import ( + emit_jin_visual_sequences, +) + + +class _Emitter: + def __init__(self): + self.events = [] + + async def emit(self, event): + self.events.append(event) + + +def test_jin_color_action_is_persisted_for_log_bootstrap(): + async def run_case(): + emitter = _Emitter() + context = SimpleNamespace( + emitter=emitter, + runtime_current_turn_id="turn-color-persist", + ) + action = RuntimeActionCall( + name="JIN_COLOR", + payload="#00ff00", + ) + + async def log_runtime(_message): + return None + + with patch( + "utils.actions.jin_visual_sequence_actions.append_chat_runtime_event" + ) as append_event: + await emit_jin_visual_sequences( + context, + [action], + action_display_ids={id(action): "color-action"}, + log_runtime=log_runtime, + with_action_context=lambda event: event, + ) + + assert len(emitter.events) == 1 + append_event.assert_called_once() + kwargs = append_event.call_args.kwargs + assert kwargs["event"] == "runtime_action_request" + assert kwargs["payload"]["action"] == "JIN_COLOR" + assert kwargs["payload"]["color"] == "#00ff00" + assert ( + kwargs["payload"]["session_action"]["parts"][0]["colors"] + == ["#00ff00"] + ) + + asyncio.run(run_case()) diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index f7da7c0b..d495a719 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -286,6 +286,51 @@ def test_payload_restores_dialog_reasoning_runtime_and_actions(self): self.assertEqual(payload["tool_results"][0]["kind"], "search") self.assertIn("People recommend cold noir music.", payload["tool_results"][0]["result"]) + def test_runtime_color_event_restores_color_and_structured_action(self): + root, session_id = self._build_fixture() + dialog_path = ( + root + / "2026-08-16" + / session_id + / "222828.jsonl" + ) + color_event = { + "ts": "2026-08-16T23:09:38+03:00", + "turn": 5, + "turn_id": "turn_000005", + "session_id": session_id, + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "color": "#ff0000", + "payload": "#ff0000", + "created_at": 1776370178.5, + }, + } + with dialog_path.open("a", encoding="utf-8") as handle: + handle.write( + "\n" + json.dumps(color_event, ensure_ascii=False) + ) + + payload = build_archived_session_restore_payload( + session_id, + root=root, + ) + + self.assertEqual(payload["current_jin_color"], "#ff0000") + color_action = payload["session_actions"][-1] + self.assertEqual(color_action["text"], "JIN_COLOR") + self.assertEqual( + color_action["parts"][0]["colors"], + ["#ff0000"], + ) + self.assertEqual( + color_action["runtime_turn_id"], + "turn_000005", + ) + def test_restore_payload_uses_primary_context_and_ignores_bootstrap_snapshot(self): root, session_id = self._build_fixture() @@ -735,6 +780,87 @@ def test_browser_checkpoint_explicit_empty_tool_results_stays_empty(self): self.assertIn("tool_results", enriched) self.assertEqual(enriched["tool_results"], []) + def test_common_browser_checkpoint_color_wins_over_action_history(self): + archived = { + "source_session_id": "archive-session", + "archive_tail_at": "2026-08-23T10:30:00+03:00", + "session_actions": [{ + "text": "JIN_COLOR", + "created_at": 10.0, + "parts": [{ + "text": "JIN_COLOR", + "colors": ["#1f4f8f"], + }], + }], + "current_jin_color": "#1f4f8f", + } + browser_color_action = { + "text": "JIN_COLOR", + "created_at": 20.0, + "runtime_turn_id": "turn_000034", + "parts": [{ + "text": "JIN_COLOR", + "colors": ["#ff0000"], + }], + } + + with ( + patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=archived, + ), + patch( + "utils.session_restore.find_latest_completed_session_restore_payload", + return_value=None, + ), + ): + enriched = enrich_session_bootstrap_from_archive({ + "type": "session_bootstrap", + "source_session_id": "archive-session", + "saved_at": "2026-08-23T10:29:00+03:00", + "session_actions": [browser_color_action], + "current_jin_color": "#00ff00", + }) + + self.assertEqual(enriched["current_jin_color"], "#00ff00") + self.assertEqual( + enriched["session_actions"][-1]["parts"][0]["colors"], + ["#ff0000"], + ) + + def test_action_history_recovers_color_when_checkpoint_has_none(self): + archived = { + "source_session_id": "archive-session", + "archive_tail_at": "2026-08-23T10:30:00+03:00", + "session_actions": [{ + "text": "JIN_COLOR", + "created_at": 20.0, + "parts": [{ + "text": "JIN_COLOR", + "colors": ["#ff0000"], + }], + }], + "current_jin_color": "#1f4f8f", + } + + with ( + patch( + "utils.session_restore.build_archived_session_restore_payload", + return_value=archived, + ), + patch( + "utils.session_restore.find_latest_completed_session_restore_payload", + return_value=None, + ), + ): + enriched = enrich_session_bootstrap_from_archive({ + "type": "session_bootstrap", + "source_session_id": "archive-session", + "saved_at": "2026-08-23T10:29:00+03:00", + }) + + self.assertEqual(enriched["current_jin_color"], "#ff0000") + def test_empty_memory_collections_keep_archive_fallback_semantics(self): archived = { "source_session_id": "archive-session", diff --git a/tests/test_bootstrap_color_action_regression.py b/tests/test_bootstrap_color_action_regression.py new file mode 100644 index 00000000..286f3ce9 --- /dev/null +++ b/tests/test_bootstrap_color_action_regression.py @@ -0,0 +1,142 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from utils.session_restore import build_archived_session_restore_payload + + +ROOT = Path(__file__).resolve().parents[1] + + +class BootstrapColorActionRegressionTests(unittest.TestCase): + + def test_archive_recovers_color_actions_from_direct_predecessor_logs(self): + root = Path(tempfile.mkdtemp()) + previous_session_id = "previous-blue-session" + current_session_id = "current-red-session" + + previous_dir = root / "2026-08-25" / previous_session_id + current_dir = root / "2026-08-25" / current_session_id + previous_dir.mkdir(parents=True) + current_dir.mkdir(parents=True) + + previous_entries = [ + { + "ts": "2026-08-25T13:04:05+03:00", + "turn": 36, + "turn_id": "turn_000036", + "session_id": previous_session_id, + "role": "user", + "text": "blue", + }, + { + "ts": "2026-08-25T13:04:58+03:00", + "turn": 36, + "turn_id": "turn_000036", + "session_id": previous_session_id, + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "color": "#0000ff", + "created_at": 100.0, + }, + }, + { + "ts": "2026-08-25T13:04:58+03:00", + "turn": 36, + "turn_id": "turn_000036", + "session_id": previous_session_id, + "role": "jin", + "text": "Blue.", + }, + ] + current_entries = [ + { + "ts": "2026-08-25T13:23:28+03:00", + "turn": 38, + "turn_id": "turn_000038", + "session_id": current_session_id, + "role": "user", + "text": "red", + }, + { + "ts": "2026-08-25T13:24:21+03:00", + "turn": 38, + "turn_id": "turn_000038", + "session_id": current_session_id, + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "color": "#ff0000", + "created_at": 200.0, + }, + }, + { + "ts": "2026-08-25T13:24:21+03:00", + "turn": 38, + "turn_id": "turn_000038", + "session_id": current_session_id, + "role": "jin", + "text": "Red.", + }, + ] + + (previous_dir / "session.jsonl").write_text( + "\n".join(json.dumps(item) for item in previous_entries), + encoding="utf-8", + ) + (previous_dir / "session.txt").write_text("", encoding="utf-8") + (current_dir / "session.jsonl").write_text( + "\n".join(json.dumps(item) for item in current_entries), + encoding="utf-8", + ) + (current_dir / "session.txt").write_text( + '\n' + "", + encoding="utf-8", + ) + + payload = build_archived_session_restore_payload( + current_session_id, + root=root, + ) + + self.assertEqual(payload["current_jin_color"], "#ff0000") + self.assertEqual( + [ + item["parts"][0]["colors"][0] + for item in payload["session_actions"] + ], + ["#0000ff", "#ff0000"], + ) + + def test_server_actions_remain_ui_color_fallback(self): + websocket_source = (ROOT / "websocket" / "__init__.py").read_text( + encoding="utf-8" + ) + handler_source = ( + ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" + ).read_text(encoding="utf-8") + session_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-session.js" + ).read_text(encoding="utf-8") + + self.assertIn("bootstrap_restore=True", websocket_source) + self.assertIn("data.bootstrap_restore === true", handler_source) + self.assertIn("runtimeSession.resolveBootstrapJinColor", handler_source) + self.assertIn("runtimeSession.applyBootstrapSceneTintShift", handler_source) + self.assertIn("resolvePersistedJinColor()", session_source) + self.assertIn( + 'sceneMain.style.removeProperty("background-color")', + session_source, + ) + self.assertNotIn("previousMain.transition", session_source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_bootstrap_completed_turn_atomicity.py b/tests/test_bootstrap_completed_turn_atomicity.py index 823ecfff..3aa2d632 100644 --- a/tests/test_bootstrap_completed_turn_atomicity.py +++ b/tests/test_bootstrap_completed_turn_atomicity.py @@ -83,8 +83,13 @@ def test_archive_dialogue_freshness_is_not_blocked_by_newer_runtime_saved_at(): assert enriched["recent_turns"] == archived["recent_turns"] assert enriched["previous_reasoning"] == "new reasoning" - # Runtime/resource state still obeys runtime checkpoint freshness. - assert enriched["session_actions"] == [{"id": "browser-action"}] + # Resources still obey runtime checkpoint freshness. Actions carry their + # own timestamps/ids, so an archive action cannot disappear merely because + # saved_at advanced after a harmless browser write. + assert enriched["session_actions"] == [ + {"id": "browser-action"}, + {"id": "archive-action"}, + ] def test_agent_runtime_end_commits_user_only_completed_turn_checkpoint(): diff --git a/tests/test_jin_color_transition_client_contract.py b/tests/test_jin_color_transition_client_contract.py new file mode 100644 index 00000000..44f22903 --- /dev/null +++ b/tests/test_jin_color_transition_client_contract.py @@ -0,0 +1,146 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_ACTIONS_JS = ( + ROOT / "ui" / "static" / "js" / "socket" / "runtime-actions.js" +) +AVATAR_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" +AVATAR_CSS = ROOT / "ui" / "static" / "css" / "runtime-avatar.css" +LOGGER_JS = ROOT / "ui" / "static" / "js" / "logger" / "logger.js" +RUNTIME_SESSION_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-session.js" +) +RUNTIME_STORAGE_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-storage.js" +) +WEBSOCKET_BOOTSTRAP_PY = ROOT / "websocket" / "bootstrap.py" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class JinColorTransitionClientContractTests(unittest.TestCase): + + def test_live_jin_color_uses_one_two_second_avatar_and_tint_transition(self): + actions_source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") + avatar_source = AVATAR_JS.read_text(encoding="utf-8") + avatar_css = AVATAR_CSS.read_text(encoding="utf-8") + + self.assertIn("const JIN_VISUAL_SEQUENCE_COLOR_MS = 2000;", actions_source) + self.assertEqual( + actions_source.count( + "transitionDurationMs: JIN_VISUAL_SEQUENCE_COLOR_MS" + ), + 2, + ) + self.assertIn( + '"--scene-jin-tint-transition-duration",\n durationValue', + avatar_source, + ) + self.assertIn( + '"--jin-avatar-center-color-transition-duration",\n durationValue', + avatar_source, + ) + self.assertIn( + "Math.max(CENTER_COLOR_STEP_MS, transitionDurationMs)", + avatar_source, + ) + self.assertIn( + "var(--jin-avatar-center-color-transition-duration, 0.16s)", + avatar_css, + ) + + def test_every_center_color_change_defaults_to_two_seconds(self): + avatar_source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertIn( + "const DEFAULT_CENTER_COLOR_TRANSITION_MS = 2000;", + avatar_source, + ) + self.assertIn( + ": DEFAULT_CENTER_COLOR_TRANSITION_MS;", + avatar_source, + ) + + def test_common_checkpoint_color_is_immediate_and_boots_first(self): + logger_source = LOGGER_JS.read_text(encoding="utf-8") + session_source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + storage_source = RUNTIME_STORAGE_JS.read_text(encoding="utf-8") + bootstrap_source = WEBSOCKET_BOOTSTRAP_PY.read_text(encoding="utf-8") + persist_start = logger_source.index("function persistRoomStateNow(") + persist_end = logger_source.index( + "function scheduleRoomStatePersist()", + persist_start, + ) + persist_block = logger_source[persist_start:persist_end] + stored_start = logger_source.index("function getStoredRoomState()") + stored_end = logger_source.index( + "function enableRoomStatePersistence(", + stored_start, + ) + stored_block = logger_source[stored_start:stored_end] + init_start = logger_source.index("function initRoomStatePersistence()") + init_end = logger_source.index( + "function clearConsoleStreamDetachTimer()", + init_start, + ) + init_block = logger_source[init_start:init_end] + + self.assertIn("current_jin_color: color", persist_block) + self.assertIn("previousSnapshot.room_state", persist_block) + self.assertIn("...checkpoint", persist_block) + self.assertNotIn("saved_at:", persist_block) + self.assertLess( + persist_block.index("if (colorOnly)"), + persist_block.index("const currentSessionId"), + ) + + self.assertIn("snapshot.current_jin_color", stored_block) + self.assertIn("roomState.avatar.color = color;", stored_block) + self.assertNotIn("delete roomState.avatar.color;", stored_block) + self.assertNotIn("resolveBootstrapRoomState", stored_block) + self.assertIn("applyBootstrapSceneTintShift", init_block) + self.assertIn( + "persistRoomStateNow({ colorOnly: true });", + init_block, + ) + self.assertIn("enableRoomStatePersistence(false);", init_block) + self.assertNotIn("latestJinColor", storage_source) + + bootstrap_start = bootstrap_source.index( + "browser_color = normalize_jin_color_payload(" + ) + bootstrap_end = bootstrap_source.index( + "# Browser persistence is the exact checkpoint when available.", + bootstrap_start, + ) + bootstrap_block = bootstrap_source[bootstrap_start:bootstrap_end] + + self.assertIn("if browser_color:", bootstrap_block) + self.assertIn("else:", bootstrap_block) + self.assertIn("_bootstrap_latest_session_action_color", bootstrap_block) + + apply_start = session_source.index( + "function applyPersistedSessionBootstrap(bootstrap)" + ) + apply_end = session_source.index( + "function getPersistedSessionBootstrap()", + apply_start, + ) + apply_block = session_source[apply_start:apply_end] + + self.assertIn("delete localRoomState.avatar.color;", apply_block) + self.assertNotIn("applyBootstrapSceneTintShift(", apply_block) + + def test_changed_assets_have_matching_cache_bumps(self): + index_source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertEqual(index_source.count("jin-color-transition=2"), 2) + self.assertIn("runtime-session.js?v=", index_source) + self.assertIn("&room-state=7&", index_source) + self.assertIn("logger.js?v=", index_source) + self.assertIn("&room-state=5&", index_source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index 45a7b155..97fff29e 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -22,6 +22,22 @@ / "runtime" / "runtime.js" ) +LOGGER_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "logger" + / "logger.js" +) +EVENT_HANDLERS_JS = ( + ROOT + / "ui" + / "static" + / "js" + / "socket" + / "event-handlers.js" +) class LiveSessionCheckpointTests(unittest.TestCase): @@ -161,5 +177,52 @@ def test_duplicate_l1_still_refreshes_live_session_checkpoint(self): self.assertIn(duplicate, source) + def test_room_state_does_not_repaint_predecessor_checkpoint(self): + source = LOGGER_JS.read_text(encoding="utf-8") + start = source.index("function persistRoomStateNow(") + end = source.index("function scheduleRoomStatePersist()", start) + block = source[start:end] + + self.assertIn("storage.getCurrentRuntimeSessionId", block) + self.assertIn("currentSessionId !== checkpointSessionId", block) + self.assertIn("return false", block) + + def test_color_updates_the_same_checkpoint_before_owner_switch(self): + source = LOGGER_JS.read_text(encoding="utf-8") + start = source.index("function persistRoomStateNow(") + end = source.index("function scheduleRoomStatePersist()", start) + block = source[start:end] + + self.assertIn("current_jin_color: color", block) + self.assertIn("previousSnapshot.room_state", block) + self.assertIn("...checkpoint", block) + self.assertNotIn("saved_at:", block) + self.assertLess( + block.index("if (colorOnly)"), + block.index("currentSessionId !== checkpointSessionId"), + ) + + def test_completed_turn_repersists_room_state_after_owner_switch(self): + handlers = EVENT_HANDLERS_JS.read_text(encoding="utf-8") + start = handlers.index("function handleAgentRuntimeEnd(data)") + end = handlers.index("function handleMessageStart(", start) + block = handlers[start:end] + + self.assertIn("data.completed_turn_commit === true", block) + self.assertIn("const persisted = runtimeSession.persistLiveSessionCheckpoint", block) + self.assertIn("window.JinPanels.persistRoomStateNow()", block) + + def test_restore_merges_checkpoint_session_actions(self): + source = (ROOT / "ui" / "static" / "js" / "session-restore.js").read_text(encoding="utf-8") + start = source.index("function mergeLatestVisualCheckpoint(") + end = source.index("function restoreVisualState(", start) + block = source[start:end] + + self.assertIn("Array.isArray(snapshot.session_actions)", block) + self.assertIn("merged.session_actions = snapshot.session_actions", block) + self.assertNotIn("rejectCheckpointColor", block) + + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_session_bootstrap_lineage_client_contract.py b/tests/test_session_bootstrap_lineage_client_contract.py index f476bb7b..3daa7ee2 100644 --- a/tests/test_session_bootstrap_lineage_client_contract.py +++ b/tests/test_session_bootstrap_lineage_client_contract.py @@ -60,6 +60,72 @@ def test_boot_materializes_current_session_as_next_direct_predecessor(self): self.assertIn("bootstrap.source_session_id", block) self.assertIn("bootstrap.runtime_memory", block) + def test_boot_color_prefers_common_checkpoint_then_action_fallback(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + resolver_start = source.index("function resolveBootstrapJinColor(") + resolver_end = source.index( + "function applyBootstrapSceneTintShift(", + resolver_start, + ) + resolver = source[resolver_start:resolver_end] + + self.assertLess( + resolver.index("resolvePersistedJinColor()"), + resolver.index("source.session_actions"), + ) + self.assertLess( + resolver.index("source.session_actions"), + resolver.index("source.current_jin_color"), + ) + self.assertIn("readLatestSavedSessionSnapshot()", source) + self.assertIn('toUpperCase() !== "JIN_COLOR"', resolver) + self.assertIn("part.colors", resolver) + + apply_start = source.index( + "function applyPersistedSessionBootstrap(bootstrap)" + ) + apply_end = source.index( + "function getPersistedSessionBootstrap()", + apply_start, + ) + apply_block = source[apply_start:apply_end] + self.assertIn("resolveBootstrapRoomState(bootstrap)", apply_block) + self.assertIn("delete localRoomState.avatar.color;", apply_block) + self.assertNotIn("applyBootstrapSceneTintShift(", apply_block) + + def test_early_room_restore_uses_common_checkpoint_color(self): + runtime_source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + helper_start = runtime_source.index( + "function resolveBootstrapRoomState(" + ) + helper_end = runtime_source.index( + "function applyBootstrapSceneTintShift(", + helper_start, + ) + helper = runtime_source[helper_start:helper_end] + + self.assertIn("resolveBootstrapJinColor(source)", helper) + self.assertIn("color: restoredColor", helper) + self.assertIn( + "session.resolveBootstrapRoomState = resolveBootstrapRoomState", + runtime_source, + ) + + logger_source = ( + ROOT / "ui" / "static" / "js" / "logger" / "logger.js" + ).read_text(encoding="utf-8") + stored_start = logger_source.index("function getStoredRoomState()") + stored_end = logger_source.index( + "function enableRoomStatePersistence(", + stored_start, + ) + stored_block = logger_source[stored_start:stored_end] + + self.assertNotIn("resolveBootstrapRoomState", stored_block) + self.assertIn("snapshot.current_jin_color", stored_block) + self.assertIn("roomState.avatar.color = color;", stored_block) + self.assertNotIn("delete roomState.avatar.color;", stored_block) + def test_live_bootstrap_preserves_tool_results(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function normalizeLiveSessionSnapshot(") diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index cad14fc4..5a90bcc5 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -916,13 +916,14 @@ .jin-avatar-center-core, .jin-avatar-center-point { transition: - fill 0.16s ease-out, - fill-opacity 0.16s ease-out, - opacity 0.16s ease-out; + fill var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out, + fill-opacity var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out, + opacity var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out; } .jin-avatar-center-glow-stop { - transition: stop-color 0.16s ease-out; + transition: + stop-color var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out; } .jin-avatar-orbit-entry { diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index e9f79fd2..5233b9e7 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -4024,11 +4024,16 @@ function applyRoomState(roomState, options = {}) { return true; } -function persistRoomStateNow() { +function persistRoomStateNow(options = {}) { + const colorOnly = options.colorOnly === true; + if ( - !roomStatePersistenceEnabled - || applyingRoomState - || roomStateRestoreInProgress + !colorOnly + && ( + !roomStatePersistenceEnabled + || applyingRoomState + || roomStateRestoreInProgress + ) ) { return false; } @@ -4059,6 +4064,79 @@ function persistRoomStateNow() { return false; } + if (colorOnly) { + const avatarApi = + window.JinRuntime + && window.JinRuntime.avatar; + const color = + String( + avatarApi + && typeof avatarApi.getCenterColor === "function" + ? avatarApi.getCenterColor() + : "" + ) + .trim() + .toLowerCase(); + + if (!/^#[0-9a-f]{6}$/.test(color)) { + return false; + } + + const previousSnapshot = + checkpoint.session_snapshot; + const nextSnapshot = { + ...previousSnapshot, + current_jin_color: color, + }; + + if (isRoomStateObject(previousSnapshot.room_state)) { + const previousRoomState = + previousSnapshot.room_state; + const previousAvatar = + isRoomStateObject(previousRoomState.avatar) + ? previousRoomState.avatar + : {}; + + nextSnapshot.room_state = { + ...previousRoomState, + avatar: { + ...previousAvatar, + color, + }, + }; + } + + // JIN_COLOR is a field-local mutation of this same checkpoint. Do not + // advance saved_at or replace its owner, lineage, actions or room data. + storage.writeLatestSavedSessionSnapshot({ + ...checkpoint, + session_snapshot: nextSnapshot, + }); + + return true; + } + + const currentSessionId = + typeof storage.getCurrentRuntimeSessionId === "function" + ? String( + storage.getCurrentRuntimeSessionId() + || "" + ).trim() + : ""; + const checkpointSessionId = + String(checkpoint.session_id || "").trim(); + + // The room state is part of the same bootstrap checkpoint as its action + // history. A newly opened/background tab must not repaint the predecessor + // checkpoint before it has completed a visible turn and taken ownership. + if ( + currentSessionId + && checkpointSessionId + && currentSessionId !== checkpointSessionId + ) { + return false; + } + const previousRoomState = isRoomStateObject( checkpoint.session_snapshot.room_state @@ -4165,7 +4243,34 @@ function getStoredRoomState() { const snapshot = checkpoint.session_snapshot; if (isRoomStateObject(snapshot.room_state)) { - return snapshot.room_state; + const roomState = { + ...snapshot.room_state, + }; + const color = + String( + snapshot.current_jin_color + || ( + roomState.avatar + && roomState.avatar.color + ) + || "" + ).trim(); + + if (isRoomStateObject(roomState.avatar)) { + roomState.avatar = { + ...roomState.avatar, + }; + + if (color) { + roomState.avatar.color = color; + } + } else if (color) { + roomState.avatar = { + color, + }; + } + + return roomState; } if ( @@ -4199,9 +4304,12 @@ function getStoredRoomState() { }; } -function enableRoomStatePersistence() { +function enableRoomStatePersistence(scheduleInitialPersist = true) { roomStatePersistenceEnabled = true; - scheduleRoomStatePersist(); + + if (scheduleInitialPersist) { + scheduleRoomStatePersist(); + } } function initRoomStatePersistence() { @@ -4225,7 +4333,10 @@ function initRoomStatePersistence() { window.addEventListener( "jin:avatar-room-state-changed", - scheduleRoomStatePersist + () => { + persistRoomStateNow({ colorOnly: true }); + scheduleRoomStatePersist(); + } ); window.addEventListener( "beforeunload", @@ -4235,19 +4346,19 @@ function initRoomStatePersistence() { const storedRoomState = getStoredRoomState(); if (storedRoomState) { - const applyStoredRoomState = () => { - return applyRoomState( - storedRoomState, - { - persist: false, - animateTint: false, - } - ); - }; - const avatarState = - isRoomStateObject(storedRoomState.avatar) - ? storedRoomState.avatar - : {}; + const applyStoredRoomState = () => applyRoomState( + storedRoomState, + { + persist: false, + animateTint: false, + } + ); + const color = + String( + storedRoomState.avatar + && storedRoomState.avatar.color + || "" + ).trim(); const sceneTintShift = window.JinRuntime && window.JinRuntime.session @@ -4256,19 +4367,16 @@ function initRoomStatePersistence() { ? window.JinRuntime.session.applyBootstrapSceneTintShift : null; - if ( - sceneTintShift - && String(avatarState.color || "").trim() - ) { + if (color && sceneTintShift) { sceneTintShift( applyStoredRoomState, - avatarState.color + color ); } else { applyStoredRoomState(); } - enableRoomStatePersistence(); + enableRoomStatePersistence(false); return; } diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 3f2c2e23..b0ab909e 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -62,6 +62,8 @@ Object.freeze(["delayed", "l4", "active"]); const SNAPSHOT_GLOW_CLEAR_DELAY_MS = 360; const CENTER_COLOR_STEP_MS = 120; + const DEFAULT_CENTER_COLOR_TRANSITION_MS = 2000; + const CENTER_COLOR_TRANSITION_RESET_BUFFER_MS = 80; const MEMORY_LAYERS_HIDDEN_CLASS = "is-memory-layers-hidden"; const MEMORY_LAYERS_DORMANT_CLASS = "is-memory-layers-dormant"; const MEMORY_LAYERS_FADE_MS = 420; @@ -139,6 +141,7 @@ let centerColor = DEFAULT_CENTER_COLOR; let centerColorTransitionQueue = []; let centerColorTransitionTimer = null; + let centerColorTransitionStyleTimer = null; const reasoningMotionSources = new Set(); const requestedAnimationPlaybackRates = new WeakMap(); let avatarRotationAnimationsCache = null; @@ -4883,22 +4886,92 @@ } } + function prepareCenterColorTransition(durationMs) { + const duration = Math.max(0, Number(durationMs) || 0); + const rootStyle = document.documentElement.style; + + if (centerColorTransitionStyleTimer) { + window.clearTimeout(centerColorTransitionStyleTimer); + centerColorTransitionStyleTimer = null; + } + + if (!duration) { + avatarRoot.style.removeProperty( + "--jin-avatar-center-color-transition-duration" + ); + rootStyle.removeProperty( + "--scene-jin-tint-transition-duration" + ); + return 0; + } + + const durationValue = `${duration}ms`; + + avatarRoot.style.setProperty( + "--jin-avatar-center-color-transition-duration", + durationValue + ); + rootStyle.setProperty( + "--scene-jin-tint-transition-duration", + durationValue + ); + + // Commit the transition duration before changing either the SVG paint or + // the scene variables. This keeps both projections on the same 2s ramp. + avatarRoot.getBoundingClientRect(); + + centerColorTransitionStyleTimer = window.setTimeout( + () => { + avatarRoot.style.removeProperty( + "--jin-avatar-center-color-transition-duration" + ); + rootStyle.removeProperty( + "--scene-jin-tint-transition-duration" + ); + centerColorTransitionStyleTimer = null; + }, + duration + CENTER_COLOR_TRANSITION_RESET_BUFFER_MS + ); + + return duration; + } + function processCenterColorQueue() { - const nextColor = centerColorTransitionQueue.shift(); + const nextTransition = centerColorTransitionQueue.shift(); - if (!nextColor) { + if (!nextTransition) { centerColorTransitionTimer = null; return; } - applyCenterColor(nextColor); + const transitionDurationMs = prepareCenterColorTransition( + nextTransition.transitionDurationMs + ); + + applyCenterColor(nextTransition.color); centerColorTransitionTimer = - setTimeout(processCenterColorQueue, CENTER_COLOR_STEP_MS); + window.setTimeout( + processCenterColorQueue, + Math.max(CENTER_COLOR_STEP_MS, transitionDurationMs) + ); } - function setCenterColor(color) { + function setCenterColor(color, options = {}) { const normalizedColor = normalizeHexColor(color); + const hasExplicitTransitionDuration = Boolean( + options + && Object.prototype.hasOwnProperty.call( + options, + "transitionDurationMs" + ) + ); + const transitionDurationMs = hasExplicitTransitionDuration + ? Math.max( + 0, + Number(options.transitionDurationMs) || 0 + ) + : DEFAULT_CENTER_COLOR_TRANSITION_MS; if (!normalizedColor) { return false; @@ -4911,7 +4984,10 @@ return true; } - centerColorTransitionQueue.push(normalizedColor); + centerColorTransitionQueue.push({ + color: normalizedColor, + transitionDurationMs, + }); if (!centerColorTransitionTimer) { processCenterColorQueue(); diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 21b09a59..59c814eb 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -11,6 +11,8 @@ isReconnectInitialRuntimeMemoryUpdate: notInitialized, isLatestRuntimeMemoryDuplicate: notInitialized, isBootstrapRuntimeMemoryDuplicate: notInitialized, + resolveBootstrapJinColor: notInitialized, + resolveBootstrapRoomState: notInitialized, applyBootstrapSceneTintShift: notInitialized, applyBootstrapRuntimeMemoryUpdate: notInitialized, }; @@ -102,6 +104,177 @@ ); } + function normalizeBootstrapJinColor(value) { + const match = String(value || "") + .trim() + .match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i); + + if (!match) { + return ""; + } + + let color = match[1].toLowerCase(); + + if (color.length === 3) { + color = color + .split("") + .map(char => char + char) + .join(""); + } + + return `#${color}`; + } + + function resolvePersistedJinColor() { + const checkpoint = + readLatestSavedSessionSnapshot(); + const snapshot = + checkpoint + && checkpoint.session_snapshot + && typeof checkpoint.session_snapshot === "object" + && !Array.isArray(checkpoint.session_snapshot) + ? checkpoint.session_snapshot + : null; + + if (!snapshot) { + return ""; + } + + return normalizeBootstrapJinColor( + snapshot.current_jin_color + || ( + snapshot.room_state + && snapshot.room_state.avatar + && snapshot.room_state.avatar.color + ) + ); + } + + function resolveBootstrapJinColor(bootstrap) { + const source = + bootstrap + && typeof bootstrap === "object" + && !Array.isArray(bootstrap) + ? bootstrap + : {}; + const persistedColor = + resolvePersistedJinColor(); + + if (persistedColor) { + return persistedColor; + } + + const actions = Array.isArray(source.session_actions) + ? source.session_actions + : []; + + for (let actionIndex = actions.length - 1; actionIndex >= 0; actionIndex -= 1) { + const action = actions[actionIndex]; + const parts = + action + && Array.isArray(action.parts) + ? action.parts + : []; + + for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) { + const part = parts[partIndex]; + + if ( + !part + || typeof part !== "object" + || Array.isArray(part) + || String(part.text || "").trim().toUpperCase() !== "JIN_COLOR" + ) { + continue; + } + + const colors = Array.isArray(part.colors) + ? part.colors + : []; + + for (let colorIndex = colors.length - 1; colorIndex >= 0; colorIndex -= 1) { + const color = normalizeBootstrapJinColor( + colors[colorIndex] + ); + + if (color) { + return color; + } + } + } + } + + const currentColor = normalizeBootstrapJinColor( + source.current_jin_color + ); + + if (currentColor) { + return currentColor; + } + + return normalizeBootstrapJinColor( + source.room_state + && source.room_state.avatar + && source.room_state.avatar.color + ); + } + + function resolveBootstrapRoomState(bootstrap) { + const source = + bootstrap + && typeof bootstrap === "object" + && !Array.isArray(bootstrap) + ? bootstrap + : {}; + const roomState = + source.room_state + && typeof source.room_state === "object" + && !Array.isArray(source.room_state) + ? source.room_state + : null; + + if (!roomState) { + return null; + } + + const restoredColor = + resolveBootstrapJinColor(source); + const sourceAvatarState = + roomState.avatar + && typeof roomState.avatar === "object" + && !Array.isArray(roomState.avatar) + ? roomState.avatar + : {}; + + if (!restoredColor) { + return roomState; + } + + return { + ...roomState, + avatar: { + ...sourceAvatarState, + color: restoredColor, + }, + }; + } + + function clearBootstrapSceneTintInlineStyles( + sceneMain, + sceneTint + ) { + if (sceneMain) { + sceneMain.style.removeProperty("transition"); + sceneMain.style.removeProperty("background-color"); + } + + if (sceneTint) { + sceneTint.style.removeProperty("transition"); + sceneTint.style.removeProperty("background-color"); + sceneTint.style.removeProperty("opacity"); + } + } + function applyBootstrapSceneTintShift( apply, targetColor = "" @@ -129,6 +302,19 @@ const sceneTint = document.getElementById("scene-jin-tint"); + if ( + activeBootstrapSceneTintColor + && normalizedTargetColor !== activeBootstrapSceneTintColor + ) { + // A newer authoritative bootstrap color is retargeting an in-flight + // local checkpoint transition. Release that helper's inline paint + // first; otherwise its stale color becomes the next cleanup baseline. + clearBootstrapSceneTintInlineStyles( + sceneMain, + sceneTint + ); + } + if ( prefersReducedSceneTintMotion() || (!sceneMain && !sceneTint) @@ -141,19 +327,6 @@ const root = document.documentElement; - const previousMain = sceneMain - ? { - transition: sceneMain.style.transition, - backgroundColor: sceneMain.style.backgroundColor, - } - : null; - const previousTint = sceneTint - ? { - transition: sceneTint.style.transition, - backgroundColor: sceneTint.style.backgroundColor, - opacity: sceneTint.style.opacity, - } - : null; if (sceneMain) { const style = @@ -229,22 +402,10 @@ } activeBootstrapSceneTintColor = ""; - - if (sceneMain) { - sceneMain.style.transition = - previousMain.transition; - sceneMain.style.backgroundColor = - previousMain.backgroundColor; - } - - if (sceneTint) { - sceneTint.style.transition = - previousTint.transition; - sceneTint.style.backgroundColor = - previousTint.backgroundColor; - sceneTint.style.opacity = - previousTint.opacity; - } + clearBootstrapSceneTintInlineStyles( + sceneMain, + sceneTint + ); }, BOOTSTRAP_SCENE_TINT_SHIFT_MS + 80); }); @@ -1389,30 +1550,32 @@ && window.JinPanels && typeof window.JinPanels.applyRoomState === "function" ) { - const applyRoomState = () => { - return window.JinPanels.applyRoomState( - bootstrap.room_state, - { - persist: false, - animateTint: false, - } - ); - }; + const restoredRoomState = + resolveBootstrapRoomState(bootstrap); const avatarState = - bootstrap.room_state.avatar - && typeof bootstrap.room_state.avatar === "object" - && !Array.isArray(bootstrap.room_state.avatar) - ? bootstrap.room_state.avatar + restoredRoomState.avatar + && typeof restoredRoomState.avatar === "object" + && !Array.isArray(restoredRoomState.avatar) + ? restoredRoomState.avatar : {}; + const localRoomState = { + ...restoredRoomState, + avatar: { + ...avatarState, + }, + }; - if (String(avatarState.color || "").trim()) { - applyBootstrapSceneTintShift( - applyRoomState, - avatarState.color - ); - } else { - applyRoomState(); - } + // This is the browser checkpoint sent before archive enrichment. + // Apply layout now, but let the authoritative bootstrap action event + // own the only visible color change from the built-in default. + delete localRoomState.avatar.color; + window.JinPanels.applyRoomState( + localRoomState, + { + persist: false, + animateTint: false, + } + ); } if ( @@ -1820,6 +1983,8 @@ session.isReconnectInitialRuntimeMemoryUpdate = isReconnectInitialRuntimeMemoryUpdate; session.isLatestRuntimeMemoryDuplicate = isLatestRuntimeMemoryDuplicate; session.isBootstrapRuntimeMemoryDuplicate = isBootstrapRuntimeMemoryDuplicate; + session.resolveBootstrapJinColor = resolveBootstrapJinColor; + session.resolveBootstrapRoomState = resolveBootstrapRoomState; session.applyBootstrapSceneTintShift = applyBootstrapSceneTintShift; session.applyBootstrapRuntimeMemoryUpdate = applyBootstrapRuntimeMemoryUpdate; session.rememberStableRuntimeSnapshot = rememberStableRuntimeSnapshot; diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index 314a593b..0334d5d2 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -380,20 +380,54 @@ return merged; } + const restoreSourceSessionId = + String( + merged.source_session_id + || sourceSessionId + || "" + ).trim(); const checkpoint = storage.readLatestSavedSessionSnapshot(); - const snapshot = + const checkpointSnapshot = checkpoint && String(checkpoint.session_id || "").trim() - === String( - merged.source_session_id - || sourceSessionId - || "" - ).trim() + === restoreSourceSessionId && checkpoint.session_snapshot && typeof checkpoint.session_snapshot === "object" ? checkpoint.session_snapshot : null; + const completedRuntimeSnapshot = + restoreSourceSessionId + && typeof storage.collectOtherLatestRuntimeMemorySnapshots + === "function" + ? storage.collectOtherLatestRuntimeMemorySnapshots() + .find(item => ( + item + && String( + item.session_id + || item.key_session_id + || "" + ).trim() === restoreSourceSessionId + && item.session_snapshot + && typeof item.session_snapshot === "object" + && !Array.isArray(item.session_snapshot) + )) + : null; + const completedSnapshot = + completedRuntimeSnapshot + && completedRuntimeSnapshot.session_snapshot + ? completedRuntimeSnapshot.session_snapshot + : null; + + // Layout stays on the normal saved-session checkpoint. Actions and color + // prefer the completed record for this exact archived session, so a later + // room-state write cannot erase the final JIN_COLOR turn. + const snapshot = + checkpointSnapshot + || completedSnapshot; + const actionSnapshot = + completedSnapshot + || checkpointSnapshot; const savedRuntime = typeof storage.readLatestSavedRuntimeMemory === "function" @@ -404,11 +438,7 @@ && typeof savedRuntime === "object" && !Array.isArray(savedRuntime) && String(savedRuntime.session_id || "").trim() - === String( - merged.source_session_id - || sourceSessionId - || "" - ).trim() + === restoreSourceSessionId ); if (savedRuntimeMatchesSource) { @@ -442,8 +472,71 @@ return merged; } + let sessionActions = null; + + if ( + actionSnapshot + && Array.isArray(actionSnapshot.session_actions) + ) { + sessionActions = actionSnapshot.session_actions + .filter(item => ( + item + && typeof item === "object" + && !Array.isArray(item) + )) + .slice(-200) + .map(item => ({ ...item })); + merged.session_actions = sessionActions; + } else if (Array.isArray(snapshot.session_actions)) { + // Compatibility fallback for checkpoints written before per-session + // completed snapshots existed. + merged.session_actions = snapshot.session_actions + .filter(item => ( + item + && typeof item === "object" + && !Array.isArray(item) + )) + .slice(-200) + .map(item => ({ ...item })); + sessionActions = merged.session_actions; + } + + const latestColorPart = + (sessionActions || []) + .slice() + .reverse() + .flatMap(item => ( + Array.isArray(item.parts) + ? item.parts.slice().reverse() + : [] + )) + .find(part => ( + part + && typeof part === "object" + && !Array.isArray(part) + && String(part.text || "") + .trim() + .toUpperCase() === "JIN_COLOR" + )); + const actionColor = + latestColorPart + && Array.isArray(latestColorPart.colors) + && latestColorPart.colors.length + ? String( + latestColorPart.colors[ + latestColorPart.colors.length - 1 + ] + || "" + ).trim() + : ""; const color = - String(snapshot.current_jin_color || "").trim(); + actionColor + || String( + actionSnapshot + && actionSnapshot.current_jin_color + || "" + ).trim() + || String(snapshot.current_jin_color || "").trim(); if (color) { merged.current_jin_color = color; @@ -462,7 +555,23 @@ && typeof value === "object" && !Array.isArray(value) ) { - merged[key] = { ...value }; + if ( + key === "room_state" + && color + && value.avatar + && typeof value.avatar === "object" + && !Array.isArray(value.avatar) + ) { + merged[key] = { + ...value, + avatar: { + ...value.avatar, + color, + }, + }; + } else { + merged[key] = { ...value }; + } } } diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 63be260e..4e334b09 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -187,6 +187,43 @@ function handleSessionActionsUpdate( data ) { + const runtimeSession = + window.JinRuntime + && window.JinRuntime.session; + + if ( + data + && data.bootstrap_restore === true + && runtimeSession + && typeof runtimeSession.resolveBootstrapJinColor === "function" + ) { + const color = runtimeSession.resolveBootstrapJinColor({ + session_actions: + Array.isArray(data.items) + ? data.items + : [], + }); + const applyColor = () => Boolean( + color + && window.JinRuntime.avatar + && typeof window.JinRuntime.avatar.setCenterColor === "function" + && window.JinRuntime.avatar.setCenterColor(color) + ); + + if (color) { + if ( + typeof runtimeSession.applyBootstrapSceneTintShift === "function" + ) { + runtimeSession.applyBootstrapSceneTintShift( + applyColor, + color + ); + } else { + applyColor(); + } + } + } + if (window.updateSessionActionsLog) { window.updateSessionActionsLog( data @@ -358,12 +395,24 @@ function handleAgentRuntimeEnd(data) { && runtimeSession && typeof runtimeSession.persistLiveSessionCheckpoint === "function" ) { - runtimeSession.persistLiveSessionCheckpoint({ + const persisted = runtimeSession.persistLiveSessionCheckpoint({ session_snapshot: data.session_snapshot, completed_turn_commit: Boolean( data.completed_turn_commit === true ), }); + + // A fresh continuation cannot repaint its predecessor while booting. + // Right after the first real turn takes checkpoint ownership, save the + // actual client room state once so the just-applied JIN color is retained. + if ( + persisted + && data.completed_turn_commit === true + && window.JinPanels + && typeof window.JinPanels.persistRoomStateNow === "function" + ) { + window.JinPanels.persistRoomStateNow(); + } } if (data && data.retryable_response === true) { diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index bbacf5db..f3172ce2 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -10,7 +10,7 @@ const JIN_VISUAL_SEQUENCE_ACTIONS = new Set([ "jin_speed", "jin_position", ]); -const JIN_VISUAL_SEQUENCE_COLOR_MS = 160; +const JIN_VISUAL_SEQUENCE_COLOR_MS = 2000; const JIN_VISUAL_SEQUENCE_SIZE_MS = 320; const JIN_VISUAL_SEQUENCE_CROSS_STAGE_RATIO = 0.58; const jinVisualSequenceBuffers = new Map(); @@ -43,7 +43,12 @@ function applyJinVisualSequenceCommand(command) { && window.JinRuntime && window.JinRuntime.avatar && typeof window.JinRuntime.avatar.setCenterColor === "function" - && window.JinRuntime.avatar.setCenterColor(command.color) + && window.JinRuntime.avatar.setCenterColor( + command.color, + { + transitionDurationMs: JIN_VISUAL_SEQUENCE_COLOR_MS, + } + ) ); return { @@ -1791,7 +1796,10 @@ function handleRuntimeAction( && typeof window.JinRuntime.avatar.setCenterColor === "function" ) { window.JinRuntime.avatar.setCenterColor( - color + color, + { + transitionDurationMs: JIN_VISUAL_SEQUENCE_COLOR_MS, + } ); } } diff --git a/ui/templates/index.html b/ui/templates/index.html index 2048dbc7..462bee31 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -17,7 +17,7 @@ - + @@ -510,16 +510,16 @@ - + - + - + @@ -535,8 +535,8 @@ - - + + diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index 9c67c630..2e69cb82 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -762,6 +762,7 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_JIN_COLOR: current_jin_color = jin_color + context.jin_color = jin_color jin_color_last_by_message[ jin_color_message_scope ] = jin_color diff --git a/utils/actions/jin_visual_sequence_actions.py b/utils/actions/jin_visual_sequence_actions.py index 5ba6b855..19986df2 100644 --- a/utils/actions/jin_visual_sequence_actions.py +++ b/utils/actions/jin_visual_sequence_actions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import contextlib +import time from uuid import uuid4 from contracts.rules_assembler import ( @@ -17,6 +19,7 @@ normalize_jin_size_dict, normalize_jin_speed_value, ) +from utils.chat_log import append_chat_runtime_event JIN_VISUAL_ACTION_NAMES = { @@ -158,6 +161,10 @@ async def emit_jin_visual_sequences( for sequence_index, (action, event) in enumerate(prepared): action_name = getattr(action, "name", "") payload = event["payload"] + action_display_id = str( + action_display_ids.get(id(action), "") + or "" + ).strip() if action_name == RUNTIME_ACTION_JIN_SPEED: context.runtime_avatar_move_speed = event["speed"] @@ -166,14 +173,43 @@ async def emit_jin_visual_sequences( "x": event["x"], "y": event["y"], } + elif action_name == RUNTIME_ACTION_JIN_COLOR: + created_at = time.time() + color = event["color"] + session_action = { + "text": "JIN_COLOR", + "created_at": created_at, + "runtime_turn_id": str( + getattr( + context, + "runtime_current_turn_id", + "", + ) + or "" + ).strip(), + "parts": [{ + "text": "JIN_COLOR", + "colors": [color], + }], + } + with contextlib.suppress(Exception): + append_chat_runtime_event( + context, + event="runtime_action_request", + payload={ + "action": RUNTIME_ACTION_JIN_COLOR, + "id": action_display_id, + "color": color, + "payload": color, + "session_action": session_action, + "created_at": created_at, + }, + ) await emit(with_action_context({ "type": "runtime_action", "action": event["action"], - "id": str( - action_display_ids.get(id(action), "") - or "" - ).strip(), + "id": action_display_id, "status": "completed", **build_runtime_action_event_display_fields( action_name, diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index ee481f8c..e664a40e 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -3023,6 +3023,7 @@ async def emit_session_actions_update( context, *, current_sequence: bool, + bootstrap_restore: bool = False, ) -> None: items = build_session_actions_update_items( @@ -3060,6 +3061,7 @@ async def emit_session_actions_update( "sequence_id": get_current_action_sequence_turn_id( context ), + "bootstrap_restore": bool(bootstrap_restore), "items": items, }) diff --git a/utils/session_restore.py b/utils/session_restore.py index 08856528..d3320cfd 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -15,7 +15,11 @@ _clean_session_id, chat_log_root_for_mode, ) -from utils.actions import normalize_jin_position_dict, normalize_jin_speed_value +from utils.actions import ( + normalize_jin_color_payload, + normalize_jin_position_dict, + normalize_jin_speed_value, +) BLOCK_RE_TEMPLATE = r"<{name}(?:\s+[^>]*)?>\s*(?P[\s\S]*?)\s*" @@ -41,6 +45,10 @@ r"^[ \t]*[ \t]*[\"'`]*$", re.IGNORECASE | re.MULTILINE, ) +RESTORED_DIALOG_SOURCE_RE = re.compile( + r']*\bsession_id="(?P[^"]+)"', + re.IGNORECASE, +) ACTION_LABELS = { @@ -813,7 +821,57 @@ def _build_runtime_event_session_actions(entries: list[dict]) -> list[dict]: payload = entry.get("payload") if not isinstance(payload, dict): continue - if str(payload.get("action", "") or "").strip().upper() != "UPDATE_L4_FACTS": + + action_name = str( + payload.get("action", "") + or "" + ).strip().upper() + created_at = _runtime_event_created_at(entry, payload) + runtime_turn_id = str( + entry.get("turn_id", "") + or "" + ).strip() + + if action_name == "JIN_COLOR": + color = normalize_jin_color_payload( + payload.get("color") + or payload.get("payload") + ) + if not color: + continue + + if ( + items + and runtime_turn_id + and items[-1].get("runtime_turn_id") == runtime_turn_id + and len(items[-1].get("parts", [])) == 1 + and str( + items[-1]["parts"][0].get("text", "") + or "" + ).strip().upper() == "JIN_COLOR" + ): + colors = items[-1]["parts"][0].setdefault( + "colors", + [], + ) + if color not in colors: + colors.append(color) + continue + + item = { + "text": "JIN_COLOR", + "created_at": created_at, + "parts": [{ + "text": "JIN_COLOR", + "colors": [color], + }], + } + if runtime_turn_id: + item["runtime_turn_id"] = runtime_turn_id + items.append(item) + continue + + if action_name != "UPDATE_L4_FACTS": continue raw_action = payload.get("session_action") @@ -832,14 +890,92 @@ def _build_runtime_event_session_actions(entries: list[dict]) -> list[dict]: item = { "text": text, - "created_at": _runtime_event_created_at(entry, payload), + "created_at": created_at, "parts": [part], } + if runtime_turn_id: + item["runtime_turn_id"] = runtime_turn_id items.append(item) return items[-200:] +def _build_predecessor_runtime_event_session_actions( + context_text: str, + root: Path, + *, + seen_session_ids: set[str], + remaining_sessions: int = 3, +) -> list[dict]: + """Recover inherited actions from the real direct-predecessor log chain.""" + if remaining_sessions <= 0: + return [] + + match = RESTORED_DIALOG_SOURCE_RE.search(str(context_text or "")) + if match is None: + return [] + + source_session_id = _clean_session_id(match.group("session_id")) + if not source_session_id or source_session_id in seen_session_ids: + return [] + + seen_session_ids.add(source_session_id) + session_directory = _find_session_directory(source_session_id, root) + if session_directory is None: + return [] + + dialog_paths = sorted( + (path for path in session_directory.glob("*.jsonl") if path.is_file()), + key=lambda path: path.name, + ) + if not dialog_paths: + return [] + + dialog_path = dialog_paths[-1] + entries = _load_dialog(dialog_path) + context_path = dialog_path.with_suffix(".txt") + try: + predecessor_context = ( + context_path.read_text(encoding="utf-8", errors="replace") + if context_path.is_file() + else "" + ) + except OSError: + predecessor_context = "" + + older_actions = _build_predecessor_runtime_event_session_actions( + predecessor_context, + root, + seen_session_ids=seen_session_ids, + remaining_sessions=remaining_sessions - 1, + ) + return ( + older_actions + + _build_runtime_event_session_actions(entries) + )[-200:] + + +def _latest_runtime_jin_color(entries: list[dict]) -> str: + for entry in reversed(entries): + if str(entry.get("event", "") or "").strip() != "runtime_action_request": + continue + + payload = entry.get("payload") + if not isinstance(payload, dict): + continue + if str(payload.get("action", "") or "").strip().upper() != "JIN_COLOR": + continue + + color = normalize_jin_color_payload( + payload.get("color") + or payload.get("payload") + ) + if color: + return color + + return "" + + def _build_runtime_event_tool_results(entries: list[dict]) -> list[dict]: items = [] @@ -1275,7 +1411,14 @@ def build_archived_session_restore_payload( fallback_created_at = _entry_timestamp(visible_entries[0]) or dialog_path.stat().st_mtime session_actions = _build_session_actions(context_text, fallback_created_at) - runtime_session_actions = _build_runtime_event_session_actions(entries) + runtime_session_actions = ( + _build_predecessor_runtime_event_session_actions( + context_text, + root_path, + seen_session_ids={_clean_session_id(session_id)}, + ) + + _build_runtime_event_session_actions(entries) + )[-200:] if runtime_session_actions: session_actions = [ item @@ -1381,7 +1524,10 @@ def build_archived_session_restore_payload( "turn_number": max_turn, "user_message_count": len(user_entries), "assistant_message_count": len(jin_entries), - "current_jin_color": trusted_values.get("CURRENT_JIN_COLOR", ""), + "current_jin_color": ( + _latest_runtime_jin_color(entries) + or trusted_values.get("CURRENT_JIN_COLOR", "") + ), "current_jin_size": _parse_jin_size( trusted_values.get("CURRENT_JIN_SIZE", "") ), diff --git a/websocket/__init__.py b/websocket/__init__.py index 5733a5c4..8bdbf6ac 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -615,6 +615,7 @@ async def process_pending_requests(): await emit_session_actions_update( context, current_sequence=False, + bootstrap_restore=True, ) chat_tail = build_session_bootstrap_chat_tail( diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 81e90826..200e52d3 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -35,6 +35,7 @@ from utils.actions import ( is_active_memory_key, is_delayed_memory_report_id, + normalize_jin_color_payload, normalize_jin_speed_value, refresh_active_memory_runtime_metadata, remove_active_memory_entries, @@ -545,6 +546,15 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: context.runtime_session_action_history = normalized_actions + restored_jin_color = normalize_jin_color_payload( + message_data.get("current_jin_color", "") + ) + if restored_jin_color: + # Normal next-tab bootstrap owns the live color too. Previously only + # explicit archived restores staged it, leaving RuntimeContext on the + # default #1f4f8f even after the action trail was recovered. + context.jin_color = restored_jin_color + if bool( message_data.get("archived_session_restore") and source_session_id @@ -2340,6 +2350,130 @@ def _bootstrap_recent_turns_tail_timestamp(value) -> float: return newest +def _bootstrap_session_action_identity(item: dict) -> str: + explicit_id = clean_bootstrap_memory( + item.get("id", ""), + limit=200, + ) + if explicit_id: + return "id:" + explicit_id + + runtime_turn_id = clean_bootstrap_memory( + item.get("runtime_turn_id", ""), + limit=120, + ) + part_names = [ + clean_bootstrap_memory( + part.get("text", ""), + limit=600, + ).upper() + for part in item.get("parts", []) + if isinstance(part, dict) + and clean_bootstrap_memory( + part.get("text", ""), + limit=600, + ) + ] if isinstance(item.get("parts"), list) else [] + + if runtime_turn_id and part_names: + return "turn:" + runtime_turn_id + ":" + "|".join(part_names) + + try: + created_at = int(float(item.get("created_at", 0) or 0)) + except (TypeError, ValueError): + created_at = 0 + text = clean_bootstrap_memory( + item.get("text", ""), + limit=2000, + ) + colors = [] + for part in item.get("parts", []) if isinstance(item.get("parts"), list) else []: + if not isinstance(part, dict): + continue + raw_colors = part.get("colors", []) + if isinstance(raw_colors, (str, bytes)): + raw_colors = [raw_colors] + if isinstance(raw_colors, list): + colors.extend( + str(color or "").strip().lower() + for color in raw_colors + if str(color or "").strip() + ) + + return json.dumps( + [created_at, text, part_names, colors], + ensure_ascii=False, + separators=(",", ":"), + ) + + +def _bootstrap_session_action_timestamp(item: dict) -> float: + raw = item.get("created_at") + try: + return float(raw or 0) + except (TypeError, ValueError): + return _bootstrap_iso_timestamp(raw) + + +def _bootstrap_latest_session_action_color(value) -> str: + if not isinstance(value, list): + return "" + + for item in reversed(value): + if not isinstance(item, dict): + continue + parts = item.get("parts", []) + if not isinstance(parts, list): + continue + for part in reversed(parts): + if ( + not isinstance(part, dict) + or clean_bootstrap_memory( + part.get("text", ""), + limit=600, + ).upper() != "JIN_COLOR" + ): + continue + colors = part.get("colors", []) + if isinstance(colors, (str, bytes)): + colors = [colors] + if not isinstance(colors, list): + continue + for raw_color in reversed(colors): + color = normalize_jin_color_payload(raw_color) + if color: + return color + + return "" + + +def _merge_bootstrap_session_actions( + browser_actions, + archive_actions, +) -> list[dict]: + merged = [] + seen = set() + + for item in [ + *(browser_actions if isinstance(browser_actions, list) else []), + *(archive_actions if isinstance(archive_actions, list) else []), + ]: + if not isinstance(item, dict): + continue + identity = _bootstrap_session_action_identity(item) + if identity in seen: + continue + seen.add(identity) + merged.append(dict(item)) + + # Archive-derived actions can be older than the browser checkpoint even + # when they are appended later during enrichment. Sort by their real + # timestamp so a stale archive item cannot push the just-completed + # JIN_COLOR action out of the restored three-item tail. + merged.sort(key=_bootstrap_session_action_timestamp) + return merged[-200:] + + def enrich_session_bootstrap_from_archive( message_data: dict, *, @@ -2536,13 +2670,47 @@ def enrich_session_bootstrap_from_archive( for field in ( "restore_delayed_memory_metadata", "restore_attached_file_metadata", - "session_actions", "attached_file_ids", ): value = archived.get(field) if value not in (None, "", [], {}): enriched[field] = value + # Actions have their own causal timestamps and are recovered from raw-log + # predecessor lineage. Merge that exact stream independently of saved_at: + # saved_at can advance after a harmless browser room-state/L1 write, while + # the JSONL action is still the authoritative missing middle event. + browser_actions = enriched.get("session_actions", []) + archive_actions = archived.get("session_actions", []) + if archive_actions not in (None, "", [], {}): + enriched["session_actions"] = ( + _merge_bootstrap_session_actions( + browser_actions, + archive_actions, + ) + ) + + browser_color = normalize_jin_color_payload( + enriched.get("current_jin_color", "") + ) + if browser_color: + # The browser checkpoint is updated in the same tick that JIN_COLOR is + # applied. Session actions remain history; they only recover color for + # checkpoints created before that field-local write existed. + enriched["current_jin_color"] = browser_color + else: + archived_color = normalize_jin_color_payload( + archived.get("current_jin_color", "") + ) + action_color = _bootstrap_latest_session_action_color( + enriched.get("session_actions", []) + ) + + if action_color: + enriched["current_jin_color"] = action_color + elif archived_color: + enriched["current_jin_color"] = archived_color + # Browser persistence is the exact checkpoint when available. Fall back # to archived PREVIOUS_RUNTIME_STATE/resources only when the browser half # is absent. From 5b739d2ec948826be5b1ef7d10ff27b482da304f Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:21:54 +0300 Subject: [PATCH 098/168] chore: bootstrap - cleanup bootstrap logic --- .../test_jin_color_bootstrap_persistence.py | 40 ++ .../test_bootstrap_color_action_regression.py | 100 ++- ...test_bootstrap_completed_turn_atomicity.py | 10 +- ...test_bootstrap_latest_completed_session.py | 49 +- ...st_jin_color_transition_client_contract.py | 51 +- tests/test_live_session_checkpoint.py | 48 +- tests/test_session_bootstrap_chat_tail.py | 26 +- ...ssion_bootstrap_lineage_client_contract.py | 81 +-- ui/static/css/base.css | 2 +- ui/static/css/runtime-avatar.css | 8 +- ui/static/js/logger/logger.js | 117 +--- ui/static/js/runtime/runtime-avatar.js | 78 +-- ui/static/js/runtime/runtime-session.js | 631 +++--------------- ui/static/js/runtime/runtime-storage.js | 17 +- ui/static/js/session-restore.js | 272 +++----- ui/static/js/socket/event-handlers.js | 108 +-- ui/static/js/socket/runtime-actions.js | 2 +- ui/templates/index.html | 16 +- utils/actions/jin_visual_sequence_actions.py | 15 +- utils/session_actions_history.py | 14 +- utils/session_restore.py | 80 ++- websocket/bootstrap.py | 104 ++- 22 files changed, 773 insertions(+), 1096 deletions(-) diff --git a/tests/runtime_actions/test_jin_color_bootstrap_persistence.py b/tests/runtime_actions/test_jin_color_bootstrap_persistence.py index b921e43b..3942a517 100644 --- a/tests/runtime_actions/test_jin_color_bootstrap_persistence.py +++ b/tests/runtime_actions/test_jin_color_bootstrap_persistence.py @@ -48,9 +48,49 @@ async def log_runtime(_message): assert kwargs["event"] == "runtime_action_request" assert kwargs["payload"]["action"] == "JIN_COLOR" assert kwargs["payload"]["color"] == "#00ff00" + assert kwargs["payload"]["event_id"].startswith("jin-") + assert ( + kwargs["payload"]["session_action"]["id"] + == kwargs["payload"]["event_id"] + ) assert ( kwargs["payload"]["session_action"]["parts"][0]["colors"] == ["#00ff00"] ) asyncio.run(run_case()) + + +def test_jin_color_log_failure_is_visible_without_blocking_ui_event(): + async def run_case(): + emitter = _Emitter() + context = SimpleNamespace( + emitter=emitter, + runtime_current_turn_id="turn-color-log-failure", + ) + action = RuntimeActionCall( + name="JIN_COLOR", + payload="#ff0000", + ) + + with ( + patch( + "utils.actions.jin_visual_sequence_actions.append_chat_runtime_event", + side_effect=OSError("disk full"), + ), + patch( + "utils.actions.jin_visual_sequence_actions.LOGGER.exception" + ) as log_exception, + ): + await emit_jin_visual_sequences( + context, + [action], + action_display_ids={id(action): "color-action"}, + log_runtime=None, + with_action_context=lambda event: event, + ) + + assert len(emitter.events) == 1 + log_exception.assert_called_once() + + asyncio.run(run_case()) diff --git a/tests/test_bootstrap_color_action_regression.py b/tests/test_bootstrap_color_action_regression.py index 286f3ce9..4bbc3720 100644 --- a/tests/test_bootstrap_color_action_regression.py +++ b/tests/test_bootstrap_color_action_regression.py @@ -11,6 +11,68 @@ class BootstrapColorActionRegressionTests(unittest.TestCase): + def test_raw_color_tail_does_not_delete_context_l4_action(self): + root = Path(tempfile.mkdtemp()) + session_id = "color-and-l4-session" + session_dir = root / "2026-08-25" / session_id + session_dir.mkdir(parents=True) + entries = [ + { + "ts": "2026-08-25T13:00:00+03:00", + "turn": 1, + "turn_id": "turn_000001", + "session_id": session_id, + "role": "user", + "text": "green", + }, + { + "ts": "2026-08-25T13:00:01+03:00", + "turn": 1, + "turn_id": "turn_000001", + "session_id": session_id, + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "event_id": "green-event", + "color": "#00ff00", + "created_at": 100.0, + }, + }, + { + "ts": "2026-08-25T13:00:02+03:00", + "turn": 1, + "turn_id": "turn_000001", + "session_id": session_id, + "role": "jin", + "text": "Green.", + }, + ] + (session_dir / "session.jsonl").write_text( + "\n".join(json.dumps(item) for item in entries), + encoding="utf-8", + ) + (session_dir / "session.txt").write_text( + '\n' + '{"message":"kept fact"}\n' + "", + encoding="utf-8", + ) + + payload = build_archived_session_restore_payload( + session_id, + root=root, + ) + action_parts = [ + part["text"] + for item in payload["session_actions"] + for part in item.get("parts", []) + ] + + self.assertIn("Updated L4 facts", action_parts) + self.assertIn("JIN_COLOR", action_parts) + def test_archive_recovers_color_actions_from_direct_predecessor_logs(self): root = Path(tempfile.mkdtemp()) previous_session_id = "previous-blue-session" @@ -72,6 +134,22 @@ def test_archive_recovers_color_actions_from_direct_predecessor_logs(self): "event": "runtime_action_request", "payload": { "action": "JIN_COLOR", + "event_id": "same-turn-blue", + "color": "#0000ff", + "created_at": 150.0, + }, + }, + { + "ts": "2026-08-25T13:24:21+03:00", + "turn": 38, + "turn_id": "turn_000038", + "session_id": current_session_id, + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "event_id": "same-turn-red", "color": "#ff0000", "created_at": 200.0, }, @@ -112,30 +190,28 @@ def test_archive_recovers_color_actions_from_direct_predecessor_logs(self): item["parts"][0]["colors"][0] for item in payload["session_actions"] ], - ["#0000ff", "#ff0000"], + ["#0000ff", "#0000ff", "#ff0000"], ) - def test_server_actions_remain_ui_color_fallback(self): + def test_server_emits_one_authoritative_bootstrap_color(self): websocket_source = (ROOT / "websocket" / "__init__.py").read_text( encoding="utf-8" ) handler_source = ( ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" ).read_text(encoding="utf-8") - session_source = ( - ROOT / "ui" / "static" / "js" / "runtime" / "runtime-session.js" + history_source = ( + ROOT / "utils" / "session_actions_history.py" ).read_text(encoding="utf-8") self.assertIn("bootstrap_restore=True", websocket_source) self.assertIn("data.bootstrap_restore === true", handler_source) - self.assertIn("runtimeSession.resolveBootstrapJinColor", handler_source) - self.assertIn("runtimeSession.applyBootstrapSceneTintShift", handler_source) - self.assertIn("resolvePersistedJinColor()", session_source) - self.assertIn( - 'sceneMain.style.removeProperty("background-color")', - session_source, - ) - self.assertNotIn("previousMain.transition", session_source) + self.assertIn("data.current_jin_color", handler_source) + self.assertIn("initialBootstrap: true", handler_source) + self.assertIn("persist: false", handler_source) + self.assertIn('payload["current_jin_color"]', history_source) + self.assertNotIn("resolveBootstrapJinColor", handler_source) + self.assertNotIn("applyBootstrapSceneTintShift", handler_source) if __name__ == "__main__": diff --git a/tests/test_bootstrap_completed_turn_atomicity.py b/tests/test_bootstrap_completed_turn_atomicity.py index 3aa2d632..08e70e87 100644 --- a/tests/test_bootstrap_completed_turn_atomicity.py +++ b/tests/test_bootstrap_completed_turn_atomicity.py @@ -83,13 +83,9 @@ def test_archive_dialogue_freshness_is_not_blocked_by_newer_runtime_saved_at(): assert enriched["recent_turns"] == archived["recent_turns"] assert enriched["previous_reasoning"] == "new reasoning" - # Resources still obey runtime checkpoint freshness. Actions carry their - # own timestamps/ids, so an archive action cannot disappear merely because - # saved_at advanced after a harmless browser write. - assert enriched["session_actions"] == [ - {"id": "browser-action"}, - {"id": "archive-action"}, - ] + # The atomic checkpoint owns actions committed before saved_at. Only a raw + # action with a strictly newer created_at may extend this list. + assert enriched["session_actions"] == [{"id": "browser-action"}] def test_agent_runtime_end_commits_user_only_completed_turn_checkpoint(): diff --git a/tests/test_bootstrap_latest_completed_session.py b/tests/test_bootstrap_latest_completed_session.py index 01d57ac6..26b0e0cc 100644 --- a/tests/test_bootstrap_latest_completed_session.py +++ b/tests/test_bootstrap_latest_completed_session.py @@ -107,7 +107,7 @@ def test_normal_bootstrap_never_reads_newer_anonymous_log_root(tmp_path): assert enriched["recent_turns"][-1]["jin"] == "normal jin" -def test_bootstrap_replaces_stale_source_with_newer_complete_raw_session(): +def test_bootstrap_replaces_stale_source_with_newer_user_move(): stale = { "source_session_id": "stale-session", "recent_turns": [{ @@ -122,9 +122,8 @@ def test_bootstrap_replaces_stale_source_with_newer_complete_raw_session(): "source_session_id": "fresh-session", "recent_turns": [{ "user": "напиши быстро тест", - "jin": "Тест пройден", + "jin": "", "user_created_at": 200.0, - "jin_created_at": 201.0, }], "dialog_context": "fresh", } @@ -223,7 +222,43 @@ def test_latest_selector_accepts_committed_user_only_action_turn(tmp_path): assert payload["recent_turns"][-1]["jin"] == "" -def test_latest_selector_ignores_unfinished_bare_user_turn(tmp_path): +def test_latest_selector_keeps_raw_color_before_jin_or_l1(tmp_path): + previous_session = "previous" + color_session = "color-move" + + _write_dialog(tmp_path, previous_session, "201000", [ + {"ts": "2026-08-24T20:10:00+03:00", "turn": 1, "role": "user", "text": "old"}, + {"ts": "2026-08-24T20:10:05+03:00", "turn": 1, "role": "jin", "text": "old answer"}, + ]) + _write_dialog(tmp_path, color_session, "201500", [ + {"ts": "2026-08-24T20:15:00+03:00", "turn": 2, "turn_id": "turn_000002", "role": "user", "text": "стань красным"}, + { + "ts": "2026-08-24T20:15:01+03:00", + "turn": 2, + "turn_id": "turn_000002", + "role": "runtime", + "text": "", + "event": "runtime_action_request", + "payload": { + "action": "JIN_COLOR", + "event_id": "jin-color-event", + "color": "#ff0000", + "created_at": 1724519701.0, + }, + }, + ]) + + payload = find_latest_completed_session_restore_payload(root=tmp_path) + + assert payload is not None + assert payload["source_session_id"] == color_session + assert payload["recent_turns"][-1]["user"] == "стань красным" + assert payload["recent_turns"][-1]["jin"] == "" + assert payload["current_jin_color"] == "#ff0000" + assert payload["session_actions"][-1]["parts"][0]["colors"] == ["#ff0000"] + + +def test_latest_selector_promotes_newer_bare_user_move(tmp_path): complete_session = "complete" interrupted_session = "interrupted" @@ -238,5 +273,7 @@ def test_latest_selector_ignores_unfinished_bare_user_turn(tmp_path): payload = find_latest_completed_session_restore_payload(root=tmp_path) assert payload is not None - assert payload["source_session_id"] == complete_session - assert payload["recent_turns"][-1]["jin"] == "complete jin" + assert payload["source_session_id"] == interrupted_session + assert payload["recent_turns"][-1]["user"] == "in flight" + assert payload["recent_turns"][-1]["jin"] == "" + assert "jin_created_at" not in payload["recent_turns"][-1] diff --git a/tests/test_jin_color_transition_client_contract.py b/tests/test_jin_color_transition_client_contract.py index 44f22903..1145b911 100644 --- a/tests/test_jin_color_transition_client_contract.py +++ b/tests/test_jin_color_transition_client_contract.py @@ -21,12 +21,12 @@ class JinColorTransitionClientContractTests(unittest.TestCase): - def test_live_jin_color_uses_one_two_second_avatar_and_tint_transition(self): + def test_live_jin_color_uses_one_333ms_avatar_and_tint_transition(self): actions_source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") avatar_source = AVATAR_JS.read_text(encoding="utf-8") avatar_css = AVATAR_CSS.read_text(encoding="utf-8") - self.assertIn("const JIN_VISUAL_SEQUENCE_COLOR_MS = 2000;", actions_source) + self.assertIn("const JIN_VISUAL_SEQUENCE_COLOR_MS = 333;", actions_source) self.assertEqual( actions_source.count( "transitionDurationMs: JIN_VISUAL_SEQUENCE_COLOR_MS" @@ -41,28 +41,28 @@ def test_live_jin_color_uses_one_two_second_avatar_and_tint_transition(self): '"--jin-avatar-center-color-transition-duration",\n durationValue', avatar_source, ) + self.assertNotIn("centerColorTransitionQueue", avatar_source) + self.assertNotIn("processCenterColorQueue", avatar_source) self.assertIn( - "Math.max(CENTER_COLOR_STEP_MS, transitionDurationMs)", - avatar_source, - ) - self.assertIn( - "var(--jin-avatar-center-color-transition-duration, 0.16s)", + "var(--jin-avatar-center-color-transition-duration, 333ms)", avatar_css, ) - def test_every_center_color_change_defaults_to_two_seconds(self): + def test_only_first_bootstrap_color_uses_two_seconds(self): avatar_source = AVATAR_JS.read_text(encoding="utf-8") self.assertIn( - "const DEFAULT_CENTER_COLOR_TRANSITION_MS = 2000;", + "const INITIAL_BOOTSTRAP_COLOR_TRANSITION_MS = 2000;", avatar_source, ) self.assertIn( - ": DEFAULT_CENTER_COLOR_TRANSITION_MS;", + "const DEFAULT_CENTER_COLOR_TRANSITION_MS = 333;", avatar_source, ) + self.assertIn("let initialBootstrapColorPending = true;", avatar_source) + self.assertIn("initialBootstrapColorPending = false;", avatar_source) - def test_common_checkpoint_color_is_immediate_and_boots_first(self): + def test_common_checkpoint_color_is_immediate_within_same_session_and_boots_first(self): logger_source = LOGGER_JS.read_text(encoding="utf-8") session_source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") storage_source = RUNTIME_STORAGE_JS.read_text(encoding="utf-8") @@ -86,29 +86,33 @@ def test_common_checkpoint_color_is_immediate_and_boots_first(self): ) init_block = logger_source[init_start:init_end] - self.assertIn("current_jin_color: color", persist_block) - self.assertIn("previousSnapshot.room_state", persist_block) + self.assertIn("const roomState = getRoomState(previousRoomState);", persist_block) + self.assertIn("sessionSnapshot.current_jin_color = avatar.color;", persist_block) self.assertIn("...checkpoint", persist_block) self.assertNotIn("saved_at:", persist_block) + self.assertNotIn("colorOnly", persist_block) + self.assertIn("currentSessionId !== checkpointSessionId", persist_block) self.assertLess( - persist_block.index("if (colorOnly)"), - persist_block.index("const currentSessionId"), + persist_block.index("currentSessionId !== checkpointSessionId"), + persist_block.index("const roomState = getRoomState(previousRoomState);"), ) self.assertIn("snapshot.current_jin_color", stored_block) self.assertIn("roomState.avatar.color = color;", stored_block) self.assertNotIn("delete roomState.avatar.color;", stored_block) self.assertNotIn("resolveBootstrapRoomState", stored_block) - self.assertIn("applyBootstrapSceneTintShift", init_block) self.assertIn( - "persistRoomStateNow({ colorOnly: true });", + "initialBootstrapColor: true", init_block, ) + self.assertIn("event.detail.immediate === true", init_block) + self.assertNotIn("colorOnly", init_block) + self.assertNotIn("applyBootstrapSceneTintShift", init_block) self.assertIn("enableRoomStatePersistence(false);", init_block) self.assertNotIn("latestJinColor", storage_source) bootstrap_start = bootstrap_source.index( - "browser_color = normalize_jin_color_payload(" + "browser_color = (" ) bootstrap_end = bootstrap_source.index( "# Browser persistence is the exact checkpoint when available.", @@ -116,8 +120,8 @@ def test_common_checkpoint_color_is_immediate_and_boots_first(self): ) bootstrap_block = bootstrap_source[bootstrap_start:bootstrap_end] + self.assertIn("if source_changed", bootstrap_block) self.assertIn("if browser_color:", bootstrap_block) - self.assertIn("else:", bootstrap_block) self.assertIn("_bootstrap_latest_session_action_color", bootstrap_block) apply_start = session_source.index( @@ -129,17 +133,18 @@ def test_common_checkpoint_color_is_immediate_and_boots_first(self): ) apply_block = session_source[apply_start:apply_end] - self.assertIn("delete localRoomState.avatar.color;", apply_block) + self.assertNotIn("applyRoomState", apply_block) + self.assertNotIn("resolveBootstrapJinColor", session_source) self.assertNotIn("applyBootstrapSceneTintShift(", apply_block) def test_changed_assets_have_matching_cache_bumps(self): index_source = INDEX_HTML.read_text(encoding="utf-8") - self.assertEqual(index_source.count("jin-color-transition=2"), 2) + self.assertEqual(index_source.count("jin-color-transition=3"), 2) self.assertIn("runtime-session.js?v=", index_source) - self.assertIn("&room-state=7&", index_source) + self.assertIn("&room-state=8&", index_source) self.assertIn("logger.js?v=", index_source) - self.assertIn("&room-state=5&", index_source) + self.assertIn("&room-state=6&", index_source) if __name__ == "__main__": diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index 97fff29e..e8b1d74e 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -177,7 +177,20 @@ def test_duplicate_l1_still_refreshes_live_session_checkpoint(self): self.assertIn(duplicate, source) - def test_room_state_does_not_repaint_predecessor_checkpoint(self): + def test_real_user_activity_can_advance_checkpoint_before_completed_answer(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + start = source.index("function persistLiveSessionCheckpoint(data)") + end = source.index("function clearPersistedToolResultsCheckpoint()", start) + block = source[start:end] + + self.assertIn("const sessionMoved = Boolean(", block) + self.assertIn("hasUnsavedSessionActivity", block) + self.assertIn("|| completedTurnCommit", block) + self.assertIn("&& !sessionMoved", block) + self.assertIn("hasUnsavedSessionActivity = false;", block) + self.assertIn("return Boolean(checkpointWritten);", block) + + def test_room_state_never_switches_the_session_checkpoint(self): source = LOGGER_JS.read_text(encoding="utf-8") start = source.index("function persistRoomStateNow(") end = source.index("function scheduleRoomStatePersist()", start) @@ -185,32 +198,35 @@ def test_room_state_does_not_repaint_predecessor_checkpoint(self): self.assertIn("storage.getCurrentRuntimeSessionId", block) self.assertIn("currentSessionId !== checkpointSessionId", block) - self.assertIn("return false", block) + self.assertLess( + block.index("currentSessionId !== checkpointSessionId"), + block.index("const roomState = getRoomState(previousRoomState);"), + ) + self.assertIn("room_state: roomState", block) + self.assertIn("sessionSnapshot.current_jin_color = avatar.color;", block) + self.assertNotIn("colorOnly", block) - def test_color_updates_the_same_checkpoint_before_owner_switch(self): + def test_color_has_no_separate_checkpoint_writer(self): source = LOGGER_JS.read_text(encoding="utf-8") start = source.index("function persistRoomStateNow(") end = source.index("function scheduleRoomStatePersist()", start) block = source[start:end] - self.assertIn("current_jin_color: color", block) - self.assertIn("previousSnapshot.room_state", block) - self.assertIn("...checkpoint", block) + self.assertEqual(block.count("writeLatestSavedSessionSnapshot"), 2) + self.assertIn("sessionSnapshot.current_jin_color = avatar.color;", block) self.assertNotIn("saved_at:", block) - self.assertLess( - block.index("if (colorOnly)"), - block.index("currentSessionId !== checkpointSessionId"), - ) + self.assertNotIn("colorOnly", block) - def test_completed_turn_repersists_room_state_after_owner_switch(self): + def test_completed_turn_persists_server_and_room_as_one_snapshot(self): handlers = EVENT_HANDLERS_JS.read_text(encoding="utf-8") start = handlers.index("function handleAgentRuntimeEnd(data)") end = handlers.index("function handleMessageStart(", start) block = handlers[start:end] self.assertIn("data.completed_turn_commit === true", block) - self.assertIn("const persisted = runtimeSession.persistLiveSessionCheckpoint", block) - self.assertIn("window.JinPanels.persistRoomStateNow()", block) + self.assertIn("withCurrentRoomState(", block) + self.assertIn("runtimeSession.persistLiveSessionCheckpoint", block) + self.assertNotIn("window.JinPanels.persistRoomStateNow()", block) def test_restore_merges_checkpoint_session_actions(self): source = (ROOT / "ui" / "static" / "js" / "session-restore.js").read_text(encoding="utf-8") @@ -218,9 +234,9 @@ def test_restore_merges_checkpoint_session_actions(self): end = source.index("function restoreVisualState(", start) block = source[start:end] - self.assertIn("Array.isArray(snapshot.session_actions)", block) - self.assertIn("merged.session_actions = snapshot.session_actions", block) - self.assertNotIn("rejectCheckpointColor", block) + self.assertIn("const candidates = [];", block) + self.assertIn("Object.assign(merged, selected.snapshot);", block) + self.assertNotIn("actionSnapshot", block) diff --git a/tests/test_session_bootstrap_chat_tail.py b/tests/test_session_bootstrap_chat_tail.py index 4764f4d0..3aa1d968 100644 --- a/tests/test_session_bootstrap_chat_tail.py +++ b/tests/test_session_bootstrap_chat_tail.py @@ -18,7 +18,7 @@ class SessionBootstrapChatTailTests(unittest.TestCase): - def test_archive_tail_keeps_three_newest_complete_pairs_with_reasoning(self): + def test_archive_tail_keeps_three_newest_user_moves_with_reasoning(self): entries = [] reasoning = {} for turn in range(1, 5): @@ -46,14 +46,14 @@ def test_archive_tail_keeps_three_newest_complete_pairs_with_reasoning(self): self.assertEqual( [(turn["user"], turn["jin"]) for turn in turns], [ - ("user 1", "jin 1"), ("user 2", "jin 2"), + ("user 3", ""), ("user 4", "jin 4"), ], ) self.assertEqual( - [turn["reasoning"] for turn in turns], - ["reasoning 1", "reasoning 2", "reasoning 4"], + [turn.get("reasoning", "") for turn in turns], + ["reasoning 2", "", "reasoning 4"], ) def test_bootstrap_hydration_preserves_turn_reasoning_and_ui_tail(self): @@ -102,6 +102,24 @@ def test_bootstrap_hydration_preserves_turn_reasoning_and_ui_tail(self): ], ) + def test_bootstrap_tail_keeps_interrupted_user_move_without_jin(self): + context = SimpleNamespace( + runtime_recent_turns=[{ + "user": "я отправил и сразу остановил", + "jin": "", + "user_created_at": 100.0, + }], + ) + + self.assertEqual( + build_session_bootstrap_chat_tail(context), + [{ + "user": "я отправил и сразу остановил", + "jin": "", + "user_created_at": 100.0, + }], + ) + def test_bootstrap_tail_keeps_committed_user_only_action_turn(self): context = SimpleNamespace( runtime_recent_turns=[{ diff --git a/tests/test_session_bootstrap_lineage_client_contract.py b/tests/test_session_bootstrap_lineage_client_contract.py index 3daa7ee2..8dfc084b 100644 --- a/tests/test_session_bootstrap_lineage_client_contract.py +++ b/tests/test_session_bootstrap_lineage_client_contract.py @@ -33,13 +33,13 @@ class SessionBootstrapLineageClientContractTests(unittest.TestCase): def test_session_save_uses_current_runtime_id_not_restored_facts_id(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getCurrentSavedSessionId()") - end = source.index("function buildSessionSaveRuntimeSnapshot", start) + end = source.index("function normalizeLiveSessionSnapshot", start) block = source[start:end] self.assertIn("getCurrentRuntimeSessionId()", block) self.assertNotIn("getCurrentFactsMemorySessionId()", block) - def test_normal_boot_prefers_latest_live_runtime_snapshot(self): + def test_normal_boot_uses_common_checkpoint_and_only_matching_l1(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") end = source.index("function clearPersistedSessionBootstrap()", start) @@ -47,8 +47,13 @@ def test_normal_boot_prefers_latest_live_runtime_snapshot(self): self.assertIn("readLatestPreviousRuntimeMemory()", block) self.assertIn("browserLatestRuntimeMemory", block) - self.assertIn("saved_runtime.txt is a last-resort compatibility fallback", block) - self.assertIn("if (!sessionText && !runtimeText)", block) + self.assertIn( + "browserCheckpoint || legacyCompletedCheckpoint", + block, + ) + self.assertIn("String(record.session_id || \"\").trim()", block) + self.assertIn("=== checkpointSessionId", block) + self.assertIn("!runtimeMemory && !conversationCheckpoint", block) def test_boot_materializes_current_session_as_next_direct_predecessor(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") @@ -60,57 +65,19 @@ def test_boot_materializes_current_session_as_next_direct_predecessor(self): self.assertIn("bootstrap.source_session_id", block) self.assertIn("bootstrap.runtime_memory", block) - def test_boot_color_prefers_common_checkpoint_then_action_fallback(self): + def test_boot_color_has_one_local_and_one_server_reconciliation_path(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") - resolver_start = source.index("function resolveBootstrapJinColor(") - resolver_end = source.index( - "function applyBootstrapSceneTintShift(", - resolver_start, - ) - resolver = source[resolver_start:resolver_end] - - self.assertLess( - resolver.index("resolvePersistedJinColor()"), - resolver.index("source.session_actions"), - ) - self.assertLess( - resolver.index("source.session_actions"), - resolver.index("source.current_jin_color"), - ) - self.assertIn("readLatestSavedSessionSnapshot()", source) - self.assertIn('toUpperCase() !== "JIN_COLOR"', resolver) - self.assertIn("part.colors", resolver) + handlers = ( + ROOT / "ui" / "static" / "js" / "socket" / "event-handlers.js" + ).read_text(encoding="utf-8") - apply_start = source.index( - "function applyPersistedSessionBootstrap(bootstrap)" - ) - apply_end = source.index( - "function getPersistedSessionBootstrap()", - apply_start, - ) - apply_block = source[apply_start:apply_end] - self.assertIn("resolveBootstrapRoomState(bootstrap)", apply_block) - self.assertIn("delete localRoomState.avatar.color;", apply_block) - self.assertNotIn("applyBootstrapSceneTintShift(", apply_block) + self.assertNotIn("resolveBootstrapJinColor", source) + self.assertNotIn("applyBootstrapSceneTintShift", source) + self.assertIn("data.current_jin_color", handlers) + self.assertIn("initialBootstrap: true", handlers) + self.assertIn("persist: false", handlers) def test_early_room_restore_uses_common_checkpoint_color(self): - runtime_source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") - helper_start = runtime_source.index( - "function resolveBootstrapRoomState(" - ) - helper_end = runtime_source.index( - "function applyBootstrapSceneTintShift(", - helper_start, - ) - helper = runtime_source[helper_start:helper_end] - - self.assertIn("resolveBootstrapJinColor(source)", helper) - self.assertIn("color: restoredColor", helper) - self.assertIn( - "session.resolveBootstrapRoomState = resolveBootstrapRoomState", - runtime_source, - ) - logger_source = ( ROOT / "ui" / "static" / "js" / "logger" / "logger.js" ).read_text(encoding="utf-8") @@ -126,6 +93,15 @@ def test_early_room_restore_uses_common_checkpoint_color(self): self.assertIn("roomState.avatar.color = color;", stored_block) self.assertNotIn("delete roomState.avatar.color;", stored_block) + init_start = logger_source.index("function initRoomStatePersistence()") + init_end = logger_source.index( + "function clearConsoleStreamDetachTimer()", + init_start, + ) + init_block = logger_source[init_start:init_end] + self.assertIn("initialBootstrapColor: true", init_block) + self.assertNotIn("applyBootstrapSceneTintShift", init_block) + def test_live_bootstrap_preserves_tool_results(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function normalizeLiveSessionSnapshot(") @@ -153,7 +129,8 @@ def test_live_runtime_snapshot_keeps_direct_predecessor_link(self): self.assertIn("setBootSourceRuntimeSessionId", source) self.assertIn("readLatestPreviousRuntimeMemory", source) self.assertIn("collectOtherLatestRuntimeMemorySnapshots", source) - self.assertIn("saved_at:\n new Date().toISOString()", source) + self.assertIn("Opening a tab is not conversation activity", source) + self.assertIn("|| new Date().toISOString()", source) def test_persisted_runtime_snapshot_keeps_its_origin_session_id(self): storage_source = RUNTIME_STORAGE_JS.read_text(encoding="utf-8") diff --git a/ui/static/css/base.css b/ui/static/css/base.css index d8116d04..27ca433b 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -68,7 +68,7 @@ body.jin-scrollbar-active, --jin-color: #1f4f8f; --scene-base-color: #122540; --scene-jin-tint-alpha: 0.048; - --scene-jin-tint-transition-duration: 0.22s; + --scene-jin-tint-transition-duration: 333ms; --scene-layer-base: 0; --scene-layer-context: 1; diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 5a90bcc5..d50260d9 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -916,14 +916,14 @@ .jin-avatar-center-core, .jin-avatar-center-point { transition: - fill var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out, - fill-opacity var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out, - opacity var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out; + fill var(--jin-avatar-center-color-transition-duration, 333ms) ease-out, + fill-opacity var(--jin-avatar-center-color-transition-duration, 333ms) ease-out, + opacity var(--jin-avatar-center-color-transition-duration, 333ms) ease-out; } .jin-avatar-center-glow-stop { transition: - stop-color var(--jin-avatar-center-color-transition-duration, 0.16s) ease-out; + stop-color var(--jin-avatar-center-color-transition-duration, 333ms) ease-out; } .jin-avatar-orbit-entry { diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 5233b9e7..3cfa855a 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -3911,7 +3911,14 @@ function applyRoomState(roomState, options = {}) { && avatarApi && typeof avatarApi.setCenterColor === "function" ) { - avatarApi.setCenterColor(avatarState.color); + avatarApi.setCenterColor( + avatarState.color, + { + initialBootstrap: + options.initialBootstrapColor === true, + persist: options.persist !== false, + } + ); } if (finiteRoomNumber(avatarState.speed_px_per_second) > 0) { @@ -4024,16 +4031,11 @@ function applyRoomState(roomState, options = {}) { return true; } -function persistRoomStateNow(options = {}) { - const colorOnly = options.colorOnly === true; - +function persistRoomStateNow() { if ( - !colorOnly - && ( - !roomStatePersistenceEnabled - || applyingRoomState - || roomStateRestoreInProgress - ) + !roomStatePersistenceEnabled + || applyingRoomState + || roomStateRestoreInProgress ) { return false; } @@ -4064,58 +4066,6 @@ function persistRoomStateNow(options = {}) { return false; } - if (colorOnly) { - const avatarApi = - window.JinRuntime - && window.JinRuntime.avatar; - const color = - String( - avatarApi - && typeof avatarApi.getCenterColor === "function" - ? avatarApi.getCenterColor() - : "" - ) - .trim() - .toLowerCase(); - - if (!/^#[0-9a-f]{6}$/.test(color)) { - return false; - } - - const previousSnapshot = - checkpoint.session_snapshot; - const nextSnapshot = { - ...previousSnapshot, - current_jin_color: color, - }; - - if (isRoomStateObject(previousSnapshot.room_state)) { - const previousRoomState = - previousSnapshot.room_state; - const previousAvatar = - isRoomStateObject(previousRoomState.avatar) - ? previousRoomState.avatar - : {}; - - nextSnapshot.room_state = { - ...previousRoomState, - avatar: { - ...previousAvatar, - color, - }, - }; - } - - // JIN_COLOR is a field-local mutation of this same checkpoint. Do not - // advance saved_at or replace its owner, lineage, actions or room data. - storage.writeLatestSavedSessionSnapshot({ - ...checkpoint, - session_snapshot: nextSnapshot, - }); - - return true; - } - const currentSessionId = typeof storage.getCurrentRuntimeSessionId === "function" ? String( @@ -4126,9 +4076,10 @@ function persistRoomStateNow(options = {}) { const checkpointSessionId = String(checkpoint.session_id || "").trim(); - // The room state is part of the same bootstrap checkpoint as its action - // history. A newly opened/background tab must not repaint the predecessor - // checkpoint before it has completed a visible turn and taken ownership. + // Room/avatar writes are field-local. They may update the current + // session checkpoint, but they never decide that a freshly opened tab + // became a new conversation. The full checkpoint path does that only + // after real session activity; until then leave the predecessor intact. if ( currentSessionId && checkpointSessionId @@ -4173,12 +4124,10 @@ function persistRoomStateNow(options = {}) { }; } - storage.writeLatestSavedSessionSnapshot({ + return storage.writeLatestSavedSessionSnapshot({ ...checkpoint, session_snapshot: sessionSnapshot, }); - - return true; } function scheduleRoomStatePersist() { @@ -4333,8 +4282,11 @@ function initRoomStatePersistence() { window.addEventListener( "jin:avatar-room-state-changed", - () => { - persistRoomStateNow({ colorOnly: true }); + (event) => { + if (event.detail && event.detail.immediate === true) { + persistRoomStateNow(); + return; + } scheduleRoomStatePersist(); } ); @@ -4346,35 +4298,14 @@ function initRoomStatePersistence() { const storedRoomState = getStoredRoomState(); if (storedRoomState) { - const applyStoredRoomState = () => applyRoomState( + applyRoomState( storedRoomState, { persist: false, animateTint: false, + initialBootstrapColor: true, } ); - const color = - String( - storedRoomState.avatar - && storedRoomState.avatar.color - || "" - ).trim(); - const sceneTintShift = - window.JinRuntime - && window.JinRuntime.session - && typeof window.JinRuntime.session.applyBootstrapSceneTintShift - === "function" - ? window.JinRuntime.session.applyBootstrapSceneTintShift - : null; - - if (color && sceneTintShift) { - sceneTintShift( - applyStoredRoomState, - color - ); - } else { - applyStoredRoomState(); - } enableRoomStatePersistence(false); return; diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index b0ab909e..a3d53f85 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -61,8 +61,8 @@ const MEMORY_SIGNAL_KIND_ORDER = Object.freeze(["delayed", "l4", "active"]); const SNAPSHOT_GLOW_CLEAR_DELAY_MS = 360; - const CENTER_COLOR_STEP_MS = 120; - const DEFAULT_CENTER_COLOR_TRANSITION_MS = 2000; + const INITIAL_BOOTSTRAP_COLOR_TRANSITION_MS = 2000; + const DEFAULT_CENTER_COLOR_TRANSITION_MS = 333; const CENTER_COLOR_TRANSITION_RESET_BUFFER_MS = 80; const MEMORY_LAYERS_HIDDEN_CLASS = "is-memory-layers-hidden"; const MEMORY_LAYERS_DORMANT_CLASS = "is-memory-layers-dormant"; @@ -139,9 +139,8 @@ } let centerColor = DEFAULT_CENTER_COLOR; - let centerColorTransitionQueue = []; - let centerColorTransitionTimer = null; let centerColorTransitionStyleTimer = null; + let initialBootstrapColorPending = true; const reasoningMotionSources = new Set(); const requestedAnimationPlaybackRates = new WeakMap(); let avatarRotationAnimationsCache = null; @@ -4815,9 +4814,16 @@ lastAuxiliaryRenderSignatures = auxiliarySignatures; } - function notifyRoomStateChanged() { + function notifyRoomStateChanged(options = {}) { window.dispatchEvent( - new CustomEvent("jin:avatar-room-state-changed") + new CustomEvent( + "jin:avatar-room-state-changed", + { + detail: { + immediate: options.immediate === true, + }, + } + ) ); } @@ -4831,7 +4837,7 @@ ); } - function applyCenterColor(color) { + function applyCenterColor(color, options = {}) { const svg = avatarRoot.querySelector("svg"); const overallColor = avatarRoot.style.getPropertyValue("--jin-avatar-overall-color").trim() @@ -4851,7 +4857,9 @@ String(0.12 * sceneColorIntensity) ); - notifyRoomStateChanged(); + if (options.persist !== false) { + notifyRoomStateChanged({ immediate: true }); + } if (!svg) { renderAvatar(getLatestSnapshot(), { @@ -4916,8 +4924,8 @@ durationValue ); - // Commit the transition duration before changing either the SVG paint or - // the scene variables. This keeps both projections on the same 2s ramp. + // Commit the duration before changing either projection so avatar and + // scene tint always share one transition. avatarRoot.getBoundingClientRect(); centerColorTransitionStyleTimer = window.setTimeout( @@ -4936,29 +4944,11 @@ return duration; } - function processCenterColorQueue() { - const nextTransition = centerColorTransitionQueue.shift(); - - if (!nextTransition) { - centerColorTransitionTimer = null; - return; - } - - const transitionDurationMs = prepareCenterColorTransition( - nextTransition.transitionDurationMs - ); - - applyCenterColor(nextTransition.color); - - centerColorTransitionTimer = - window.setTimeout( - processCenterColorQueue, - Math.max(CENTER_COLOR_STEP_MS, transitionDurationMs) - ); - } - function setCenterColor(color, options = {}) { const normalizedColor = normalizeHexColor(color); + const initialBootstrap = Boolean( + options && options.initialBootstrap === true + ); const hasExplicitTransitionDuration = Boolean( options && Object.prototype.hasOwnProperty.call( @@ -4971,28 +4961,30 @@ 0, Number(options.transitionDurationMs) || 0 ) - : DEFAULT_CENTER_COLOR_TRANSITION_MS; + : ( + initialBootstrap + && initialBootstrapColorPending + ? INITIAL_BOOTSTRAP_COLOR_TRANSITION_MS + : DEFAULT_CENTER_COLOR_TRANSITION_MS + ); if (!normalizedColor) { return false; } - if ( - normalizedColor === centerColor - && centerColorTransitionQueue.length === 0 - ) { + + + if (normalizedColor === centerColor) { return true; } - centerColorTransitionQueue.push({ - color: normalizedColor, - transitionDurationMs, - }); - - if (!centerColorTransitionTimer) { - processCenterColorQueue(); + if (initialBootstrap && initialBootstrapColorPending) { + initialBootstrapColorPending = false; } + prepareCenterColorTransition(transitionDurationMs); + applyCenterColor(normalizedColor, options); + return true; } diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 59c814eb..39087eaa 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -11,9 +11,6 @@ isReconnectInitialRuntimeMemoryUpdate: notInitialized, isLatestRuntimeMemoryDuplicate: notInitialized, isBootstrapRuntimeMemoryDuplicate: notInitialized, - resolveBootstrapJinColor: notInitialized, - resolveBootstrapRoomState: notInitialized, - applyBootstrapSceneTintShift: notInitialized, applyBootstrapRuntimeMemoryUpdate: notInitialized, }; @@ -84,9 +81,6 @@ let lastStableRuntimeMemorySnapshot = null; let persistedSessionBootstrapCleared = false; let hasUnsavedSessionActivity = false; - const BOOTSTRAP_SCENE_TINT_SHIFT_MS = 2000; - let bootstrapSceneTintShiftSequence = 0; - let activeBootstrapSceneTintColor = ""; function getCurrentSavedSessionId() { return String( @@ -95,323 +89,6 @@ ).trim(); } - function prefersReducedSceneTintMotion() { - return Boolean( - window.matchMedia - && window.matchMedia( - "(prefers-reduced-motion: reduce)" - ).matches - ); - } - - function normalizeBootstrapJinColor(value) { - const match = String(value || "") - .trim() - .match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i); - - if (!match) { - return ""; - } - - let color = match[1].toLowerCase(); - - if (color.length === 3) { - color = color - .split("") - .map(char => char + char) - .join(""); - } - - return `#${color}`; - } - - function resolvePersistedJinColor() { - const checkpoint = - readLatestSavedSessionSnapshot(); - const snapshot = - checkpoint - && checkpoint.session_snapshot - && typeof checkpoint.session_snapshot === "object" - && !Array.isArray(checkpoint.session_snapshot) - ? checkpoint.session_snapshot - : null; - - if (!snapshot) { - return ""; - } - - return normalizeBootstrapJinColor( - snapshot.current_jin_color - || ( - snapshot.room_state - && snapshot.room_state.avatar - && snapshot.room_state.avatar.color - ) - ); - } - - function resolveBootstrapJinColor(bootstrap) { - const source = - bootstrap - && typeof bootstrap === "object" - && !Array.isArray(bootstrap) - ? bootstrap - : {}; - const persistedColor = - resolvePersistedJinColor(); - - if (persistedColor) { - return persistedColor; - } - - const actions = Array.isArray(source.session_actions) - ? source.session_actions - : []; - - for (let actionIndex = actions.length - 1; actionIndex >= 0; actionIndex -= 1) { - const action = actions[actionIndex]; - const parts = - action - && Array.isArray(action.parts) - ? action.parts - : []; - - for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) { - const part = parts[partIndex]; - - if ( - !part - || typeof part !== "object" - || Array.isArray(part) - || String(part.text || "").trim().toUpperCase() !== "JIN_COLOR" - ) { - continue; - } - - const colors = Array.isArray(part.colors) - ? part.colors - : []; - - for (let colorIndex = colors.length - 1; colorIndex >= 0; colorIndex -= 1) { - const color = normalizeBootstrapJinColor( - colors[colorIndex] - ); - - if (color) { - return color; - } - } - } - } - - const currentColor = normalizeBootstrapJinColor( - source.current_jin_color - ); - - if (currentColor) { - return currentColor; - } - - return normalizeBootstrapJinColor( - source.room_state - && source.room_state.avatar - && source.room_state.avatar.color - ); - } - - function resolveBootstrapRoomState(bootstrap) { - const source = - bootstrap - && typeof bootstrap === "object" - && !Array.isArray(bootstrap) - ? bootstrap - : {}; - const roomState = - source.room_state - && typeof source.room_state === "object" - && !Array.isArray(source.room_state) - ? source.room_state - : null; - - if (!roomState) { - return null; - } - - const restoredColor = - resolveBootstrapJinColor(source); - const sourceAvatarState = - roomState.avatar - && typeof roomState.avatar === "object" - && !Array.isArray(roomState.avatar) - ? roomState.avatar - : {}; - - if (!restoredColor) { - return roomState; - } - - return { - ...roomState, - avatar: { - ...sourceAvatarState, - color: restoredColor, - }, - }; - } - - function clearBootstrapSceneTintInlineStyles( - sceneMain, - sceneTint - ) { - if (sceneMain) { - sceneMain.style.removeProperty("transition"); - sceneMain.style.removeProperty("background-color"); - } - - if (sceneTint) { - sceneTint.style.removeProperty("transition"); - sceneTint.style.removeProperty("background-color"); - sceneTint.style.removeProperty("opacity"); - } - } - - function applyBootstrapSceneTintShift( - apply, - targetColor = "" - ) { - if (typeof apply !== "function") { - return false; - } - - const normalizedTargetColor = - String(targetColor || "").trim().toLowerCase(); - - if ( - normalizedTargetColor - && normalizedTargetColor === activeBootstrapSceneTintColor - ) { - return Boolean(apply()); - } - - bootstrapSceneTintShiftSequence += 1; - const sequence = - bootstrapSceneTintShiftSequence; - - const sceneMain = - document.querySelector("main"); - const sceneTint = - document.getElementById("scene-jin-tint"); - - if ( - activeBootstrapSceneTintColor - && normalizedTargetColor !== activeBootstrapSceneTintColor - ) { - // A newer authoritative bootstrap color is retargeting an in-flight - // local checkpoint transition. Release that helper's inline paint - // first; otherwise its stale color becomes the next cleanup baseline. - clearBootstrapSceneTintInlineStyles( - sceneMain, - sceneTint - ); - } - - if ( - prefersReducedSceneTintMotion() - || (!sceneMain && !sceneTint) - ) { - return Boolean(apply()); - } - - activeBootstrapSceneTintColor = - normalizedTargetColor; - - const root = - document.documentElement; - - if (sceneMain) { - const style = - window.getComputedStyle(sceneMain); - sceneMain.style.transition = "none"; - sceneMain.style.backgroundColor = - style.backgroundColor; - } - - if (sceneTint) { - const style = - window.getComputedStyle(sceneTint); - sceneTint.style.transition = "none"; - sceneTint.style.backgroundColor = - style.backgroundColor; - sceneTint.style.opacity = - style.opacity; - } - - if (sceneMain) { - sceneMain.getBoundingClientRect(); - } else if (sceneTint) { - sceneTint.getBoundingClientRect(); - } - - const applied = - Boolean(apply()); - const rootStyle = - window.getComputedStyle(root); - const targetMainColor = - rootStyle.getPropertyValue("--scene-base-color").trim(); - const targetTintColor = - rootStyle.getPropertyValue("--jin-color").trim(); - const targetTintOpacity = - rootStyle.getPropertyValue("--scene-jin-tint-alpha").trim(); - - const transition = - `background-color ${BOOTSTRAP_SCENE_TINT_SHIFT_MS}ms ease`; - - // Do not rely on a custom-property duration changing in the same style - // recalculation as the restored color. On a fresh tab that can collapse - // into one paint and make the inherited room color appear instantly. - // Keep the actually painted colors inline, then explicitly release them - // towards the restored values on the next frame. - if (sceneMain) { - sceneMain.style.transition = transition; - } - - if (sceneTint) { - sceneTint.style.transition = - `${transition}, opacity ${BOOTSTRAP_SCENE_TINT_SHIFT_MS}ms ease`; - } - - window.requestAnimationFrame(() => { - if (sceneMain && targetMainColor) { - sceneMain.style.backgroundColor = - targetMainColor; - } - - if (sceneTint && targetTintColor) { - sceneTint.style.backgroundColor = - targetTintColor; - } - - if (sceneTint && targetTintOpacity) { - sceneTint.style.opacity = - targetTintOpacity; - } - - window.setTimeout(() => { - if (sequence !== bootstrapSceneTintShiftSequence) { - return; - } - - activeBootstrapSceneTintColor = ""; - clearBootstrapSceneTintInlineStyles( - sceneMain, - sceneTint - ); - }, BOOTSTRAP_SCENE_TINT_SHIFT_MS + 80); - }); - - return applied; - } - function normalizeLiveSessionSnapshot( data, fallbackSnapshot = null @@ -708,16 +385,19 @@ data && data.completed_turn_commit === true ); + const sessionMoved = Boolean( + hasUnsavedSessionActivity + || completedTurnCommit + ); - // The global checkpoint represents the latest completed conversation, - // not the most recently opened tab. A boot-only/stale tab may keep its - // own per-session L1 cache, but it must not steal bootstrap ownership - // through a reconnect/duplicate/background L1 echo. Ownership moves to - // another tab only when that tab finishes a visible USER -> JIN turn. + // Opening/reloading a tab creates a runtime id, not a new conversation. + // The common checkpoint switches to this session only after a real move. + // A user send marks activity immediately; completedTurnCommit is only a + // server-side fallback for paths that reached us without that UI mark. if ( previousCheckpoint && !sameSession - && !completedTurnCommit + && !sessionMoved ) { return false; } @@ -761,10 +441,9 @@ ? savedAt : previousConversationCommittedAt; - // Persist the conversation owner on this session's own record. Unlike - // saved_at (which advances on L1 updates/reconnects), this timestamp - // advances only after a visible USER -> JIN turn has completed. A blank - // tab or a late L1 echo therefore cannot become the next bootstrap owner. + // Keep completed-turn time separate from session movement. A USER-only + // interrupted session may already be the latest checkpoint, but this + // timestamp still advances only after a completed visible turn. if (completedTurnCommit) { writeLatestRuntimeMemory({ ...currentRuntime, @@ -794,18 +473,26 @@ ), }); - writeLatestSavedSessionSnapshot({ - version: 1, - session_id: currentSessionId, - previous_session_id: previousSessionId, - saved_at: savedAt, - conversation_committed_at: conversationCommittedAt, - loaded_memory_ids: - sessionSnapshot.loaded_memory_ids, - session_snapshot: sessionSnapshot, - }); + const checkpointWritten = + writeLatestSavedSessionSnapshot({ + version: 1, + session_id: currentSessionId, + previous_session_id: previousSessionId, + saved_at: savedAt, + conversation_committed_at: conversationCommittedAt, + loaded_memory_ids: + sessionSnapshot.loaded_memory_ids, + session_snapshot: sessionSnapshot, + }); - return true; + if (checkpointWritten) { + // The move is now represented by a full current-session checkpoint. + // Clear the dirty bit so a late background echo from this tab cannot + // later rewind a newer session that has already moved. + hasUnsavedSessionActivity = false; + } + + return Boolean(checkpointWritten); } function clearPersistedToolResultsCheckpoint() { @@ -1538,44 +1225,9 @@ bootstrap.runtime_snapshot || null, }); - // Do not advance the global conversation checkpoint on page load. - // This tab becomes the next bootstrap predecessor only after it - // completes a visible USER -> JIN turn. - } - - if ( - bootstrap - && bootstrap.room_state - && typeof bootstrap.room_state === "object" - && window.JinPanels - && typeof window.JinPanels.applyRoomState === "function" - ) { - const restoredRoomState = - resolveBootstrapRoomState(bootstrap); - const avatarState = - restoredRoomState.avatar - && typeof restoredRoomState.avatar === "object" - && !Array.isArray(restoredRoomState.avatar) - ? restoredRoomState.avatar - : {}; - const localRoomState = { - ...restoredRoomState, - avatar: { - ...avatarState, - }, - }; - - // This is the browser checkpoint sent before archive enrichment. - // Apply layout now, but let the authoritative bootstrap action event - // own the only visible color change from the built-in default. - delete localRoomState.avatar.color; - window.JinPanels.applyRoomState( - localRoomState, - { - persist: false, - animateTint: false, - } - ); + // Do not advance the common conversation checkpoint on page load. + // A real user move may advance it later even if generation is stopped + // before JIN finishes. } if ( @@ -1658,38 +1310,15 @@ ? readLatestCompletedConversationRuntimeMemory() : null; - const parseCommitTimestamp = value => { - const parsed = Date.parse( - String(value || "").trim() - ); - return Number.isFinite(parsed) - ? parsed - : 0; - }; - const checkpointCommittedAt = - parseCommitTimestamp( - browserCheckpoint - && browserCheckpoint.conversation_committed_at - ); - const perSessionCommittedAt = - parseCommitTimestamp( - latestCompletedConversationRuntime - && latestCompletedConversationRuntime.conversation_committed_at - ); - const perSessionConversationWins = Boolean( - latestCompletedConversationRuntime - && perSessionCommittedAt - && ( - !checkpointCommittedAt - || perSessionCommittedAt > checkpointCommittedAt - ) + const isRuntimeRecord = value => Boolean( + value + && typeof value === "object" + && !Array.isArray(value) + && String(value.runtime_memory || "").trim() ); - - // Dialogue ownership comes from an actual completed turn, never from the - // most recently touched L1 record. This heals stale global checkpoints - // left by older builds while keeping empty/reconnected tabs ineligible. - const conversationCheckpoint = - perSessionConversationWins + const legacyCompletedCheckpoint = + !browserCheckpoint + && isRuntimeRecord(latestCompletedConversationRuntime) ? { version: 1, session_id: @@ -1703,7 +1332,8 @@ || latestCompletedConversationRuntime.conversation_committed_at || "", conversation_committed_at: - latestCompletedConversationRuntime.conversation_committed_at, + latestCompletedConversationRuntime.conversation_committed_at + || "", loaded_memory_ids: ( latestCompletedConversationRuntime.session_snapshot @@ -1726,7 +1356,13 @@ } : {}, } - : browserCheckpoint; + : null; + + // The common checkpoint names the last session that actually moved. + // Per-session records only provide its matching L1 payload, or a legacy + // fallback when no common checkpoint exists at all. + const conversationCheckpoint = + browserCheckpoint || legacyCompletedCheckpoint; const checkpointSessionId = String( @@ -1734,79 +1370,29 @@ && conversationCheckpoint.session_id || "" ).trim(); - const savedRuntimeSessionId = - String( - browserLatestSavedRuntimeMemory - && browserLatestSavedRuntimeMemory.session_id - || "" - ).trim(); - const pairedSavedRuntime = Boolean( - checkpointSessionId - && browserLatestSavedRuntimeMemory - && typeof browserLatestSavedRuntimeMemory === "object" - && !Array.isArray(browserLatestSavedRuntimeMemory) - && savedRuntimeSessionId === checkpointSessionId - && String( - browserLatestSavedRuntimeMemory.runtime_memory - || "" - ).trim() - ) - ? browserLatestSavedRuntimeMemory - : null; - const checkpointRuntime = - ( - checkpointSessionId - && collectOtherLatestRuntimeMemorySnapshots - ) + const otherRuntimeRecords = + collectOtherLatestRuntimeMemorySnapshots ? collectOtherLatestRuntimeMemorySnapshots() - .find(snapshot => ( - snapshot - && String(snapshot.session_id || "").trim() - === checkpointSessionId - && String(snapshot.runtime_memory || "").trim() - )) - : null; - const completedConversationRuntime = Boolean( - checkpointSessionId - && latestCompletedConversationRuntime - && String( - latestCompletedConversationRuntime.session_id - || "" - ).trim() === checkpointSessionId - && String( - latestCompletedConversationRuntime.runtime_memory - || "" - ).trim() - ) - ? latestCompletedConversationRuntime - : null; + : []; + const runtimeCandidates = [ + browserLatestSavedRuntimeMemory, + latestCompletedConversationRuntime, + ...( + Array.isArray(otherRuntimeRecords) + ? otherRuntimeRecords + : [] + ), + browserLatestRuntimeMemory, + ].filter(isRuntimeRecord); - let runtimeMemory = - completedConversationRuntime - || pairedSavedRuntime - || checkpointRuntime - || ( - browserLatestRuntimeMemory - && typeof browserLatestRuntimeMemory === "object" - && !Array.isArray(browserLatestRuntimeMemory) - && String( - browserLatestRuntimeMemory.runtime_memory - || "" - ).trim() - ? browserLatestRuntimeMemory - : null - ) - || ( - browserLatestSavedRuntimeMemory - && String( - browserLatestSavedRuntimeMemory.runtime_memory - || "" - ).trim() - ? browserLatestSavedRuntimeMemory - : null - ); + let runtimeMemory = checkpointSessionId + ? runtimeCandidates.find(record => ( + String(record.session_id || "").trim() + === checkpointSessionId + )) || null + : runtimeCandidates[0] || null; - if (!runtimeMemory) { + if (!runtimeMemory && !conversationCheckpoint) { const savedRuntimeFallback = getSavedRuntimeMemoryFallback(); @@ -1818,44 +1404,32 @@ } } - if (!runtimeMemory) { - return null; - } - const runtimeText = String( - runtimeMemory.runtime_memory + runtimeMemory && runtimeMemory.runtime_memory || "" ).trim(); - if (!runtimeText) { + if (!conversationCheckpoint && !runtimeText) { return null; } let sourceSessionId = String( - runtimeMemory.session_id + checkpointSessionId + || ( + runtimeMemory + && runtimeMemory.session_id + ) || ( - runtimeMemory.runtime_snapshot + runtimeMemory + && runtimeMemory.runtime_snapshot && runtimeMemory.runtime_snapshot.session_id ) || "" ).trim(); - const checkpointMatchesSource = Boolean( - conversationCheckpoint - && typeof conversationCheckpoint === "object" - && !Array.isArray(conversationCheckpoint) - && ( - !sourceSessionId - || String(conversationCheckpoint.session_id || "").trim() - === sourceSessionId - ) - ); - const checkpoint = - checkpointMatchesSource - ? conversationCheckpoint - : null; + const checkpoint = conversationCheckpoint; if (!sourceSessionId && checkpoint) { sourceSessionId = @@ -1868,8 +1442,8 @@ checkpoint && checkpoint.previous_session_id ) - || runtimeMemory.previous_session_id - || runtimeMemory.booted_from_session_id + || (runtimeMemory && runtimeMemory.previous_session_id) + || (runtimeMemory && runtimeMemory.booted_from_session_id) || "" ).trim(); const sessionSnapshot = @@ -1893,20 +1467,20 @@ ); } - const runtimeDisplaySnapshot = - buildRuntimeMemoryDisplaySnapshot({ - runtime_memory: runtimeText, - runtime_memory_updates: - Number( - runtimeMemory.runtime_memory_updates - || 0 - ), - runtime_snapshot: - runtimeMemory.runtime_snapshot || null, - source_session_id: sourceSessionId || null, - previous_session_id: previousSessionId || null, - }) - || buildDefaultRuntimeMemorySnapshot(); + const runtimeDisplaySnapshot = runtimeText + ? buildRuntimeMemoryDisplaySnapshot({ + runtime_memory: runtimeText, + runtime_memory_updates: + Number( + runtimeMemory.runtime_memory_updates + || 0 + ), + runtime_snapshot: + runtimeMemory.runtime_snapshot || null, + source_session_id: sourceSessionId || null, + previous_session_id: previousSessionId || null, + }) + : null; return { ...sessionSnapshot, @@ -1915,8 +1489,8 @@ previous_session_id: previousSessionId || null, saved_at: String( - runtimeMemory.saved_at - || (checkpoint && checkpoint.saved_at) + (checkpoint && checkpoint.saved_at) + || (runtimeMemory && runtimeMemory.saved_at) || "" ).trim(), loaded_memory_ids: @@ -1938,11 +1512,11 @@ runtime_memory: runtimeText, runtime_memory_updates: Number( - runtimeMemory.runtime_memory_updates + runtimeMemory && runtimeMemory.runtime_memory_updates || 0 ), runtime_snapshot: - runtimeMemory.runtime_snapshot || null, + runtimeMemory && runtimeMemory.runtime_snapshot || null, runtime_display_snapshot: runtimeDisplaySnapshot, }; } @@ -1983,9 +1557,6 @@ session.isReconnectInitialRuntimeMemoryUpdate = isReconnectInitialRuntimeMemoryUpdate; session.isLatestRuntimeMemoryDuplicate = isLatestRuntimeMemoryDuplicate; session.isBootstrapRuntimeMemoryDuplicate = isBootstrapRuntimeMemoryDuplicate; - session.resolveBootstrapJinColor = resolveBootstrapJinColor; - session.resolveBootstrapRoomState = resolveBootstrapRoomState; - session.applyBootstrapSceneTintShift = applyBootstrapSceneTintShift; session.applyBootstrapRuntimeMemoryUpdate = applyBootstrapRuntimeMemoryUpdate; session.rememberStableRuntimeSnapshot = rememberStableRuntimeSnapshot; diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 0384b338..4fdd18ed 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -446,8 +446,10 @@ key, JSON.stringify(value) ); + return true; } catch (error) { - // Browser memory is helpful, not required for chat. + console.warn("Failed to persist browser checkpoint", key, error); + return false; } } @@ -830,17 +832,22 @@ ) { if (shouldIsolateAnonymousStorage()) { - return; + return false; } - writeBrowserMemory( + const written = writeBrowserMemory( latestSavedSessionSnapshotStorageKey, normalizeSavedSessionSnapshot(value) ); + if (!written) { + return false; + } removeBrowserMemory( legacyLatestSavedSessionSnapshotStorageKey ); + return true; + } @@ -2110,8 +2117,8 @@ version: runtimeMemory.version || 1, // Opening a tab is not conversation activity. Keep the predecessor's - // causal timestamp until this tab produces a real L1 update / completed - // turn; otherwise empty tabs outrank the session the user actually used. + // causal timestamp until a real user move is checkpointed; otherwise an + // untouched tab can outrank the session the user actually used. saved_at: String(runtimeMemory.saved_at || "").trim() || new Date().toISOString(), diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index 0334d5d2..5e871fbd 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -388,208 +388,96 @@ ).trim(); const checkpoint = storage.readLatestSavedSessionSnapshot(); - const checkpointSnapshot = - checkpoint - && String(checkpoint.session_id || "").trim() - === restoreSourceSessionId - && checkpoint.session_snapshot - && typeof checkpoint.session_snapshot === "object" - ? checkpoint.session_snapshot - : null; - const completedRuntimeSnapshot = - restoreSourceSessionId - && typeof storage.collectOtherLatestRuntimeMemorySnapshots - === "function" - ? storage.collectOtherLatestRuntimeMemorySnapshots() - .find(item => ( - item - && String( - item.session_id - || item.key_session_id - || "" - ).trim() === restoreSourceSessionId - && item.session_snapshot - && typeof item.session_snapshot === "object" - && !Array.isArray(item.session_snapshot) - )) - : null; - const completedSnapshot = - completedRuntimeSnapshot - && completedRuntimeSnapshot.session_snapshot - ? completedRuntimeSnapshot.session_snapshot - : null; - - // Layout stays on the normal saved-session checkpoint. Actions and color - // prefer the completed record for this exact archived session, so a later - // room-state write cannot erase the final JIN_COLOR turn. - const snapshot = - checkpointSnapshot - || completedSnapshot; - const actionSnapshot = - completedSnapshot - || checkpointSnapshot; - - const savedRuntime = + const runtimeRecords = []; + const latestSavedRuntime = typeof storage.readLatestSavedRuntimeMemory === "function" ? storage.readLatestSavedRuntimeMemory() : null; - const savedRuntimeMatchesSource = Boolean( - savedRuntime - && typeof savedRuntime === "object" - && !Array.isArray(savedRuntime) - && String(savedRuntime.session_id || "").trim() - === restoreSourceSessionId - ); - - if (savedRuntimeMatchesSource) { - const runtimeMemory = - String(savedRuntime.runtime_memory || "").trim(); - const runtimeSnapshot = - savedRuntime.runtime_snapshot - && typeof savedRuntime.runtime_snapshot === "object" - && !Array.isArray(savedRuntime.runtime_snapshot) - ? savedRuntime.runtime_snapshot - : null; - if (runtimeMemory) { - merged.runtime_memory = runtimeMemory; - } - - if (runtimeSnapshot) { - merged.runtime_snapshot = { - ...runtimeSnapshot, - }; - } - - merged.runtime_memory_updates = Number( - savedRuntime.runtime_memory_updates - || merged.runtime_memory_updates - || 0 - ); + if (latestSavedRuntime) { + runtimeRecords.push(latestSavedRuntime); } - - if (!snapshot) { - return merged; - } - - let sessionActions = null; - if ( - actionSnapshot - && Array.isArray(actionSnapshot.session_actions) + typeof storage.collectOtherLatestRuntimeMemorySnapshots + === "function" ) { - sessionActions = actionSnapshot.session_actions - .filter(item => ( - item - && typeof item === "object" - && !Array.isArray(item) - )) - .slice(-200) - .map(item => ({ ...item })); - merged.session_actions = sessionActions; - } else if (Array.isArray(snapshot.session_actions)) { - // Compatibility fallback for checkpoints written before per-session - // completed snapshots existed. - merged.session_actions = snapshot.session_actions - .filter(item => ( - item - && typeof item === "object" - && !Array.isArray(item) - )) - .slice(-200) - .map(item => ({ ...item })); - sessionActions = merged.session_actions; - } - - const latestColorPart = - (sessionActions || []) - .slice() - .reverse() - .flatMap(item => ( - Array.isArray(item.parts) - ? item.parts.slice().reverse() - : [] - )) - .find(part => ( - part - && typeof part === "object" - && !Array.isArray(part) - && String(part.text || "") - .trim() - .toUpperCase() === "JIN_COLOR" - )); - const actionColor = - latestColorPart - && Array.isArray(latestColorPart.colors) - && latestColorPart.colors.length - ? String( - latestColorPart.colors[ - latestColorPart.colors.length - 1 - ] - || "" - ).trim() - : ""; - const color = - actionColor - || String( - actionSnapshot - && actionSnapshot.current_jin_color - || "" - ).trim() - || String(snapshot.current_jin_color || "").trim(); - - if (color) { - merged.current_jin_color = color; + const collected = + storage.collectOtherLatestRuntimeMemorySnapshots(); + if (Array.isArray(collected)) { + runtimeRecords.push(...collected); + } } - for (const key of [ - "current_jin_size", - "current_jin_position", - "current_window_size", - "room_state", - ]) { - const value = snapshot[key]; - + const timestamp = value => { + const parsed = Date.parse(String(value || "").trim()); + return Number.isFinite(parsed) ? parsed : 0; + }; + const candidates = []; + const addCandidate = (record, snapshot) => { if ( - value - && typeof value === "object" - && !Array.isArray(value) + !record + || String( + record.session_id + || record.key_session_id + || "" + ).trim() !== restoreSourceSessionId + || !snapshot + || typeof snapshot !== "object" + || Array.isArray(snapshot) ) { - if ( - key === "room_state" - && color - && value.avatar - && typeof value.avatar === "object" - && !Array.isArray(value.avatar) - ) { - merged[key] = { - ...value, - avatar: { - ...value.avatar, - color, - }, - }; - } else { - merged[key] = { ...value }; - } + return; } - } - const speed = - Number(snapshot.current_jin_speed || 0); + candidates.push({ + record, + snapshot, + savedAt: Math.max( + timestamp(record.conversation_committed_at), + timestamp(record.saved_at), + timestamp( + snapshot.room_state + && snapshot.room_state.saved_at + ) + ), + }); + }; + + addCandidate( + checkpoint, + checkpoint && checkpoint.session_snapshot + ); + runtimeRecords.forEach(record => { + addCandidate(record, record.session_snapshot); + }); + candidates.sort((left, right) => right.savedAt - left.savedAt); - if (speed > 0) { - merged.current_jin_speed = speed; + const selected = candidates[0]; + const archiveTailAt = timestamp(merged.archive_tail_at); + if ( + !selected + || (archiveTailAt && selected.savedAt < archiveTailAt) + ) { + return merged; } + Object.assign(merged, selected.snapshot); + + const runtimeMemory = + String(selected.record.runtime_memory || "").trim(); + if (runtimeMemory) { + merged.runtime_memory = runtimeMemory; + merged.runtime_memory_updates = Number( + selected.record.runtime_memory_updates || 0 + ); + } if ( - Object.prototype.hasOwnProperty.call( - snapshot, - "current_jin_collapsed" - ) + selected.record.runtime_snapshot + && typeof selected.record.runtime_snapshot === "object" + && !Array.isArray(selected.record.runtime_snapshot) ) { - merged.current_jin_collapsed = - Boolean(snapshot.current_jin_collapsed); + merged.runtime_snapshot = { + ...selected.record.runtime_snapshot, + }; } return merged; @@ -613,7 +501,10 @@ === "function" && window.JinPanels.applyRoomState( roomState, - { persist: false } + { + persist: false, + initialBootstrapColor: true, + } ) ) { return; @@ -633,7 +524,11 @@ === "function" ) { window.JinRuntime.avatar.setCenterColor( - color + color, + { + initialBootstrap: true, + persist: false, + } ); } @@ -691,7 +586,10 @@ memory_layers_hidden: false, }, }, - { persist: false } + { + persist: false, + initialBootstrapColor: true, + } ) ) { return; diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 4e334b09..629d6cc1 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -186,42 +186,20 @@ function handleSessionBootstrapChatTail( function handleSessionActionsUpdate( data ) { - - const runtimeSession = - window.JinRuntime - && window.JinRuntime.session; - if ( data && data.bootstrap_restore === true - && runtimeSession - && typeof runtimeSession.resolveBootstrapJinColor === "function" - ) { - const color = runtimeSession.resolveBootstrapJinColor({ - session_actions: - Array.isArray(data.items) - ? data.items - : [], - }); - const applyColor = () => Boolean( - color + && data.current_jin_color && window.JinRuntime.avatar && typeof window.JinRuntime.avatar.setCenterColor === "function" - && window.JinRuntime.avatar.setCenterColor(color) - ); - - if (color) { - if ( - typeof runtimeSession.applyBootstrapSceneTintShift === "function" - ) { - runtimeSession.applyBootstrapSceneTintShift( - applyColor, - color - ); - } else { - applyColor(); + ) { + window.JinRuntime.avatar.setCenterColor( + data.current_jin_color, + { + initialBootstrap: true, + persist: false, } - } + ); } if (window.updateSessionActionsLog) { @@ -383,6 +361,54 @@ function handleAgentRuntimeStart(data) { ); } +function withCurrentRoomState(sessionSnapshot) { + const roomState = + window.JinPanels + && typeof window.JinPanels.getRoomState === "function" + ? window.JinPanels.getRoomState( + sessionSnapshot.room_state || null + ) + : null; + const avatarState = + roomState + && roomState.avatar + && typeof roomState.avatar === "object" + ? roomState.avatar + : null; + + if (!avatarState) { + return sessionSnapshot; + } + + return { + ...sessionSnapshot, + room_state: roomState, + current_jin_color: avatarState.color, + current_jin_collapsed: Boolean(avatarState.collapsed), + current_jin_speed: Number( + avatarState.speed_px_per_second || 900 + ), + current_window_size: { + width: avatarState.window_width, + height: avatarState.window_height, + }, + ...( + avatarState.geometry_known + ? { + current_jin_size: { + width: avatarState.width, + height: avatarState.height, + }, + current_jin_position: { + x: avatarState.x, + y: avatarState.y, + }, + } + : {} + ), + }; +} + function handleAgentRuntimeEnd(data) { const runtimeSession = @@ -395,24 +421,14 @@ function handleAgentRuntimeEnd(data) { && runtimeSession && typeof runtimeSession.persistLiveSessionCheckpoint === "function" ) { - const persisted = runtimeSession.persistLiveSessionCheckpoint({ - session_snapshot: data.session_snapshot, + runtimeSession.persistLiveSessionCheckpoint({ + session_snapshot: withCurrentRoomState( + data.session_snapshot + ), completed_turn_commit: Boolean( data.completed_turn_commit === true ), }); - - // A fresh continuation cannot repaint its predecessor while booting. - // Right after the first real turn takes checkpoint ownership, save the - // actual client room state once so the just-applied JIN color is retained. - if ( - persisted - && data.completed_turn_commit === true - && window.JinPanels - && typeof window.JinPanels.persistRoomStateNow === "function" - ) { - window.JinPanels.persistRoomStateNow(); - } } if (data && data.retryable_response === true) { @@ -504,7 +520,9 @@ function handleMessageEnd( && typeof runtimeSession.persistLiveSessionCheckpoint === "function" ) { runtimeSession.persistLiveSessionCheckpoint({ - session_snapshot: data.session_snapshot, + session_snapshot: withCurrentRoomState( + data.session_snapshot + ), completed_turn_commit: Boolean( data.completed_turn_commit === true ), diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index f3172ce2..23d6573f 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -10,7 +10,7 @@ const JIN_VISUAL_SEQUENCE_ACTIONS = new Set([ "jin_speed", "jin_position", ]); -const JIN_VISUAL_SEQUENCE_COLOR_MS = 2000; +const JIN_VISUAL_SEQUENCE_COLOR_MS = 333; const JIN_VISUAL_SEQUENCE_SIZE_MS = 320; const JIN_VISUAL_SEQUENCE_CROSS_STAGE_RATIO = 0.58; const jinVisualSequenceBuffers = new Map(); diff --git a/ui/templates/index.html b/ui/templates/index.html index 462bee31..c53bdbd4 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,14 +10,14 @@ - + - + @@ -510,16 +510,16 @@ - + - + - + @@ -531,12 +531,12 @@ - + - - + + diff --git a/utils/actions/jin_visual_sequence_actions.py b/utils/actions/jin_visual_sequence_actions.py index 19986df2..1cf46d22 100644 --- a/utils/actions/jin_visual_sequence_actions.py +++ b/utils/actions/jin_visual_sequence_actions.py @@ -1,6 +1,6 @@ from __future__ import annotations -import contextlib +import logging import time from uuid import uuid4 @@ -22,6 +22,9 @@ from utils.chat_log import append_chat_runtime_event +LOGGER = logging.getLogger(__name__) + + JIN_VISUAL_ACTION_NAMES = { RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_JIN_SIZE, @@ -176,7 +179,9 @@ async def emit_jin_visual_sequences( elif action_name == RUNTIME_ACTION_JIN_COLOR: created_at = time.time() color = event["color"] + event_id = f"{sequence_id}:{sequence_index}" session_action = { + "id": event_id, "text": "JIN_COLOR", "created_at": created_at, "runtime_turn_id": str( @@ -192,11 +197,12 @@ async def emit_jin_visual_sequences( "colors": [color], }], } - with contextlib.suppress(Exception): + try: append_chat_runtime_event( context, event="runtime_action_request", payload={ + "event_id": event_id, "action": RUNTIME_ACTION_JIN_COLOR, "id": action_display_id, "color": color, @@ -205,6 +211,11 @@ async def emit_jin_visual_sequences( "created_at": created_at, }, ) + except Exception: + LOGGER.exception( + "Failed to persist JIN_COLOR runtime event %s", + event_id, + ) await emit(with_action_context({ "type": "runtime_action", diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index e664a40e..bf09e8c2 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -5,6 +5,7 @@ from utils.actions.action_counter_utils import ( format_runtime_action_count, ) +from utils.actions.jin_color_utils import normalize_jin_color_payload from utils.actions.jin_position_utils import ( normalize_jin_position_payload, ) @@ -3031,7 +3032,7 @@ async def emit_session_actions_update( current_sequence=current_sequence, ) - if not items: + if not items and not bootstrap_restore: return emitter = getattr( @@ -3048,7 +3049,7 @@ async def emit_session_actions_update( if emit is None: return - await emit({ + payload = { "type": "session_actions_update", "session_id": get_session_action_session_id( context @@ -3063,7 +3064,14 @@ async def emit_session_actions_update( ), "bootstrap_restore": bool(bootstrap_restore), "items": items, - }) + } + current_jin_color = normalize_jin_color_payload( + getattr(context, "jin_color", "") + ) + if current_jin_color: + payload["current_jin_color"] = current_jin_color + + await emit(payload) def mark_current_action_sequence( diff --git a/utils/session_restore.py b/utils/session_restore.py index d3320cfd..fe077520 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -351,17 +351,22 @@ def _build_recent_turns( turn["jin_created_at"] = timestamp ordered_turns.sort(key=lambda item: item[0]) - committed_turns = [] + visible_turns = [] for _, item in ordered_turns: - if not item.get("user") or not item.get("_jin_row_seen"): + # A real USER row is the session move. Keep it immediately even when + # generation was stopped before JIN produced a row. The missing JIN + # side stays empty, so interrupted and action-only turns remain + # distinguishable by their durable timestamps/rows rather than being + # rewritten as a completed exchange. + if not item.get("user"): continue - committed_turns.append({ + visible_turns.append({ key: value for key, value in item.items() if not key.startswith("_") }) - return committed_turns[-RECENT_MESSAGES_MAX_PAIRS:] + return visible_turns[-RECENT_MESSAGES_MAX_PAIRS:] def _read_reasoning(session_directory: Path, entries: list[dict]) -> dict[str, str]: @@ -839,35 +844,30 @@ def _build_runtime_event_session_actions(entries: list[dict]) -> list[dict]: ) if not color: continue - - if ( - items - and runtime_turn_id - and items[-1].get("runtime_turn_id") == runtime_turn_id - and len(items[-1].get("parts", [])) == 1 - and str( - items[-1]["parts"][0].get("text", "") - or "" - ).strip().upper() == "JIN_COLOR" - ): - colors = items[-1]["parts"][0].setdefault( - "colors", - [], - ) - if color not in colors: - colors.append(color) - continue - + raw_action = payload.get("session_action") + raw_action = raw_action if isinstance(raw_action, dict) else {} item = { - "text": "JIN_COLOR", + "text": str(raw_action.get("text", "") or "JIN_COLOR").strip(), "created_at": created_at, "parts": [{ "text": "JIN_COLOR", "colors": [color], }], } - if runtime_turn_id: - item["runtime_turn_id"] = runtime_turn_id + event_id = str( + raw_action.get("id", "") + or payload.get("event_id", "") + or "" + ).strip() + if event_id: + item["id"] = event_id + action_turn_id = str( + raw_action.get("runtime_turn_id", "") + or runtime_turn_id + or "" + ).strip() + if action_turn_id: + item["runtime_turn_id"] = action_turn_id items.append(item) continue @@ -893,6 +893,13 @@ def _build_runtime_event_session_actions(entries: list[dict]) -> list[dict]: "created_at": created_at, "parts": [part], } + event_id = str( + raw_action.get("id", "") + or payload.get("event_id", "") + or "" + ).strip() + if event_id: + item["id"] = event_id if runtime_turn_id: item["runtime_turn_id"] = runtime_turn_id items.append(item) @@ -1201,11 +1208,11 @@ def find_latest_completed_session_restore_payload( root: Path | str | None = None, anonymous_mode: bool | None = None, ) -> dict | None: - """Return the newest raw-log session containing a committed user turn. + """Return the newest raw-log session containing a real user move. - A normal USER/JIN pair qualifies, and so does a USER followed by an empty - JIN row after an action-only response. A bare USER row does not qualify, so - bootstrap-only/in-flight sessions remain ignored. + A USER row qualifies immediately, even if generation was stopped before a + JIN row or L1 update. Bootstrap-only sessions with no real USER row do not + qualify, so merely opening/stopping a fresh tab keeps the predecessor. """ root_path = Path( root @@ -1419,7 +1426,17 @@ def build_archived_session_restore_payload( ) + _build_runtime_event_session_actions(entries) )[-200:] - if runtime_session_actions: + runtime_has_l4_action = any( + isinstance(item, dict) + and any( + isinstance(part, dict) + and str(part.get("text", "") or "").strip().upper() + == "UPDATE_L4_FACTS" + for part in item.get("parts", []) or [] + ) + for item in runtime_session_actions + ) + if runtime_has_l4_action: session_actions = [ item for item in session_actions @@ -1433,6 +1450,7 @@ def build_archived_session_restore_payload( ) ) ] + if runtime_session_actions: session_actions.extend(runtime_session_actions) elif not any( isinstance(item, dict) diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 200e52d3..e4abe409 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -443,6 +443,18 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: "text": text, } + for identity_field, limit in ( + ("id", 200), + ("event_id", 200), + ("runtime_turn_id", 120), + ): + identity_value = clean_bootstrap_memory( + item.get(identity_field, ""), + limit=limit, + ) + if identity_value: + normalized_item[identity_field] = identity_value + item_session_id = clean_bootstrap_memory( item.get( "session_id", @@ -522,8 +534,7 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: for char in color ) normalized_color = f"#{color}" - if normalized_color not in colors: - colors.append(normalized_color) + colors.append(normalized_color) if colors: normalized_part["colors"] = colors @@ -2352,7 +2363,8 @@ def _bootstrap_recent_turns_tail_timestamp(value) -> float: def _bootstrap_session_action_identity(item: dict) -> str: explicit_id = clean_bootstrap_memory( - item.get("id", ""), + item.get("id", "") + or item.get("event_id", ""), limit=200, ) if explicit_id: @@ -2375,33 +2387,49 @@ def _bootstrap_session_action_identity(item: dict) -> str: ) ] if isinstance(item.get("parts"), list) else [] - if runtime_turn_id and part_names: - return "turn:" + runtime_turn_id + ":" + "|".join(part_names) - try: - created_at = int(float(item.get("created_at", 0) or 0)) + created_at = float(item.get("created_at", 0) or 0) except (TypeError, ValueError): created_at = 0 text = clean_bootstrap_memory( item.get("text", ""), limit=2000, ) - colors = [] + normalized_parts = [] for part in item.get("parts", []) if isinstance(item.get("parts"), list) else []: if not isinstance(part, dict): continue + normalized_part = { + "text": clean_bootstrap_memory( + part.get("text", ""), + limit=600, + ).upper(), + "detail": clean_bootstrap_memory( + part.get("detail", ""), + limit=1200, + ), + "message": clean_bootstrap_memory( + part.get("message", ""), + limit=2400, + ), + "id": clean_bootstrap_memory( + part.get("id", ""), + limit=200, + ), + } raw_colors = part.get("colors", []) if isinstance(raw_colors, (str, bytes)): raw_colors = [raw_colors] if isinstance(raw_colors, list): - colors.extend( + normalized_part["colors"] = [ str(color or "").strip().lower() for color in raw_colors if str(color or "").strip() - ) + ] + normalized_parts.append(normalized_part) return json.dumps( - [created_at, text, part_names, colors], + [created_at, runtime_turn_id, text, part_names, normalized_parts], ensure_ascii=False, separators=(",", ":"), ) @@ -2498,6 +2526,7 @@ def enrich_session_bootstrap_from_archive( ) if not source_session_id: return message_data + browser_source_session_id = source_session_id try: from utils.session_restore import ( @@ -2520,8 +2549,9 @@ def enrich_session_bootstrap_from_archive( # source_session_id comes from browser persistence and can be stale. The # raw JSONL dialogue is authoritative: if another session has a strictly - # newer COMPLETE USER/JIN turn, continue from that session instead. Empty - # bootstrap-only sessions never qualify for this override. + # newer real USER move, continue from that session immediately. A stopped + # generation may therefore restore as USER-only; opening a blank tab still + # does nothing because bootstrap-only sessions contain no real USER row. if isinstance(latest_completed_archive, dict): latest_source_session_id = clean_bootstrap_memory( latest_completed_archive.get( @@ -2676,12 +2706,33 @@ def enrich_session_bootstrap_from_archive( if value not in (None, "", [], {}): enriched[field] = value - # Actions have their own causal timestamps and are recovered from raw-log - # predecessor lineage. Merge that exact stream independently of saved_at: - # saved_at can advance after a harmless browser room-state/L1 write, while - # the JSONL action is still the authoritative missing middle event. - browser_actions = enriched.get("session_actions", []) + # The common checkpoint owns all actions committed before its saved_at. + # Raw JSONL contributes only a newer tail (or the whole fallback when the + # checkpoint has no actions), so the same marker cannot appear once from + # localStorage and again from the file log. + source_changed = source_session_id != browser_source_session_id + browser_actions = ( + [] + if source_changed + else enriched.get("session_actions", []) + ) archive_actions = archived.get("session_actions", []) + if ( + browser_actions + and isinstance(archive_actions, list) + and not source_changed + ): + checkpoint_timestamp = _bootstrap_iso_timestamp( + message_data.get("saved_at", "") + ) + if checkpoint_timestamp: + archive_actions = [ + item + for item in archive_actions + if isinstance(item, dict) + and _bootstrap_session_action_timestamp(item) + > checkpoint_timestamp + ] if archive_actions not in (None, "", [], {}): enriched["session_actions"] = ( _merge_bootstrap_session_actions( @@ -2690,8 +2741,12 @@ def enrich_session_bootstrap_from_archive( ) ) - browser_color = normalize_jin_color_payload( - enriched.get("current_jin_color", "") + browser_color = ( + "" + if source_changed + else normalize_jin_color_payload( + enriched.get("current_jin_color", "") + ) ) if browser_color: # The browser checkpoint is updated in the same tick that JIN_COLOR is @@ -2841,9 +2896,9 @@ def build_session_bootstrap_chat_tail( turn.get("jin", ""), limit=12000, ) - # runtime_recent_turns contains committed turns. An action-only - # completion can legitimately have no visible JIN text, but its USER - # bubble still belongs to the predecessor chat tail. + # runtime_recent_turns contains the latest real USER moves. A stopped + # turn or action-only completion can legitimately have no visible JIN + # text; its USER bubble still belongs to the predecessor chat tail. if not user_text: continue @@ -3110,6 +3165,9 @@ def apply_session_bootstrap( return bool( runtime_memory + or source_session_id + or getattr(context, "runtime_session_action_history", []) + or getattr(context, "runtime_recent_turns", []) ) From 790bcb3cd1675eedae67f576621ff0c0076c9337 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:24:14 +0300 Subject: [PATCH 099/168] chore: bootstrap - stabilize bootstrap logic --- .../test_bootstrap_color_action_regression.py | 2 +- ...st_jin_color_transition_client_contract.py | 7 +++- tests/test_live_session_checkpoint.py | 3 ++ ...ssion_bootstrap_lineage_client_contract.py | 3 +- ui/static/js/logger/logger.js | 35 +++++++++++++++---- ui/static/js/runtime/runtime-avatar.js | 8 ++--- ui/static/js/socket/event-handlers.js | 2 +- ui/templates/index.html | 4 +-- 8 files changed, 48 insertions(+), 16 deletions(-) diff --git a/tests/test_bootstrap_color_action_regression.py b/tests/test_bootstrap_color_action_regression.py index 4bbc3720..5334f52e 100644 --- a/tests/test_bootstrap_color_action_regression.py +++ b/tests/test_bootstrap_color_action_regression.py @@ -208,7 +208,7 @@ def test_server_emits_one_authoritative_bootstrap_color(self): self.assertIn("data.bootstrap_restore === true", handler_source) self.assertIn("data.current_jin_color", handler_source) self.assertIn("initialBootstrap: true", handler_source) - self.assertIn("persist: false", handler_source) + self.assertIn("persist: true", handler_source) self.assertIn('payload["current_jin_color"]', history_source) self.assertNotIn("resolveBootstrapJinColor", handler_source) self.assertNotIn("applyBootstrapSceneTintShift", handler_source) diff --git a/tests/test_jin_color_transition_client_contract.py b/tests/test_jin_color_transition_client_contract.py index 1145b911..020bdfb6 100644 --- a/tests/test_jin_color_transition_client_contract.py +++ b/tests/test_jin_color_transition_client_contract.py @@ -92,6 +92,9 @@ def test_common_checkpoint_color_is_immediate_within_same_session_and_boots_firs self.assertNotIn("saved_at:", persist_block) self.assertNotIn("colorOnly", persist_block) self.assertIn("currentSessionId !== checkpointSessionId", persist_block) + self.assertIn("&& !reconcileCurrentColor", persist_block) + self.assertIn("roomStateColorReconcilePending = true;", persist_block) + self.assertIn("roomStateColorReconcilePending = false;", persist_block) self.assertLess( persist_block.index("currentSessionId !== checkpointSessionId"), persist_block.index("const roomState = getRoomState(previousRoomState);"), @@ -106,6 +109,7 @@ def test_common_checkpoint_color_is_immediate_within_same_session_and_boots_firs init_block, ) self.assertIn("event.detail.immediate === true", init_block) + self.assertIn("reconcileCurrentColor: true", init_block) self.assertNotIn("colorOnly", init_block) self.assertNotIn("applyBootstrapSceneTintShift", init_block) self.assertIn("enableRoomStatePersistence(false);", init_block) @@ -144,7 +148,8 @@ def test_changed_assets_have_matching_cache_bumps(self): self.assertIn("runtime-session.js?v=", index_source) self.assertIn("&room-state=8&", index_source) self.assertIn("logger.js?v=", index_source) - self.assertIn("&room-state=6&", index_source) + self.assertIn("&room-state=7&", index_source) + self.assertIn("&bootstrap-color=2", index_source) if __name__ == "__main__": diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index e8b1d74e..f41f22db 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -198,6 +198,8 @@ def test_room_state_never_switches_the_session_checkpoint(self): self.assertIn("storage.getCurrentRuntimeSessionId", block) self.assertIn("currentSessionId !== checkpointSessionId", block) + self.assertIn("&& !reconcileCurrentColor", block) + self.assertIn("roomStateColorReconcilePending", block) self.assertLess( block.index("currentSessionId !== checkpointSessionId"), block.index("const roomState = getRoomState(previousRoomState);"), @@ -214,6 +216,7 @@ def test_color_has_no_separate_checkpoint_writer(self): self.assertEqual(block.count("writeLatestSavedSessionSnapshot"), 2) self.assertIn("sessionSnapshot.current_jin_color = avatar.color;", block) + self.assertIn("reconcileCurrentColor", block) self.assertNotIn("saved_at:", block) self.assertNotIn("colorOnly", block) diff --git a/tests/test_session_bootstrap_lineage_client_contract.py b/tests/test_session_bootstrap_lineage_client_contract.py index 8dfc084b..68c6feb9 100644 --- a/tests/test_session_bootstrap_lineage_client_contract.py +++ b/tests/test_session_bootstrap_lineage_client_contract.py @@ -75,7 +75,7 @@ def test_boot_color_has_one_local_and_one_server_reconciliation_path(self): self.assertNotIn("applyBootstrapSceneTintShift", source) self.assertIn("data.current_jin_color", handlers) self.assertIn("initialBootstrap: true", handlers) - self.assertIn("persist: false", handlers) + self.assertIn("persist: true", handlers) def test_early_room_restore_uses_common_checkpoint_color(self): logger_source = ( @@ -100,6 +100,7 @@ def test_early_room_restore_uses_common_checkpoint_color(self): ) init_block = logger_source[init_start:init_end] self.assertIn("initialBootstrapColor: true", init_block) + self.assertIn("reconcileCurrentColor: true", init_block) self.assertNotIn("applyBootstrapSceneTintShift", init_block) def test_live_bootstrap_preserves_tool_results(self): diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 3cfa855a..46007d06 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -3503,6 +3503,7 @@ const ROOM_STATE_PERSIST_DELAY_MS = 160; let roomStatePersistTimer = null; let roomStatePersistenceEnabled = false; let applyingRoomState = false; +let roomStateColorReconcilePending = false; function isRoomStateObject(value) { return Boolean( @@ -3800,7 +3801,10 @@ function finishRoomStateRestore(sequence) { roomStateRestoreFinishTimer = null; roomStateRestoreInProgress = false; - if (roomStateRestoreShouldPersist) { + if ( + roomStateRestoreShouldPersist + || roomStateColorReconcilePending + ) { scheduleRoomStatePersist(); } } @@ -4031,7 +4035,14 @@ function applyRoomState(roomState, options = {}) { return true; } -function persistRoomStateNow() { +function persistRoomStateNow(options = {}) { + if (options.reconcileCurrentColor === true) { + roomStateColorReconcilePending = true; + } + + const reconcileCurrentColor = + roomStateColorReconcilePending; + if ( !roomStatePersistenceEnabled || applyingRoomState @@ -4078,12 +4089,16 @@ function persistRoomStateNow() { // Room/avatar writes are field-local. They may update the current // session checkpoint, but they never decide that a freshly opened tab - // became a new conversation. The full checkpoint path does that only - // after real session activity; until then leave the predecessor intact. + // became a new conversation. JIN_COLOR is the one synchronous exception: + // reconcile the room into the existing common checkpoint even before the + // full turn promotes this runtime session. This keeps the checkpoint's + // session id, lineage and saved_at untouched while preventing an older + // color from being replayed on every reload. if ( currentSessionId && checkpointSessionId && currentSessionId !== checkpointSessionId + && !reconcileCurrentColor ) { return false; } @@ -4124,10 +4139,16 @@ function persistRoomStateNow() { }; } - return storage.writeLatestSavedSessionSnapshot({ + const written = storage.writeLatestSavedSessionSnapshot({ ...checkpoint, session_snapshot: sessionSnapshot, }); + + if (written && reconcileCurrentColor) { + roomStateColorReconcilePending = false; + } + + return written; } function scheduleRoomStatePersist() { @@ -4284,7 +4305,9 @@ function initRoomStatePersistence() { "jin:avatar-room-state-changed", (event) => { if (event.detail && event.detail.immediate === true) { - persistRoomStateNow(); + persistRoomStateNow({ + reconcileCurrentColor: true, + }); return; } scheduleRoomStatePersist(); diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index a3d53f85..eb637c68 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -4972,15 +4972,15 @@ return false; } - + if (initialBootstrap && initialBootstrapColorPending) { + initialBootstrapColorPending = false; + } if (normalizedColor === centerColor) { return true; } - if (initialBootstrap && initialBootstrapColorPending) { - initialBootstrapColorPending = false; - } + prepareCenterColorTransition(transitionDurationMs); applyCenterColor(normalizedColor, options); diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 629d6cc1..7b075d36 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -197,7 +197,7 @@ function handleSessionActionsUpdate( data.current_jin_color, { initialBootstrap: true, - persist: false, + persist: true, } ); } diff --git a/ui/templates/index.html b/ui/templates/index.html index c53bdbd4..eab49807 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -519,7 +519,7 @@ - + @@ -536,7 +536,7 @@ - + From 7a8ae6c12c687ba616615768f1c24da87cce2254 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:39:46 +0300 Subject: [PATCH 100/168] chore: ui - remove previous chat messages truncation in context - restore previous reasoning in context --- agent/nodes/brain.py | 22 +++----- runtime/runtime_context.py | 1 - tests/test_brain_asset_flow.py | 54 ++++++++++++++++++++ tests/test_previous_chat_messages_context.py | 49 ++++++++++++++++++ utils/context/context_exports.py | 4 +- utils/context/messages.py | 34 +++--------- 6 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 tests/test_previous_chat_messages_context.py diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index b99763c6..0e4c58f3 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -1676,13 +1676,10 @@ async def run_brain_stream( system_prompt=system_prompt, user_prompt=effective_brain_payload, runtime_actions=runtime_actions, - include_previous_reasoning=bool( - getattr( - context, - "runtime_session_restore_priming", - False, - ) - ), + # Keep the visible context snapshot aligned with the ordinary + # model prompt. Action follow-ups intentionally build their own + # reasoning context and stay excluded here. + include_previous_reasoning=not is_followup_tick, ) if preserve_runtime_action_markers: @@ -1921,13 +1918,10 @@ def ensure_runtime_list( runtime_actions=runtime_actions, user_input=state.user_input, commit_active_memory_refresh=True, - include_previous_reasoning=bool( - getattr( - context, - "runtime_session_restore_priming", - False, - ) - ), + # Ordinary user turns must carry the previous completed + # reasoning block. Follow-up builders disable it explicitly + # where they need the current turn reasoning instead. + include_previous_reasoning=True, ) ) brain_payload = ( diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 00eabc5b..0894a640 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -13,7 +13,6 @@ RECENT_MESSAGES_MAX_PAIRS = 3 -RECENT_MESSAGE_MAX_CHARS = 220 DEFAULT_JIN_COLOR = "#1f4f8f" DEFAULT_JIN_SIZE_TEXT = "120px" DEFAULT_JIN_SPEED_TEXT = "900px/s" diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index af08b30b..8a68ece9 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -1739,6 +1739,60 @@ async def fake_run_brain_stream(**kwargs): "I am JIN.", ) + async def test_regular_brain_run_includes_previous_reasoning_in_initial_prompt(self): + + build_calls = [] + + def fake_build_brain_context(*args, **kwargs): + build_calls.append(kwargs) + return "system prompt" + + async def fake_run_brain_stream(**kwargs): + return ( + "I am JIN.", + "new reasoning", + ) + + context = _context() + context.runtime_previous_reasoning_content = ( + "previous reasoning opening " + + "m" * 2600 + + " previous reasoning ending" + ) + state = AgentState( + user_input="hello", + ) + + with patch( + "agent.nodes.brain.get_brain_runtime_config", + return_value=_brain_runtime(), + ), patch( + "agent.nodes.brain.build_brain_context", + side_effect=fake_build_brain_context, + ), patch( + "agent.nodes.brain.build_brain_payload", + return_value="brain payload", + ), patch( + "agent.nodes.brain.emit_active_memory_records_update_if_dirty", + new=lambda _context: _async_noop(), + ), patch.object( + BrainNode, + "run_brain_stream", + staticmethod(fake_run_brain_stream), + ): + await BrainNode().run( + state, + context, + ) + + self.assertTrue(build_calls) + self.assertIs( + build_calls[0].get( + "include_previous_reasoning" + ), + True, + ) + async def test_regular_brain_run_stores_reasoning_for_next_chat_prompt(self): async def fake_run_brain_stream(**kwargs): diff --git a/tests/test_previous_chat_messages_context.py b/tests/test_previous_chat_messages_context.py new file mode 100644 index 00000000..943a1f5f --- /dev/null +++ b/tests/test_previous_chat_messages_context.py @@ -0,0 +1,49 @@ +import unittest + +from utils.context.messages import ( + build_previous_chat_messages_context_text, +) + + +class PreviousChatMessagesContextTests(unittest.TestCase): + + def test_preserves_complete_recent_messages_without_character_crop(self): + + user_text = "u" * 500 + jin_text = "j" * 700 + + context_text = build_previous_chat_messages_context_text([ + { + "user": user_text, + "jin": jin_text, + }, + ]) + + self.assertIn( + f"{user_text}", + context_text, + ) + self.assertIn( + f"{jin_text}", + context_text, + ) + + def test_preserves_full_text_while_escaping_physical_newlines(self): + + user_text = "first line\n" + ("x" * 400) + "\nlast line" + + context_text = build_previous_chat_messages_context_text([ + { + "user": user_text, + "jin": "ok", + }, + ]) + + self.assertIn( + "first line\\n" + ("x" * 400) + "\\nlast line", + context_text, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/context/context_exports.py b/utils/context/context_exports.py index 90480d2e..041d2879 100644 --- a/utils/context/context_exports.py +++ b/utils/context/context_exports.py @@ -30,7 +30,7 @@ append_previous_chat_messages, build_previous_chat_messages_context, build_previous_chat_messages_context_text, - crop_recent_message_text, + normalize_recent_message_text, format_context_message_age_suffix, ) from .assets import ( @@ -55,7 +55,7 @@ "build_current_runtime_context", "build_session_actions_history_context", "build_tool_results_context", - "crop_recent_message_text", + "normalize_recent_message_text", "format_active_memory_result_sections", "format_asset_result_sections", "format_context_message_age_suffix", diff --git a/utils/context/messages.py b/utils/context/messages.py index 2a480bc2..7b074c35 100644 --- a/utils/context/messages.py +++ b/utils/context/messages.py @@ -4,22 +4,18 @@ from math import isfinite from xml.sax.saxutils import escape -from runtime.runtime_context import ( - RECENT_MESSAGE_MAX_CHARS, - RECENT_MESSAGES_MAX_PAIRS, -) +from runtime.runtime_context import RECENT_MESSAGES_MAX_PAIRS from .session_actions import ( format_session_action_age, ) -def crop_recent_message_text( +def normalize_recent_message_text( text: str, - max_chars: int = RECENT_MESSAGE_MAX_CHARS, ) -> str: - cleaned = str( + return str( text or "" ).replace( @@ -28,27 +24,11 @@ def crop_recent_message_text( ).replace( "\r", "\n", - ) - - cleaned = cleaned.replace( + ).replace( "\n", "\\n", ).strip() - if max_chars <= 0: - return "" - - if len(cleaned) <= max_chars: - return cleaned - - if max_chars <= 3: - return "." * max_chars - - return ( - cleaned[: max_chars - 3].rstrip() - + "..." - ) - def format_context_message_age_suffix( created_at, @@ -164,13 +144,13 @@ def build_previous_chat_messages_context_text( ): continue - user_text = crop_recent_message_text( + user_text = normalize_recent_message_text( turn.get( "user", "", ) ) - jin_text = crop_recent_message_text( + jin_text = normalize_recent_message_text( turn.get( "jin", "", @@ -208,7 +188,7 @@ def build_previous_chat_messages_context_text( f"{escape(jin_text)}" ) - extra_user_text = crop_recent_message_text( + extra_user_text = normalize_recent_message_text( extra_user_message ) From 3cbe1fabeda02f870756c9b29ced8a8030020695 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:43:16 +0300 Subject: [PATCH 101/168] chore: sync docs - sync project docs with current codebase - update jin_size action parser with vw/vh and percent support --- AGENTS.md | 21 +- contracts/jin_size.json | 3 +- docs/JIN_ARCHITECTURE.md | 75 +++++- docs/JIN_CURRENT_STATE.md | 99 ++++++-- docs/JIN_DECISIONS.md | 111 ++++++++- tests/runtime_actions/test_jin_size.py | 130 ++++++++++- tests/test_chat_response_formatter.py | 48 ++++ tests/test_runtime_panel_client_contract.py | 64 ++++++ ui/static/js/chat-response-formatter.js | 74 +++--- ui/static/js/chat-runtime-actions.js | 54 +++-- ui/static/js/logger/log-entries.js | 62 +++-- ui/static/js/logger/logger.js | 165 ++++++++++--- ui/templates/index.html | 8 +- utils/actions/__init__.py | 2 + utils/actions/jin_size_utils.py | 242 +++++++++++++++----- utils/context/runtime_state.py | 7 +- 16 files changed, 942 insertions(+), 223 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 38887188..731e1b27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,10 +31,15 @@ A current implementation can still violate a product decision. Do not hide that. - L2 and L3 are **removed architectural layers**. Do not restore them from old README/tests/indexes. Any surviving L2/L3 names must be classified as compatibility, stale tests/docs, UI residue, or dead legacy before touching them. - Durable facts belong in L4. Do not reintroduce a durable-L1/L2/L3 memory hierarchy. - Active Memory, Delayed Memory, L4 facts, persistent Files, live L1/runtime memory, and session checkpoints are different systems with different lifetimes. Do not collapse them into one generic memory store. -- Session continuity must distinguish completed turns from interrupted turns and must preserve original timestamps where available. -- Browser checkpoint `saved_at` is a causal freshness boundary for archive enrichment, not a generic "last touched" timestamp. A field-local mutation must not advance it unless the whole checkpoint is genuinely refreshed. +- Session continuity must distinguish a real USER move, a completed turn, an interrupted USER-only turn, an action-only completion, and a blank bootstrap tab. A real USER row can become the newest conversation move without a visible JIN row; a blank tab cannot. +- The common browser checkpoint names the last runtime session that actually moved. Opening a tab may clone inherited L1 under a fresh runtime ID, but must not promote that ID until real user activity or a server-confirmed completed-turn commit. +- Preserve checkpoint lineage: `session_id`, `previous_session_id`, `booted_from_session_id`, nested runtime-snapshot origin, and `conversation_committed_at` have different meanings. Room/avatar field writes must not switch the checkpoint owner. +- Browser checkpoint `saved_at` is a causal freshness boundary for runtime/resource archive enrichment, not a generic "last touched" timestamp. Dialogue freshness is compared tail-to-tail independently. A field-local mutation must not advance `saved_at` unless the whole checkpoint is genuinely refreshed. - In predecessor bootstrap, an explicitly present `tool_results: []` is an authoritative cleared value. Do **not** generalize that tombstone rule to `loaded_memory_ids` or `active_memory_records`; their empty browser values still retain archive-fallback semantics. - Session-action history is structured continuity data. Preserve supported part metadata across persistence/bootstrap (for example JIN_COLOR `parts[].colors`), not only the visible action text. +- JIN color belongs to the common `session_snapshot`/room state and server `RuntimeContext`, not to a parallel `latestJinColor` store. The browser checkpoint wins for the same source session; structured action history and raw archive events are recovery sources only. +- Ordinary Brain turns include the previous successful reasoning block. Its projection keeps both edges and may replace only the middle with an explicit `CUTTED N chars` separator; action/recovery follow-ups build their own reasoning context and must not duplicate the ordinary block. +- `` is bounded by the newest three pairs, not by a per-message character cap. Preserve each selected USER/JIN message in full; normalize physical newlines to literal `\\n` without silently truncating content. - Archived restore intentionally stages historical resources and replays them through the real action path after the one-shot restore response. Do not add a second late apply path. - Anonymous/shadow mode may read global durable context but must not silently perform restricted persistent writes. @@ -45,6 +50,9 @@ A current implementation can still violate a product decision. Do not hide that. - Every new action needs: contract -> parser/normalization -> guard if needed -> dispatcher/handler -> emitted state/result -> tests. - Streaming can split markers at arbitrary chunk boundaries. Always test complete, split, repeated, incomplete, false-prefix, and flush/stop cases. - Do not let private action markers leak into visible answer text. +- Canonical `JIN_COLOR` and `JIN_SIZE` syntax is paired XML with the payload in the body: ` #00f2ff ` and ` w:120 h:120 `. Inline/colon forms are localized legacy compatibility only. Marker removal must preserve ordinary visible text on both sides of the marker. +- `JIN_SIZE` accepts positive decimal `px`, `vw`, `vh`, and `%` values; unitless values mean `px`. Preserve relative units through parsing/events and resolve them against the live browser viewport only when applying the action. `%` is axis-relative (width -> viewport width, height -> viewport height); `vw` and `vh` always use their named viewport axis. Persist the resulting rendered room geometry in pixels. +- Consecutive JIN visual markers are ordered state events. Drop only a true adjacent/no-op repetition in the same runtime-message scope; preserve alternation such as red -> blue -> red and allow the same color again in a later message. - Do not emit the same action twice because two parser paths recognized the same marker. - Current Delayed save contract is `` with a JSON body. The old `` key/value format is legacy only. - Current Active create/update model boundary is flat JSON. `SAVE_ACTIVE_MEMORY` custom fields must come from explicit structured JSON; plain prose such as a trailing `(field: value)` is still prose/conditions, not schema. Legacy nested update payloads and self-closing UPDATE attributes may be accepted locally for compatibility; do not advertise them as preferred syntax. @@ -62,6 +70,8 @@ Before introducing any visual state, find and reuse the closest existing JIN UI - Loaded context, mere ID reference, pin state, pause state, inspect/modal open, and delete/restore are different semantics. Do not map all of them to one generic `highlight()` call. - For UI fixes, verify the actual rendered DOM/event path, not only data/state mutation. - For bootstrap/restore visual fixes, locate every writer and prove there is one authoritative final apply path. +- Normal bootstrap renders at most the three newest real USER moves with their JIN/reasoning when present, followed by the current-session date divider. Preserve USER-only interrupted/action-only moves without manufacturing an empty JIN bubble, and do not duplicate this tail during explicit archived restore. +- The first bootstrap color transition is the one 2-second transition; ordinary/live color changes use the shared 333 ms avatar-and-scene transition. Do not restore an old tint queue or add a second transition writer. - Session-action interruption telemetry is causal UI state: validator/reasoning loops and context/output-limit recovery entries must be recorded/emitted when detected, before automatic follow-up/recovery starts. - L4 rows use a short default preview (currently 50 characters), but a fact that is bubbled by reference/citation/context-loaded state must show its full value rather than a truncated citation. - Hover/detail metadata must survive non-semantic refreshes: counter-only runtime-action updates and bootstrap normalization must not silently erase tooltip/swatch data. @@ -97,14 +107,19 @@ At minimum, run the checks that apply to the change: - duplicate writer/listener/timer search; - serialize -> reload -> hydrate round trip for restore/state changes; - explicit-empty-vs-missing tests for bootstrap fields, plus checkpoint timestamp/lineage invariance for field-only cleanup; +- blank-tab vs completed vs interrupted USER-only vs action-only latest-session selection, separately for normal and anonymous log roots; +- dialogue-tail freshness independent of runtime `saved_at`, and matching-session L1 selection for the common checkpoint; - structured session-action metadata round trip (including JIN_COLOR swatch/hex hover); +- JIN_COLOR server-context -> raw runtime log -> common checkpoint -> local early apply -> one server reconciliation round trip, including stale-source replacement and repeated reload; +- full-text recent-message context tests beyond the former character cap, including physical-newline escaping and the three-pair limit; +- ordinary-turn previous-reasoning inclusion and middle-crop tests, plus absence of duplicate ordinary reasoning in action/recovery follow-ups; - chunk-boundary tests for stream/action parsing; - legacy-record + modern-record tests for compatibility/timestamps; - real DOM/render-path check for UI behavior; - search capability tests with configured/blank/placeholder provider credentials when touching search contracts or prompt exposure; - patch apply check against the exact source snapshot when delivering `.patch`. -Do not claim the repository is green unless the relevant tests actually pass. See `docs/JIN_CURRENT_STATE.md` for the current full-suite status of the 2026-08-23 snapshot. +Do not claim the repository is green unless the relevant tests actually pass. See `docs/JIN_CURRENT_STATE.md` for the full-suite status of the exact inspected snapshot. ## Handoff format diff --git a/contracts/jin_size.json b/contracts/jin_size.json index 9c75d2e3..0e4d76a9 100644 --- a/contracts/jin_size.json +++ b/contracts/jin_size.json @@ -16,7 +16,8 @@ "Use to set JIN Live Avatar size.", "Emit marker using this schema w:120 h:120 ", "You may emit 120px to make the avatar square.", - "Payload values may include px, percent, or vw/vh. Default is - px." + "Each value may use px, vw, vh, or %. A unitless value defaults to px.", + "Percent values are axis-relative: width uses the current window width and height uses the current window height." ], "close_tag": true } diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index f283362c..2dab1e25 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -1,7 +1,8 @@ # JIN Core Engine — Current Architecture -**Verified snapshot:** `jin_core(20260823-114203).zip` -**Intent source used for reconciliation:** `JIN_CORE_CONTEXT_TRANSPLANT_2026-08-23.md` +**Verified snapshot:** `jin_core(20260826-090339).zip`
      +**Inspection date:** 2026-08-26
      +**Intent sources used for reconciliation:** current source/tests plus the accumulated 2026-08-23--26 project decisions
      **Purpose:** describe the architecture that is actually visible in the current source tree, while explicitly separating legacy compatibility from active design. This document supersedes the architectural parts of the old root `ARCHITECTURE.md` and `README.md` wherever they still describe L2/L3 as live architectural layers or `SAVE_SESSION` as a current model action. @@ -172,6 +173,10 @@ The current high-level order is: 14. identity block; 15. turn/loop rules. +On ordinary turns, `` takes the newest three recent USER/JIN pairs. The bound is pair count only: selected message bodies are no longer character-cropped. CRLF/CR is normalized, physical newlines are serialized as literal `\\n`, surrounding whitespace is stripped, and XML-sensitive characters are escaped without removing the remaining text. + +The ordinary initial Brain prompt also includes the previous successfully completed reasoning in ``. Up to 2000 characters are kept whole; above that threshold the projection keeps the first and last 25% and replaces the middle with `CUTTED N chars`. Interrupted/recovery reasoning is tracked separately. Action and recovery follow-ups explicitly assemble their own current-turn/loop reasoning context instead of duplicating the ordinary previous-reasoning block. The visible TODO context snapshot mirrors the same ordinary-vs-follow-up choice. + Prompt text is a transient projection. Canonical state remains in `RuntimeContext`, browser persistence, and filesystem stores. --- @@ -231,6 +236,8 @@ For close-tag actions, the canonical form remains a paired block. For short acti Important current compatibility boundaries: - `CLEAN_TOOL_RESULTS` is intentionally a no-payload bare marker. A redundant `
      ` is consumed as parser-only noise, including when it arrives in a later stream chunk; the paired-looking form still means one action. +- `JIN_COLOR` and `JIN_SIZE` advertise paired XML with their payload in the tag body. Legacy inline/colon/space forms remain parser compatibility only. The stream filter must remove only the marker and preserve ordinary answer text before and after it, even across chunk boundaries. +- `JIN_SIZE` normalization preserves positive decimal `px`, `vw`, `vh`, and `%` values instead of stripping their units. Unitless values become `px`. The browser resolves relative values at application time against its live viewport: `%` uses the matching width/height axis, while `vw` and `vh` always use viewport width and height respectively. The ordinary room-state checkpoint still stores the clamped rendered pixel geometry, so reload does not reinterpret an old relative command against a different window. - `UPDATE_ACTIVE_MEMORY` is advertised as paired flat JSON, while localized compatibility code can also read a self-closing attribute form. That attribute form is compatibility, not the canonical model contract. - `SAVE_ACTIVE_MEMORY` does not infer custom fields from parenthesized plain prose. Only explicit JSON root fields are structural custom fields; non-JSON text remains the `conditions` value. @@ -242,7 +249,20 @@ Important current compatibility boundaries: Action-specific rules should stay in contracts. `rules/runtime.py` should remain limited to cross-action sequence behavior and recovery rules. -### 6.5 Session-action telemetry is causal +### 6.5 JIN visual action state + +`utils/actions/jin_visual_sequence_actions.py` emits each contiguous color/size/position/speed run in original marker order with a shared sequence ID. The browser buffers the run and plays it as one ordered visual sequence instead of regrouping actions by type. + +JIN_COLOR has additional persistence semantics: + +- accepted color updates set `RuntimeContext.jin_color` before the completed session snapshot is built; +- only a true no-op against the last applied color in the same runtime-message scope is skipped, so ordered alternation remains valid and a later message may request the same color again; +- each applied color is appended to the raw JSONL log as a `runtime_action_request` with a stable event ID, turn ID, timestamp, normalized color, and structured `session_action.parts[].colors` payload; +- a log-write failure is reported but does not block the already-valid UI event. + +The raw event is the durable archive recovery path. It is not a second live state owner. + +### 6.6 Session-action telemetry is causal Session-action history is not only an end-of-turn summary. `runtime/stream.py` records and emits interruption/recovery entries at the moment the cause is detected so the logger changes before any automatic follow-up begins. This applies to reasoning/content validator loops and model context/output-limit recovery; `context_overflow` is treated as a context-limit finish reason. @@ -402,15 +422,44 @@ JIN deliberately splits persistence by owner/lifetime. `ui/static/js/runtime/runtime-session.js` persists live checkpoints and sends `session_bootstrap` data when appropriate. The backend normalizes and hydrates the browser-provided snapshot in `websocket/bootstrap.py`. -For a predecessor/browser checkpoint, `websocket/bootstrap.py::enrich_session_bootstrap_from_archive()` may enrich dialogue, reasoning, action history, files, counters, and selected runtime state from the raw archived log. The browser checkpoint `saved_at` is the freshness boundary: if the archive tail predates that checkpoint, archive-derived dialogue/reasoning/actions/files are intentionally not mixed into newer browser state. Therefore `saved_at` must represent a real whole-checkpoint refresh, not any incidental field mutation. +The browser uses one common checkpoint to name the last runtime session that actually moved. Merely opening/reloading a tab can clone inherited L1 into a fresh per-session record and record `booted_from_session_id`, but it does not promote that fresh runtime ID to the common conversation checkpoint. A real user send marks session activity immediately; a later server `completed_turn_commit` is the fallback commit signal. `conversation_committed_at` advances only for a completed turn, so it remains distinct from USER-only session movement. + +Per-session runtime records are candidates for the checkpoint's L1 payload only when their `session_id` matches the common checkpoint. Nested runtime snapshots preserve their own origin session ID; lineage is not rewritten merely because the containing browser record belongs to the new tab. + +The server sends a normalized `session_snapshot` at visible `message_end` and again at `agent_runtime_end`. The client merges the current room state into that snapshot and persists it before finishing the visible bubble. Completed state and room/avatar state therefore land as one checkpoint rather than racing through separate full writers. + +For a predecessor/browser checkpoint, `websocket/bootstrap.py::enrich_session_bootstrap_from_archive()` may enrich dialogue, reasoning, action history, files, counters, and selected runtime state from raw logs. Normal and anonymous modes use separate log roots. + +The latest raw-log selector chooses the session containing the newest real USER move. A blank bootstrap-only session with no USER row cannot win. A stopped USER-only move and a completed action-only move with an empty visible JIN row can win and remain distinguishable by whether a durable JIN row/timestamp exists. The legacy function name `find_latest_completed_session_restore_payload()` is therefore narrower than its current semantics. + +Bootstrap uses two freshness clocks: + +- dialogue/reasoning/counters compare the browser recent-turn tail to the archive recent-turn tail; +- runtime/resource fields still use checkpoint `saved_at` against archive tail time. + +This separation prevents a harmless fresh-tab clone or room-state write from pinning dialogue to an older turn. `saved_at` must still represent a whole-checkpoint refresh, not an incidental field mutation. + +Session actions are merged by stable ID when available, otherwise by structured identity, sorted by real `created_at`, and bounded. For an unchanged source session the common checkpoint owns actions at or before `saved_at`; raw logs may append only a newer tail. If raw dialogue proves that another source session has a strictly newer USER move, the stale browser source, its actions, and its color are discarded together. `CLEAN_TOOL_RESULTS` has an explicit field-local persistence rule. On successful cleanup, the browser rewrites only `session_snapshot.tool_results` to `[]` inside the existing checkpoint and preserves checkpoint timestamp/lineage verbatim. During enrichment, the mere presence of `tool_results` (including `[]`) is authoritative, so old archived search/tool output cannot resurrect in a new tab. This exact-empty rule is intentionally **not** shared by `loaded_memory_ids` or `active_memory_records`; their empty browser collections still use the established archive fallback behavior. +Late append-only L4 tool results are the one post-checkpoint tool-result enrichment: a newer raw `runtime_tool_result` of kind `l4` may be merged after the greater of checkpoint time and `tool_results_cleared_at`. Other old tool results remain blocked by the browser checkpoint/tombstone. + Session-action parts are structured continuity data. Bootstrap normalization currently preserves `text/detail/message/id` plus recognized `colors`; color metadata is normalized to lowercase `#rrggbb` (including expansion from `#rgb`) so restored JIN_COLOR actions keep the same swatch and hex hover metadata as live actions. +Current JIN color is part of the same checkpoint. For the same source session, explicit browser `current_jin_color` wins over action history and archived trusted state. If that field is absent, the newest structured JIN_COLOR session action is the first fallback, followed by the raw archive/trusted color. Applied colors are also recorded as raw runtime events so a direct-predecessor chain can recover both final color and ordered history. + +Room-state persistence is field-local. It normally refuses to write across a session-ID mismatch and never changes checkpoint `session_id`, lineage, or `saved_at`. JIN_COLOR uses one synchronous reconciliation exception: after the UI applies a live color it may merge that color into the existing common checkpoint even before the new runtime session is promoted, while preserving checkpoint ownership and freshness metadata. + Old L3-era fields can still appear in tests/compatibility paths, but there is no active L3 memory module. -### 9.3 Archived session restore +### 9.3 Normal bootstrap chat tail + +After backend hydration, normal bootstrap emits at most the three newest `runtime_recent_turns` to the browser. Each item may carry USER text, JIN text, saved reasoning, and original timestamps. Attachment-context boilerplate is removed from the visible USER bubble. + +The client rebuilds the tail through the existing chat primitives. It keeps a real USER-only interrupted/action-only turn but does not manufacture an empty BR bubble. It then appends a date-labelled current-session divider and activates the live-turn viewport at that boundary, leaving the inherited three-turn history immediately above the initial screen. Explicit archived restore owns its own rendering path and suppresses this normal-bootstrap tail. + +### 9.4 Archived session restore Archived restore is a distinct path: @@ -423,11 +472,11 @@ Archived restore is a distinct path: 7. `BrainNode` consumes the staged restore envelope and replays those resources through the normal runtime-action dispatcher. 8. The WebSocket tail performs defensive cleanup only; it must not apply the same resources a second time. -Constants in the current source limit the restore reasoning dump to two recent reasoning items with a per-item character cap, while restored visible dialogue uses the normal recent-pair limit. +Constants in the current source limit the restore reasoning dump to two recent reasoning items with a per-item character cap. Restored visible dialogue uses the normal three-pair limit and, like ordinary recent-message context, does not impose a per-message character cap. `utils/session_restore.py` still understands historical `SAVE_SESSION` labels in archived logs. That is restore compatibility, not proof of a current `SAVE_SESSION` runtime action. -### 9.4 Timestamp invariant +### 9.5 Timestamp invariant Modern records/snapshots should retain original creation timestamps across serialize -> reload -> hydrate. `now()` is only a fallback for truly legacy records without a timestamp. Rendering/loading must not rewrite historical time. @@ -463,7 +512,7 @@ Browser behavior can isolate session-facing stores in private/incognito mode. Do `process_message()` later routes interrupted turns through `schedule_interrupted_runtime_memory_update()` instead of pretending the turn completed normally. -The Brain/stream layer also contains recovery for reasoning repetition and model/context output limits. Follow-up ticks preserve sequence identity rather than starting a new user request. The corresponding session-action interruption entry is emitted before that recovery/follow-up starts, not deferred until the final answer. +The Brain/stream layer also contains recovery for reasoning repetition and model/context output limits. Follow-up ticks preserve sequence identity rather than starting a new user request. They explicitly suppress the ordinary previous-reasoning projection and carry the relevant current-turn or loop-recovery reasoning through their dedicated builder. The corresponding session-action interruption entry is emitted before that recovery/follow-up starts, not deferred until the final answer. --- @@ -492,6 +541,12 @@ The L4 panel deliberately separates compact browsing from surfaced evidence: ord Session-action logger rows are also a projection of structured history. The compact logger keeps the most recent five items in chronological order with their original numbering and reuses the existing attached-files header/button primitive for `FULL`. JIN_COLOR parts render one swatch per applied color and expose the normalized hex on hover; bootstrap must preserve the `colors` payload for this to work after reload. Runtime-action bubble details are retained across counter-only updates so a count refresh cannot erase existing hover metadata. +JIN visual-action chat bubbles are currently release-gated off, but parsing, execution, avatar updates, raw event persistence, and Session Actions logging remain active. A UI visibility flag must not be mistaken for a disabled runtime action. + +Color has one visual transition owner in the avatar API. The initial bootstrap application consumes the one 2000 ms transition; later/live JIN_COLOR applications use 333 ms. The API writes the same temporary duration to avatar-center and scene-tint CSS variables before applying the color, so both projections move together. The old color queue and separate bootstrap tint-shift helper are absent. + +Normal session bootstrap applies the color from the common local checkpoint during early room restoration, then accepts one server `session_actions_update` reconciliation with `bootstrap_restore=true`. There is no client-side color resolver scanning competing sources. + ### Header auto-hide `ui/static/js/header-autohide.js` currently uses: @@ -539,9 +594,13 @@ Do not infer architecture from these alone: - comments mentioning “L1/L2/L3 glow”; - stale CodeGraph/search indexes; - historical field names like `runtime_l3_session_memory`; +- the name `find_latest_completed_session_restore_payload()`; current selection is newest real USER move, including USER-only interruption; - treating any write to a browser checkpoint as permission to advance `saved_at`; +- using runtime `saved_at` to decide whether copied dialogue is current; +- treating a newly opened runtime ID as the newest conversation before a real USER move; - assuming an empty collection always means "missing" during archive enrichment; - sanitizing session actions down to text while dropping structured UI metadata such as `parts[].colors`. +- adding a separate JIN color key/resolver/queue instead of reconciling the common checkpoint and raw action log. The verified filesystem has no active L2/L3 modules and no current `SAVE_SESSION` contract. diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index 528c6e67..dc73c0c4 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -1,8 +1,8 @@ # JIN Core Engine — Current State / Migration Notes -**Snapshot inspected:** `jin_core(20260823-114203).zip` -**Inspection date:** 2026-08-23 -**Context reference:** `JIN_CORE_CONTEXT_TRANSPLANT_2026-08-23.md` +**Snapshot inspected:** `jin_core(20260826-090339).zip`
      +**Inspection date:** 2026-08-26
      +**Context reference:** current source/tests plus the accumulated 2026-08-23--26 project decisions This is the document to read before touching transitional code. It lists what is true in the inspected snapshot, what is legacy residue, and where product intent and implementation currently differ. @@ -20,7 +20,13 @@ The runtime is already on the post-L2/L3 architecture in the actual source tree, - search action exposure now follows real configured provider availability rather than feature flags alone; - `CLEAN_TOOL_RESULTS` now has explicit cleared-state bootstrap semantics so old tool results cannot resurrect without falsifying checkpoint freshness; - session-action logger metadata survives bootstrap, including JIN_COLOR swatches/hex hover; -- the full test suite is currently far from green on this snapshot. +- `JIN_COLOR`/`JIN_SIZE` now advertise paired XML, while legacy inline forms remain parser-only compatibility; +- normal bootstrap restores a bounded three-USER-move chat tail plus a current-session divider, including interrupted/action-only USER-only rows; +- Brain recent-message context keeps the newest three pairs in full, with newline/XML normalization but no per-message character crop; +- ordinary Brain turns again include the previous successful reasoning block with explicit middle-crop semantics, while follow-ups keep their dedicated reasoning context; +- common-checkpoint ownership, per-session L1 lineage, completed-turn time, and dialogue freshness are now explicitly separated; +- JIN color now round-trips through server context, raw runtime events, the common checkpoint, early local room restore, and one server reconciliation without a parallel color store; +- the full suite is still not certified green; the 2026-08-26 documentation environment also lacks required `httpx`/`fastapi` dependencies. New agents must not “repair” these contradictions by restoring the old architecture. @@ -160,7 +166,11 @@ Default Brain action flags currently set `CAN_RUNTIME_TODO=False`; the other map `CLEAN_TOOL_RESULTS` is a no-payload action. The canonical bare `` marker and a redundant paired-looking `...` form resolve to one cleanup; the closing tag is consumed as parser noise, including when split across stream chunks. -`JIN_SIZE` contract version 2 currently advertises ` w:120 h:120 `; a single value such as ` 120px ` means square size. Size values may use px, percent, or vw/vh, with px as the default unit. +`JIN_SIZE` contract version 2 currently advertises ` w:120 h:120 `; a single value such as ` 120px ` means square size. Positive decimal values may use `px`, `vw`, `vh`, or `%`, with unitless values defaulting to `px`. The backend preserves those units in the canonical action payload. The browser resolves them against the live viewport when the action is applied: width `%` uses viewport width, height `%` uses viewport height, `vw` always uses viewport width, and `vh` always uses viewport height. The applied/clamped room geometry is then persisted in pixels. Unsupported suffixes such as `em` are rejected instead of being silently reinterpreted as pixels. + +`JIN_COLOR` contract version 2 likewise advertises ` #00f2ff `. Both actions put payload in a paired tag body. Localized parsing still accepts old colon/space inline variants, but those are not model-facing syntax. Current parser/formatter tests require ordinary `before`/`after` answer text to survive marker removal and cover split-chunk completion. + +The JIN visual sequence path preserves the model's marker order across color/size/speed/position. Color and size filtering removes only a no-op against the last applied value in the same runtime-message scope; an alternating sequence is not a repetition failure, and the same color can be requested in another message. --- @@ -239,6 +249,9 @@ Current `build_brain_context()` order is intentionally structured. Important anc - file + delayed inventories sit near the top on ordinary turns; - Active/L1/loaded Delayed/L4 live together in the runtime-context group; - restored exact dialog replaces the normal recent-dialog window for the one-shot restore path; +- ordinary `` keeps the newest three pairs without per-message character cropping; physical newlines become literal `\\n` and XML-sensitive characters are escaped; +- ordinary initial turns include the previous successful reasoning; blocks over 2000 characters keep the first and last 25% with an explicit middle-cut marker; +- action/recovery follow-ups suppress that ordinary previous-reasoning block and inject their current-turn/loop reasoning through dedicated builders; - action contracts remain present even on restore ticks, subject to effective capability filtering; - both search contracts disappear from the prompt when `settings.CAN_SEARCH` is false; - identity and loop rules are at the bottom. @@ -249,22 +262,59 @@ Any prompt-order change can alter behavior materially. Do not reorder sections f ## 9. Bootstrap / archived restore — verified behavior -### Browser predecessor bootstrap / archive enrichment +### 9.1 Common checkpoint and lineage + +The browser common checkpoint is the last session that actually moved, not the most recently opened runtime ID. `applyPersistedSessionBootstrap()` may clone inherited L1 into the current per-session storage record and set `booted_from_session_id`, but it deliberately does not advance the common checkpoint on page load. + +A user send marks the session dirty immediately. The next live-checkpoint write can promote that session even if generation is stopped before a visible answer. `completed_turn_commit` is a server fallback and separately advances `conversation_committed_at`; that timestamp is not the definition of every session move. + +Per-session L1/runtime candidates are used only when their `session_id` matches the common checkpoint. `runtime_snapshot.session_id` preserves the snapshot's origin rather than being rewritten to the container/current runtime ID. Room/avatar persistence normally rejects cross-session writes and never changes checkpoint lineage. + +The server supplies `session_snapshot` on `message_end` and `agent_runtime_end`. The browser adds current room/avatar state and persists before finishing the visible message bubble, so completed turn data and room state are committed through one common snapshot path. + +### 9.2 Raw-log source selection and split freshness + +Despite its historical name, `find_latest_completed_session_restore_payload()` now selects the newest raw-log session containing a real USER move. A blank bootstrap-only session is ignored. A stopped USER-only move qualifies, as does an action-only completion whose durable JIN row has empty visible text. Normal and anonymous selectors read their own log roots only. + +If raw logs prove a different session has a strictly newer USER tail than both the requested archive and browser tail, bootstrap switches `source_session_id` and discards the stale source's browser actions/color with it. + +Archive enrichment now has two clocks: + +- dialogue, reasoning, and dialogue counters compare archive recent-turn tail to browser recent-turn tail; +- runtime/resource fields use whole-checkpoint `saved_at` against the archive tail. + +This fixes the case where a newly cloned runtime snapshot has a newer `saved_at` but still carries older copied dialogue. A field-local write must still preserve `saved_at`; it is not a generic last-touched value. + +Session actions are merged by event ID when present or by structured identity otherwise, sorted by real timestamp, and capped. For the same source, the common checkpoint owns actions at or before `saved_at`; raw JSONL supplies only later actions. Structured color arrays are part of identity and are preserved as normalized lowercase six-digit hex. -A browser checkpoint may be enriched from the predecessor raw log, but only when the archive is not older than the checkpoint freshness boundary. `saved_at` therefore means “this whole checkpoint is current through here,” not “some field was touched recently.” Advancing it for a one-field cleanup can suppress otherwise valid archive enrichment of dialogue/reasoning/session-actions/files. +### 9.3 Field-specific enrichment -The repaired `CLEAN_TOOL_RESULTS` path now mutates only the existing checkpoint's `session_snapshot.tool_results` to `[]`; it preserves `saved_at`, session lineage, and every unrelated checkpoint field. On bootstrap, explicit presence of `tool_results: []` is authoritative and blocks archive fallback for that field. Empty `loaded_memory_ids` and `active_memory_records` deliberately keep their prior archive-fallback semantics. +The repaired `CLEAN_TOOL_RESULTS` path mutates only the existing checkpoint's `session_snapshot.tool_results` to `[]` and records `tool_results_cleared_at`; it preserves `saved_at`, session lineage, and every unrelated checkpoint field. Explicit presence of `tool_results: []` is authoritative. Empty `loaded_memory_ids` and `active_memory_records` deliberately keep their prior archive-fallback semantics. -Session-action normalization also preserves structured `parts[].colors` metadata. Valid `#rgb`/`#rrggbb` colors are normalized to lowercase six-digit hex, allowing restored JIN_COLOR history to render the same color square and hex hover as live history. +A newer raw `runtime_tool_result` of kind `l4` may still append after the greater of checkpoint `saved_at` and `tool_results_cleared_at`. Other archived tool results cannot cross the tombstone. -### Archived restore +### 9.4 JIN color bootstrap + +Accepted JIN_COLOR now updates `context.jin_color` before checkpoint construction and is also appended to JSONL as an ordered `runtime_action_request` containing color, event ID, timestamp, turn ID, and structured Session Action metadata. Archive restore can recover color events through up to three direct-predecessor links. Raw color recovery and context-text L4 action recovery coexist; one no longer suppresses the other. + +For the same source session, explicit browser `current_jin_color` wins. If absent, the newest structured JIN_COLOR Session Action wins, then the raw/trusted archive color. When the authoritative source session changes, stale browser color is not carried over. + +Early room restore applies the common checkpoint color locally. The backend then emits one `session_actions_update` with `bootstrap_restore=true` and `current_jin_color` for reconciliation. The old client resolver/tint-shift path and separate latest-color storage are absent. + +The first bootstrap color consumes a synchronized 2000 ms avatar-center + scene-tint transition. Later/live changes use 333 ms. A live color dispatch requests immediate field-local checkpoint reconciliation; this is the only room writer allowed across a fresh-tab/common-checkpoint ID mismatch, and it preserves checkpoint `session_id`, lineage, and `saved_at`. + +### 9.5 Normal bootstrap chat tail + +The backend emits at most three newest real USER moves from `runtime_recent_turns`, with JIN, reasoning, and original timestamps where available. The browser rebuilds them through existing chat primitives, strips synthetic attached-context boilerplate from USER display, keeps USER-only moves without an empty BR bubble, appends the current date/session divider, and activates the live viewport at that divider. Explicit archived restore suppresses this duplicate normal-bootstrap tail. + +### 9.6 Archived restore Current restore code deliberately prevents the “double apply” class of bugs. Verified behavior: - archived visible dialogue is rebuilt from logs; -- the newest complete USER/JIN pairs are used for the restore context; +- the newest three real USER moves are used for the restore context in chronological order, with empty JIN retained where the turn was interrupted/action-only; - a bounded recent reasoning dump is included; - loaded Delayed IDs, attached files, and avatar state are staged; - restore Brain response occurs before normal resource reactivation; @@ -392,9 +442,19 @@ Runtime-action bubbles persist their detail in DOM dataset state. Counter-only u - answer-rating implementation remains in the client but is release-gated off; assistant bubbles now reuse the neutral hover primitive on empty padding, with double-click copy and latest-completed-answer long-tap replacement retry; - Win95 theme localStorage reads/writes are guarded so restricted storage contexts do not break theme switching. +### 13.8 Normal bootstrap tail + +Normal bootstrap renders the inherited three-USER-move tail above a date-labelled current-session divider and places the live viewport at the divider. Saved reasoning is rendered through the existing reasoning bubble path. A USER-only interrupted/action-only move remains visible without a blank BR bubble. Archived restore has its own renderer and blocks this path. + +### 13.9 JIN color projection + +JIN visual-action chat bubbles are currently gated off by `ENABLE_JIN_VISUAL_ACTION_BUBBLES=false`; parsing, execution, avatar updates, raw action logging, and Session Actions remain live. + +Avatar center and scene tint now share one transition duration variable set. First bootstrap color uses 2000 ms once; all later/live color changes use 333 ms. There is no center-color queue, secondary bootstrap tint shift, or separate client color resolver in this snapshot. + --- -## 14. Test suite status on this exact snapshot +## 14. Test status for this exact snapshot Command run: @@ -402,19 +462,22 @@ Command run: python -m unittest discover -s tests ``` -Result on the inspected archive: +Result in the 2026-08-26 documentation environment: ```text -Ran 837 tests in 28.927s -FAILED (failures=67, errors=32, skipped=9) +Ran 431 tests in 3.973s +FAILED (failures=28, errors=68) ``` Important implications: - the repository is **not green** in this snapshot; -- some collection/import errors are directly explained by stale L2/L3 tests importing deleted modules; -- several `tests/runtime_actions/*` modules also fail import because they still depend on the removed `clients.brain_client.should_execute_save_session` symbol; -- many additional failures/errors remain beyond those stale imports, including older prompt/follow-up/session-action/search expectations; +- this count is **not comparable** to the previous 2026-08-23 run: the current environment lacks project dependencies including `httpx` and `fastapi`, so many modules fail during import before their tests can be collected; +- accessible tests still include unrelated client-contract failures, so dependency errors are not the only reason the command is red; +- a dependency-light targeted run of `test_previous_chat_messages_context`, `test_bootstrap_color_action_regression`, `test_jin_color_transition_client_contract`, and `test_session_bootstrap_lineage_client_contract` runs 18 tests and passes all 18; +- `test_brain_asset_flow`, including the ordinary-turn previous-reasoning regression, cannot import in this environment because `runtime/stream.py` requires the missing `httpx` dependency; +- bootstrap/latest-session/chat-tail/live-checkpoint suites that import `websocket` or L1 client modules cannot be executed in this environment until the missing dependencies are installed; +- stale L2/L3 and old `SAVE_SESSION` tests remain repository debt even though this particular environment reaches dependency failures first; - do not use “full suite currently fails” as permission to ignore targeted regressions; - for any patch, run the smallest relevant target set and report exact pass/fail counts. diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index bc390564..bcba48f5 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -1,6 +1,6 @@ # JIN Core Engine — Durable Decisions -**Decision baseline:** reconciled on 2026-08-23 against `jin_core(20260823-114203).zip` and the accumulated project context. +**Decision baseline:** reconciled on 2026-08-26 against `jin_core(20260826-090339).zip` and the accumulated project context. This file records product/architecture intent that should survive refactors. It is not a changelog and not a dump of historical experiments. @@ -190,7 +190,7 @@ Archived restore first gives Brain bounded exact historical context, then reacti **Status:** Accepted / implemented -Stop/cancel/repetition/context-limit interruption must preserve separate lifecycle semantics. Partial reasoning/answer plus user silence must not be silently serialized as a normal finished exchange. +Stop/cancel/repetition/context-limit interruption must preserve separate lifecycle semantics. A real USER move is retained in the latest-session/bootstrap tail even when no JIN row exists, but it must remain USER-only rather than being rewritten as a completed exchange. An action-only completed turn may also have no visible JIN text; its durable empty JIN row/timestamp is the completion marker. **Why:** bootstrap continuity must reflect what actually happened. @@ -265,10 +265,12 @@ Memory/file items use a shared interaction language where applicable: long press ## D019 — Consecutive JIN visual actions should read as one sequence -**Status:** Accepted / implementation-specific +**Status:** Accepted / implemented Consecutive color/size/position/speed actions should be visually sequenced rather than looking like unrelated mechanical phases. Position movement should ease in/out while respecting semantic speed. +Only adjacent/no-op repetitions in the same runtime-message scope are removed. Alternation such as red -> blue -> red must remain ordered, and a later message may intentionally request the same color again. + **Why:** the Live Avatar is a runtime expression channel, not four disconnected CSS toggles. **Rejected alternative:** adding a second parallel animation engine when the existing sequence runner can be extended. @@ -410,3 +412,106 @@ The ordinary L4 panel uses a compact 50-character value preview. A fact bubbled **Why:** the panel should remain scan-friendly by default, while evidence JIN actually surfaced must be readable in full. The expansion is a UI projection and must not mutate canonical L4 storage/order. **Rejected alternatives:** truncating surfaced citations; expanding every L4 row all the time. + +--- + +## D031 — JIN color and size use paired XML at the model boundary + +**Status:** Accepted / implemented with localized legacy compatibility + +Canonical forms put payload in the body: + +```text + #00f2ff + w:120 h:120 +``` + +Inline/colon/space forms may still be recognized by compatibility parsing, but contracts and prompt instructions teach only the paired form. Removing a marker must preserve ordinary visible answer text on both sides and across arbitrary stream chunk boundaries. + +`JIN_SIZE` values preserve their declared unit across the model/parser/event boundary. Positive decimal `px`, `vw`, `vh`, and `%` values are supported; a missing unit means `px`. Relative units are resolved only in the live browser: `%` follows the corresponding width/height viewport axis, while `vw` and `vh` always follow viewport width and height. Persistence records the resulting rendered pixel geometry rather than the unresolved command. + +**Why:** a real closing boundary prevents an inline marker parser from swallowing the answer tail and gives streaming one deterministic completion point. + +**Rejected alternative:** advertising legacy `` / `` as the preferred syntax. + +--- + +## D032 — The newest real USER move owns normal continuation + +**Status:** Accepted / implemented + +The common browser checkpoint identifies the last runtime session that actually moved. Opening a tab may clone inherited L1 but does not promote the new runtime ID. A real USER send may promote the session before a visible answer finishes; a server completed-turn commit is the fallback signal. A blank bootstrap-only tab never outranks its predecessor. + +Raw-log selection follows the same rule: the newest real USER move wins even when it is interrupted and has no JIN row. `conversation_committed_at` remains a separate completed-turn timestamp. + +**Why:** continuity follows the conversation the user actually touched, not whichever tab most recently initialized or flushed background state. + +**Rejected alternatives:** newest runtime ID wins; newest `saved_at` wins regardless of USER activity; dropping interrupted USER-only moves. + +--- + +## D033 — Bootstrap has separate dialogue and runtime freshness clocks + +**Status:** Accepted / implemented + +Dialogue/reasoning freshness is decided by comparing browser and archive recent-turn tails. Runtime/resource archive enrichment continues to use checkpoint `saved_at`. Field-local room/color/tool-result writes preserve checkpoint freshness metadata. + +Session actions use stable identity plus timestamp ordering: the common checkpoint owns actions at or before its save boundary, and raw logs contribute only a newer tail unless the authoritative source session itself changes. + +**Why:** a fresh runtime clone or room write must not block a newer raw dialogue tail, while an old archive must not overwrite newer browser runtime/resource state. + +**Rejected alternative:** one global timestamp deciding every bootstrap field. + +--- + +## D034 — JIN color has one checkpoint and one bootstrap reconciliation path + +**Status:** Accepted / implemented + +Accepted JIN_COLOR updates `RuntimeContext.jin_color`, emits the live visual event, records an ordered raw runtime event, and is folded into the common session snapshot/room state. There is no separate latest-color storage key. + +On normal boot, the common local checkpoint color is applied during early room restore. The server then sends one authoritative color on `session_actions_update` with `bootstrap_restore=true`. Same-source browser color wins; structured action/raw archive color is fallback. A source change discards the stale browser color with the rest of that source. + +The first bootstrap color uses the one 2-second avatar-and-scene transition. Later/live colors use 333 ms. Field-local color reconciliation may write across the fresh-tab/common-checkpoint ID mismatch only while preserving checkpoint ID, lineage, and `saved_at`. + +**Why:** this removes the pink/gray/red flash-and-revert class caused by multiple color owners and late writers. + +**Rejected alternatives:** `latestJinColor`, `colorOnly` checkpoints, a client source-scanning resolver, a second tint-shift helper, or a delayed color queue. + +--- + +## D035 — Normal bootstrap shows a bounded inherited chat tail + +**Status:** Accepted / implemented + +Normal bootstrap renders the three newest real USER moves with JIN/reasoning where present, then places the current-session date divider and starts the live viewport there. USER-only turns remain visible without an empty JIN bubble. Explicit archived restore uses its own history renderer and must not duplicate this tail. + +**Why:** the user can scroll slightly upward for immediate continuity while the current response begins from a clean, stable boundary. + +**Rejected alternatives:** starting with an empty chat; dumping the whole archive; auto-scrolling inherited history below the current-session boundary. + +--- + +## D036 — Recent model dialogue is bounded by pairs, not character-cropped + +**Status:** Accepted / implemented + +`` contains the newest three recent USER/JIN pairs. Every selected message body is preserved in full after newline normalization, literal `\\n` serialization, whitespace trimming, and XML escaping. There is no per-message character cap. + +**Why:** pair-count bounding already limits history breadth; silently cutting the substance of one selected message breaks exact conversational continuity. + +**Rejected alternative:** restoring `RECENT_MESSAGE_MAX_CHARS` or an equivalent hidden per-message slice without an explicit prompt-budget decision and regression coverage. + +--- + +## D037 — Ordinary turns retain the previous completed reasoning edges + +**Status:** Accepted / implemented + +An ordinary user turn includes the previous successful reasoning in ``. The projection preserves the whole block through 2000 characters; for a larger block it keeps the first and last 25% and replaces only the middle with an explicit `CUTTED N chars` separator. + +Action/recovery follow-ups do not append this ordinary block again. They construct the relevant current-turn or loop-recovery reasoning context explicitly, and the visible TODO context snapshot follows the same distinction. + +**Why:** the opening and conclusion preserve the previous line of thought without spending the entire context window on its middle, while dedicated follow-up context avoids stale or duplicated reasoning. + +**Rejected alternatives:** exposing previous reasoning only during session-restore priming; dropping the block from ordinary turns; duplicating it inside action/recovery follow-ups; trimming only one edge. diff --git a/tests/runtime_actions/test_jin_size.py b/tests/runtime_actions/test_jin_size.py index f3fb7666..13a52649 100644 --- a/tests/runtime_actions/test_jin_size.py +++ b/tests/runtime_actions/test_jin_size.py @@ -10,7 +10,9 @@ from tests.helpers.runtime_actions import FakeEmitter, RuntimeActionTestCase from utils.actions import ( RuntimeActionCall, + RuntimeActionStreamFilter, extract_runtime_actions, + normalize_jin_size_dict, normalize_jin_size_payload, ) from utils.context.runtime_state import build_runtime_xml @@ -29,6 +31,20 @@ def test_jin_size_marker_validates_supported_payload_forms(self): (" 120 ", "120px"), (" w:120 ", "120px"), (" h:120px ", "120px"), + (" 12.5vw ", "12.5vw"), + (" 25% ", "25%"), + ( + " w:25vw h:40vh ", + "w:25vw h:40vh", + ), + ( + " width:50% height:25% ", + "w:50% h:25%", + ), + ( + " 200px 30vh ", + "w:200px h:30vh", + ), ( " width: 500px height: 300px ", "w:500px h:300px", @@ -91,8 +107,18 @@ def test_normalize_jin_size_payload_rejects_bad_sizes(self): "w:120px h:140px", ) self.assertEqual( - normalize_jin_size_payload("120em"), - "120px", + normalize_jin_size_payload("w:12.5vw h:25%"), + "w:12.5vw h:25%", + ) + self.assertEqual( + normalize_jin_size_dict({ + "width": "50%", + "height": "20vh", + }), + { + "width": "50%", + "height": "20vh", + }, ) for payload in ( @@ -100,6 +126,9 @@ def test_normalize_jin_size_payload_rejects_bad_sizes(self): "0", "-1", "w:120 h:-1", + "120em", + "120em 140px", + "w:20em h:30px", ): with self.subTest(payload=payload): self.assertEqual( @@ -108,6 +137,37 @@ def test_normalize_jin_size_payload_rejects_bad_sizes(self): ) + def test_relative_jin_size_marker_survives_stream_chunk_boundaries(self): + + stream_filter = RuntimeActionStreamFilter( + enabled_actions=( + RUNTIME_ACTION_JIN_SIZE, + ), + ) + + first = stream_filter.filter( + "before w:25" + ) + second = stream_filter.filter( + "vw h:40" + ) + third = stream_filter.filter( + "% after" + ) + final = stream_filter.flush_result() + + self.assertEqual(first.text, "before ") + self.assertEqual(first.actions, ()) + self.assertEqual(second.text, "") + self.assertEqual(second.actions, ()) + self.assertEqual(third.text, "after") + self.assertEqual( + [action.payload for action in third.actions], + ["w:25vw h:40%"], + ) + self.assertEqual(final.text, "") + + def test_apply_runtime_action_calls_emits_jin_size(self): async def run_case(): @@ -160,6 +220,55 @@ async def run_case(): asyncio.run(run_case()) + def test_apply_runtime_action_calls_preserves_relative_units(self): + + async def run_case(): + emitter = FakeEmitter() + context = SimpleNamespace( + runtime_action_events=[], + runtime_search_calls=[], + runtime_loaded_skills=[], + runtime_save_session_requested=False, + runtime_save_session_action_emitted=False, + runtime_skill_state_barrier_active=False, + runtime_current_turn_id="turn-relative-size", + logger=None, + emitter=emitter, + ) + actions = ( + RuntimeActionCall( + name=RUNTIME_ACTION_JIN_SIZE, + payload="w:25vw h:40%", + ), + ) + + applied_count = await apply_runtime_action_calls( + context, + actions, + user_message="resize avatar relatively", + ) + + self.assertEqual(applied_count, 1) + self.assertEqual( + context.runtime_action_events[-1]["payload"], + "w:25vw h:40%", + ) + self.assertEqual( + context.runtime_action_events[-1]["width"], + "25vw", + ) + self.assertEqual( + context.runtime_action_events[-1]["height"], + "40%", + ) + self.assertEqual( + emitter.events[-1]["size"], + "w:25vw h:40%", + ) + + asyncio.run(run_case()) + + def test_current_jin_size_context_only_when_avatar_collapsed(self): context = SimpleNamespace( @@ -182,8 +291,23 @@ def test_current_jin_size_context_only_when_avatar_collapsed(self): ), ) + context.runtime_avatar_current_size = { + "width": "25vw", + "height": "40%", + } + + self.assertIn( + "width: 25vw height: 40%", + build_runtime_xml( + context, + runtime_actions={ + "CAN_JIN_SIZE": True, + }, + ), + ) + self.assertIn( - " 120px 120px ", + " w:120 h:120 ", build_runtime_action_instructions( ( RUNTIME_ACTION_JIN_SIZE, diff --git a/tests/test_chat_response_formatter.py b/tests/test_chat_response_formatter.py index 91c58ff6..dda4c670 100644 --- a/tests/test_chat_response_formatter.py +++ b/tests/test_chat_response_formatter.py @@ -185,6 +185,54 @@ def test_jin_size_marker_is_rendered_as_runtime_marker(self): completed.stderr or completed.stdout, ) + @unittest.skipUnless( + shutil.which("node"), + "node is required for the browser-side response formatter test", + ) + def test_jin_size_marker_preserves_supported_relative_units(self): + script = r''' +const fs = require("fs"); +global.window = {}; +eval(fs.readFileSync(process.argv[1], "utf8")); + +const normalize = window.JinResponseFormatter.normalizeJinSizeMarker; +const cases = [ + ["120", "120px"], + ["12.5vw", "12.5vw"], + ["w:25vw h:40vh", "w:25vw h:40vh"], + ["width:50% height:25%", "w:50% h:25%"], + ["200px 30vh", "w:200px h:30vh"], +]; + +for (const [input, expected] of cases) { + const actual = normalize(input); + if (actual !== expected) { + throw new Error(`unexpected normalized size for ${input}: ${actual}`); + } +} + +if (normalize("120em") !== "") { + throw new Error("unsupported CSS unit must not be displayed as px"); +} +''' + completed = subprocess.run( + [ + shutil.which("node"), + "-e", + script, + str(FORMATTER_JS), + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + completed.returncode, + 0, + completed.stderr or completed.stdout, + ) + def test_formatter_script_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") diff --git a/tests/test_runtime_panel_client_contract.py b/tests/test_runtime_panel_client_contract.py index b813943a..7feda028 100644 --- a/tests/test_runtime_panel_client_contract.py +++ b/tests/test_runtime_panel_client_contract.py @@ -1,4 +1,6 @@ from pathlib import Path +import shutil +import subprocess import unittest @@ -194,6 +196,67 @@ def test_jin_size_collapsed_avatar_resize_animates_inside_viewport(self): css_source, ) + @unittest.skipUnless( + shutil.which("node"), + "node is required for the browser-side JIN size test", + ) + def test_jin_size_relative_units_resolve_against_live_viewport(self): + script = r''' +const fs = require("fs"); +const source = fs.readFileSync(process.argv[1], "utf8"); +const start = source.indexOf("function normalizeJinSizeLength("); +const end = source.indexOf("function normalizeJinPositionPayload(", start); + +if (start < 0 || end < 0) { + throw new Error("JIN size normalization helpers not found"); +} + +global.window = { innerWidth: 2000, innerHeight: 1000 }; +global.document = { + documentElement: { clientWidth: 2000, clientHeight: 1000 }, +}; + +const helpers = eval( + `(() => { ${source.slice(start, end)}; return { normalizeJinSizePayload }; })()` +); +const cases = [ + ["120px", { width: 120, height: 120 }], + ["w:25vw h:40vh", { width: 500, height: 400 }], + ["w:50% h:25%", { width: 1000, height: 250 }], + ["10%", { width: 200, height: 100 }], + ["w:12.5vw h:20%", { width: 250, height: 200 }], + [{ size: "w:25vw h:40%", width: 25, height: 40 }, { width: 500, height: 400 }], +]; + +for (const [input, expected] of cases) { + const actual = helpers.normalizeJinSizePayload(input); + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`unexpected size for ${JSON.stringify(input)}: ${JSON.stringify(actual)}`); + } +} + +if (helpers.normalizeJinSizePayload("120em") !== null) { + throw new Error("unsupported CSS unit must not be treated as px"); +} +''' + completed = subprocess.run( + [ + shutil.which("node"), + "-e", + script, + str(LOGGER_JS), + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + completed.returncode, + 0, + completed.stderr or completed.stdout, + ) + def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") @@ -205,6 +268,7 @@ def test_cache_versions_are_bumped(self): "/static/js/socket/input.js?v=jin-size-1", source, ) + self.assertIn("jin-size-units=1", source) if __name__ == "__main__": diff --git a/ui/static/js/chat-response-formatter.js b/ui/static/js/chat-response-formatter.js index dc81da6b..a200d338 100644 --- a/ui/static/js/chat-response-formatter.js +++ b/ui/static/js/chat-response-formatter.js @@ -194,26 +194,44 @@ return ""; } - const numberPattern = - /^(\d+)(?:px)?$/i; + const normalizeLength = (rawValue) => { + const match = String(rawValue || "").trim().match( + /^([+]?(?:\d+(?:\.\d+)?|\.\d+))\s*(px|vw|vh|%)?$/i + ); + + if (!match) { + return ""; + } + + const amount = Number.parseFloat(match[1]); + + if (!Number.isFinite(amount) || amount <= 0) { + return ""; + } + + const normalizedAmount = Number.isInteger(amount) + ? String(amount) + : String(amount).replace(/0+$/, "").replace(/\.$/, ""); + const unit = String(match[2] || "px").toLowerCase(); + + return `${normalizedAmount}${unit}`; + }; const labeledPattern = - /([wh])\s*:\s*(\d+)(?:px)?/gi; + /(width|height|w|h)\s*:\s*([+]?(?:\d+(?:\.\d+)?|\.\d+)\s*(?:px|vw|vh|%)?)/gi; const labeled = {}; const spans = []; let match = null; while ((match = labeledPattern.exec(source)) !== null) { const label = - match[1].toLowerCase(); + match[1].toLowerCase().startsWith("w") + ? "w" + : "h"; const size = - Number.parseInt( - match[2], - 10 - ); + normalizeLength(match[2]); if ( - !Number.isFinite(size) - || size <= 0 + !size || labeled[label] ) { return ""; @@ -249,7 +267,7 @@ Object.values(labeled); if (values.length === 1) { - return `${values[0]}px`; + return values[0]; } if ( @@ -257,8 +275,8 @@ && labeled.h ) { return labeled.w === labeled.h - ? `${labeled.w}px` - : `w:${labeled.w}px h:${labeled.h}px`; + ? labeled.w + : `w:${labeled.w} h:${labeled.h}`; } return ""; @@ -274,37 +292,21 @@ return ""; } - const numbers = - parts.map((part) => { - const numberMatch = - part.match(numberPattern); - - if (!numberMatch) { - return 0; - } - - return Number.parseInt( - numberMatch[1], - 10 - ); - }); + const sizes = parts.map(normalizeLength); if ( - numbers.some((number) => ( - !Number.isFinite(number) - || number <= 0 - )) + sizes.some((size) => !size) ) { return ""; } - if (numbers.length === 1) { - return `${numbers[0]}px`; + if (sizes.length === 1) { + return sizes[0]; } - return numbers[0] === numbers[1] - ? `${numbers[0]}px` - : `w:${numbers[0]}px h:${numbers[1]}px`; + return sizes[0] === sizes[1] + ? sizes[0] + : `w:${sizes[0]} h:${sizes[1]}`; } diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 9d592767..e57dacb7 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -1945,38 +1945,50 @@ function extractRuntimeActionColorFromText(text) { function normalizeRuntimeActionSize(value) { + const formatter = + window.JinResponseFormatter + && typeof window.JinResponseFormatter.normalizeJinSizeMarker === "function" + ? window.JinResponseFormatter.normalizeJinSizeMarker + : null; + if ( value && typeof value === "object" ) { - const width = - Number.parseInt( - value.width || value.w || 0, - 10 - ); - const height = - Number.parseInt( - value.height || value.h || width || 0, - 10 + const nestedPayload = String( + value.size || value.payload || "" + ).trim(); + + if (nestedPayload && formatter) { + const normalizedPayload = formatter( + nestedPayload ); + if (normalizedPayload) { + return normalizedPayload; + } + } + + const rawWidth = value.width ?? value.w; + const rawHeight = value.height ?? value.h ?? rawWidth; + if ( - Number.isFinite(width) - && Number.isFinite(height) - && width > 0 - && height > 0 + rawWidth !== undefined + && rawHeight !== undefined + && formatter ) { - return width === height - ? `${width}px` - : `w:${width}px h:${height}px`; + const normalizedDimensions = formatter( + `w:${rawWidth} h:${rawHeight}` + ); + + if (normalizedDimensions) { + return normalizedDimensions; + } } } - if ( - window.JinResponseFormatter - && typeof window.JinResponseFormatter.normalizeJinSizeMarker === "function" - ) { - return window.JinResponseFormatter.normalizeJinSizeMarker( + if (formatter) { + return formatter( value ); } diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index b82ea85d..5652f115 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -126,47 +126,37 @@ function getInternalActionJinSizeHover(data) { return ""; } - let width = Number.parseInt( - data.width || 0, - 10 - ); - let height = Number.parseInt( - data.height || 0, - 10 - ); - - if (!(width > 0) || !(height > 0)) { - const source = String( - data.size - || data.payload - || ( - Array.isArray(data.sizes) - ? data.sizes[data.sizes.length - 1] - : "" - ) - || "" - ); - const numbers = Array.from( - source.matchAll(/\d+/g) + const source = String( + data.size + || data.payload + || ( + Array.isArray(data.sizes) + ? data.sizes[data.sizes.length - 1] + : "" ) - .slice(0, 2) - .map((match) => Number.parseInt(match[0], 10)) - .filter((value) => Number.isFinite(value) && value > 0); - - if (!(width > 0) && numbers.length) { - width = numbers[0]; - } - - if (!(height > 0)) { - height = numbers[1] || width; - } - } + || ( + data.width + ? `w:${data.width} h:${data.height || data.width}` + : "" + ) + ).trim(); + const normalized = + window.JinResponseFormatter + && typeof window.JinResponseFormatter.normalizeJinSizeMarker === "function" + ? window.JinResponseFormatter.normalizeJinSizeMarker(source) + : ""; - if (!(width > 0) || !(height > 0)) { + if (!normalized) { return ""; } - return `width: ${width}px\nheight: ${height}px`; + const labeled = normalized.match( + /^w:([^\s]+)\s+h:([^\s]+)$/i + ); + const width = labeled ? labeled[1] : normalized; + const height = labeled ? labeled[2] : normalized; + + return `width: ${width}\nheight: ${height}`; } function formatInternalActionPayload(payload) { diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 46007d06..5da7bc76 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -557,22 +557,98 @@ const consolePanel = document.getElementById("console-panel"); ); } + function normalizeJinSizeLength(value) { + const source = String(value || "").trim(); + const match = source.match( + /^([+]?(?:\d+(?:\.\d+)?|\.\d+))\s*(px|vw|vh|%)?$/i + ); + + if (!match) { + return null; + } + + const amount = Number.parseFloat(match[1]); + + if (!Number.isFinite(amount) || amount <= 0) { + return null; + } + + return { + amount, + unit: String(match[2] || "px").toLowerCase(), + }; + } + + function getJinSizeViewportPixels() { + const root = document.documentElement; + const width = Number( + window.innerWidth + || (root && root.clientWidth) + || 0 + ); + const height = Number( + window.innerHeight + || (root && root.clientHeight) + || 0 + ); + + return { + width: Math.max(1, width), + height: Math.max(1, height), + }; + } + + function resolveJinSizeLengthPixels(value, axis) { + const normalized = normalizeJinSizeLength(value); + + if (!normalized) { + return Number.NaN; + } + + const viewport = getJinSizeViewportPixels(); + let pixels = normalized.amount; + + if (normalized.unit === "vw") { + pixels = viewport.width * normalized.amount / 100; + } else if (normalized.unit === "vh") { + pixels = viewport.height * normalized.amount / 100; + } else if (normalized.unit === "%") { + pixels = viewport[axis] * normalized.amount / 100; + } + + return Math.round(pixels); + } + function normalizeJinSizePayload(value) { if ( value && typeof value === "object" ) { - const width = - Number.parseInt( - value.width || value.w || 0, - 10 - ); - const height = - Number.parseInt( - value.height || value.h || width || 0, - 10 + const nestedPayload = String( + value.size || value.payload || "" + ).trim(); + + if (nestedPayload) { + const nestedSize = normalizeJinSizePayload( + nestedPayload ); + if (nestedSize) { + return nestedSize; + } + } + + const rawWidth = value.width ?? value.w; + const rawHeight = value.height ?? value.h ?? rawWidth; + const width = resolveJinSizeLengthPixels( + rawWidth, + "width" + ); + const height = resolveJinSizeLengthPixels( + rawHeight, + "height" + ); + if ( Number.isFinite(width) && Number.isFinite(height) @@ -586,46 +662,61 @@ const consolePanel = document.getElementById("console-panel"); } } - const source = - String( - value || "" - ).trim(); + const source = String(value || "").trim(); if (!source) { return null; } - const single = - source.match( - /^(\d+)(?:px)?$/i - ); + const length = + "([+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)\\s*(?:px|vw|vh|%)?)"; + const single = source.match( + new RegExp(`^${length}$`, "i") + ); + const labeled = source.match( + new RegExp( + `^(?:w|width)\\s*:\\s*${length}\\s+` + + `(?:h|height)\\s*:\\s*${length}$`, + "i" + ) + ); + const plain = source.match( + new RegExp(`^${length}\\s+${length}$`, "i") + ); - if (single) { - const size = - Number.parseInt( - single[1], - 10 - ); + const rawWidth = single + ? single[1] + : (labeled ? labeled[1] : (plain ? plain[1] : "")); + const rawHeight = single + ? single[1] + : (labeled ? labeled[2] : (plain ? plain[2] : "")); - return { - width: size, - height: size, - }; + if (!rawWidth || !rawHeight) { + return null; } - const labeled = - source.match( - /^w\s*:\s*(\d+)(?:px)?\s+h\s*:\s*(\d+)(?:px)?$/i - ); + const width = resolveJinSizeLengthPixels( + rawWidth, + "width" + ); + const height = resolveJinSizeLengthPixels( + rawHeight, + "height" + ); - if (labeled) { - return { - width: Number.parseInt(labeled[1], 10), - height: Number.parseInt(labeled[2], 10), - }; + if ( + !Number.isFinite(width) + || !Number.isFinite(height) + || width <= 0 + || height <= 0 + ) { + return null; } - return null; + return { + width, + height, + }; } function normalizeJinPositionPayload(value) { diff --git a/ui/templates/index.html b/ui/templates/index.html index eab49807..81f00fa0 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -519,18 +519,18 @@ - + - + - + - + diff --git a/utils/actions/__init__.py b/utils/actions/__init__.py index 6929cb53..85ec4aa0 100644 --- a/utils/actions/__init__.py +++ b/utils/actions/__init__.py @@ -67,6 +67,7 @@ normalize_jin_speed_value, ) from .jin_size_utils import ( + format_jin_size_value, format_jin_size_payload, get_applied_jin_size, is_noop_jin_size_action, @@ -165,5 +166,6 @@ "format_jin_position_payload", "format_jin_speed_payload", "format_jin_size_payload", + "format_jin_size_value", "DEFAULT_RUNTIME_JIN_SPEED", ] diff --git a/utils/actions/jin_size_utils.py b/utils/actions/jin_size_utils.py index 62dff3eb..8db8a5be 100644 --- a/utils/actions/jin_size_utils.py +++ b/utils/actions/jin_size_utils.py @@ -1,3 +1,4 @@ +import math import re from contracts.rules_assembler import RUNTIME_ACTION_JIN_SIZE @@ -8,21 +9,66 @@ "height": 120, } -MAX_RUNTIME_JIN_SIZE_PIXELS = 10000 +MAX_RUNTIME_JIN_SIZE_VALUE = 10000 JIN_SIZE_NUMBER_RE = re.compile( - r"^(?P\d+)(?:px)?$", + r"^(?P[+-]?(?:\d+(?:\.\d+)?|\.\d+))" + r"\s*(?Ppx|vw|vh|%)?$", re.IGNORECASE, ) JIN_SIZE_VALUE_RE = re.compile( - r"[+-]?\d+", + r"(?[+-]?(?:\d+(?:\.\d+)?|\.\d+)" + r"\s*(?:px|vw|vh|%)?)" + r"(?![a-zA-Z0-9_.%])", + re.IGNORECASE, +) + +JIN_SIZE_LABEL_PREFIX_RE = re.compile( + r"(?[a-zA-Z]+)" + r"(?![a-zA-Z0-9_])" + r"(?!\s*[:=])", + re.IGNORECASE, +) + +JIN_SIZE_LABELED_VALUE_RE = re.compile( + r"(?width|height|w|h)\s*[:=]\s*" + r"(?P[+-]?(?:\d+(?:\.\d+)?|\.\d+)" + r"\s*(?:px|vw|vh|%)?)" + r"(?![a-zA-Z0-9_.%])", + re.IGNORECASE, ) -def _parse_size_number( +def _format_size_number( + value: float, +) -> str: + + if value.is_integer(): + return str(int(value)) + + return ( + f"{value:.6f}" + .rstrip("0") + .rstrip(".") + ) + + +def _parse_size_value( value, -) -> int | None: +) -> int | float | str | None: + + if isinstance(value, bool): + return None match = JIN_SIZE_NUMBER_RE.fullmatch( str(value or "").strip() @@ -32,7 +78,7 @@ def _parse_size_number( return None try: - number = int( + number = float( match.group("value") ) except ( @@ -42,17 +88,49 @@ def _parse_size_number( return None if ( - number <= 0 - or number > MAX_RUNTIME_JIN_SIZE_PIXELS + not math.isfinite(number) + or number <= 0 + or number > MAX_RUNTIME_JIN_SIZE_VALUE ): return None - return number + unit = str( + match.group("unit") + or "px" + ).casefold() + + if unit == "px": + return ( + int(number) + if number.is_integer() + else number + ) + + return f"{_format_size_number(number)}{unit}" + + +def format_jin_size_value( + value, +) -> str: + + normalized = _parse_size_value( + value + ) + + if normalized is None: + return "" + + if isinstance(normalized, str): + return normalized + + return ( + f"{_format_size_number(float(normalized))}px" + ) def parse_jin_size_payload( payload: str, -) -> dict[str, int] | None: +) -> dict[str, int | float | str] | None: text = str( payload @@ -62,37 +140,104 @@ def parse_jin_size_payload( if not text: return None - raw_numbers = [ - match.group(0) + if any( + str(match.group("unit") or "").casefold() + not in {"px", "vw", "vh"} + for match in JIN_SIZE_EXPLICIT_ALPHA_UNIT_RE.finditer( + text + ) + ): + return None + + label_prefixes = list( + JIN_SIZE_LABEL_PREFIX_RE.finditer( + text + ) + ) + + if label_prefixes: + labeled_matches = list( + JIN_SIZE_LABELED_VALUE_RE.finditer( + text + ) + ) + + if len(labeled_matches) != len(label_prefixes): + return None + + dimensions = {} + + for match in labeled_matches: + label = str( + match.group("label") + or "" + ).casefold() + dimension = ( + "width" + if label in {"w", "width"} + else "height" + ) + + if dimension in dimensions: + return None + + normalized = _parse_size_value( + match.group("value") + ) + + if normalized is None: + return None + + dimensions[dimension] = normalized + + width = dimensions.get("width") + height = dimensions.get("height") + + if width is None: + width = height + + if height is None: + height = width + + if width is None or height is None: + return None + + return { + "width": width, + "height": height, + } + + raw_values = [ + match.group("value") for match in JIN_SIZE_VALUE_RE.finditer(text) ] - if not raw_numbers: + if not raw_values: return None - numbers = [] + values = [] - for raw_number in raw_numbers[:2]: - number = _parse_size_number( - raw_number + for raw_value in raw_values[:2]: + normalized = _parse_size_value( + raw_value ) - if number is None: + if normalized is None: return None - numbers.append( - number + values.append( + normalized ) - if len(numbers) == 1: + if len(values) == 1: return { - "width": numbers[0], - "height": numbers[0], + "width": values[0], + "height": values[0], } return { - "width": numbers[0], - "height": numbers[1], + "width": values[0], + "height": values[1], } @@ -106,29 +251,24 @@ def format_jin_size_payload( ): return "" - try: - width = int( - size.get("width") - ) - height = int( - size.get("height") - ) - except ( - TypeError, - ValueError, - ): - return "" + normalized = normalize_jin_size_dict( + size + ) - if ( - width <= 0 - or height <= 0 - ): + if not normalized: return "" + width = format_jin_size_value( + normalized["width"] + ) + height = format_jin_size_value( + normalized["height"] + ) + if width == height: - return f"{width}px" + return width - return f"w:{width}px h:{height}px" + return f"w:{width} h:{height}" def normalize_jin_size_payload( @@ -144,7 +284,7 @@ def normalize_jin_size_payload( def normalize_jin_size_dict( value, -) -> dict[str, int] | None: +) -> dict[str, int | float | str] | None: if isinstance( value, @@ -169,22 +309,22 @@ def normalize_jin_size_dict( if width is None: width = height - width_number = _parse_size_number( + width_value = _parse_size_value( width ) - height_number = _parse_size_number( + height_value = _parse_size_value( height ) if ( - width_number is None - or height_number is None + width_value is None + or height_value is None ): return None return { - "width": width_number, - "height": height_number, + "width": width_value, + "height": height_value, } parsed = parse_jin_size_payload( @@ -196,7 +336,7 @@ def normalize_jin_size_dict( def get_applied_jin_size( context=None, -) -> dict[str, int]: +) -> dict[str, int | float | str]: current_size = dict( DEFAULT_RUNTIME_JIN_SIZE diff --git a/utils/context/runtime_state.py b/utils/context/runtime_state.py index 6ab4be2f..0fed6a3c 100644 --- a/utils/context/runtime_state.py +++ b/utils/context/runtime_state.py @@ -24,6 +24,7 @@ DEFAULT_JIN_COLOR, ) from utils.actions import ( + format_jin_size_value, get_applied_jin_size, normalize_jin_size_dict, normalize_jin_color_payload, @@ -95,8 +96,10 @@ def format_current_jin_size( return "" return ( - f"width: {normalized_size['width']}px " - f"height: {normalized_size['height']}px" + "width: " + f"{format_jin_size_value(normalized_size['width'])} " + "height: " + f"{format_jin_size_value(normalized_size['height'])}" ) From 6d402511044b817aeef6fbfdd7760fbfe14fa302 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:07:56 +0300 Subject: [PATCH 102/168] - fix tests --- tests/runtime_actions/test_active_memory.py | 103 +- tests/runtime_actions/test_asset_actions.py | 1 - tests/runtime_actions/test_delayed_memory.py | 1 - tests/runtime_actions/test_jin_color.py | 1 - tests/runtime_actions/test_skill_actions.py | 8 +- .../test_skill_marker_semantics.py | 2 +- tests/runtime_actions/test_stream_filter.py | 248 +--- tests/runtime_actions/test_todo.py | 1 - tests/test_anonymous_mode.py | 2 +- tests/test_archived_session_restore.py | 78 +- tests/test_attached_files_client_contract.py | 9 +- tests/test_behavior_contract.py | 54 +- ...trap_latest_session_fix_client_contract.py | 39 +- tests/test_brain_asset_flow.py | 236 +--- tests/test_brain_prompt_memory.py | 134 +- tests/test_brain_runtime_actions.py | 617 +-------- ...test_chat_stream_avatar_client_contract.py | 2 +- ...l_results_bootstrap_checkpoint_contract.py | 2 +- tests/test_current_context_window.py | 2 +- tests/test_delayed_memory_client_contract.py | 30 +- ...ayed_memory_pinned_glow_client_contract.py | 10 +- tests/test_facts_memory_client_contract.py | 17 +- tests/test_jin_motion_client_contract.py | 2 +- tests/test_l1_memory.py | 8 +- tests/test_l2_memory.py | 880 ------------ tests/test_l3_session_memory.py | 1214 ----------------- tests/test_l4_logger_cards_client_contract.py | 12 +- tests/test_live_session_checkpoint.py | 30 +- ...t_memory_reference_sync_client_contract.py | 18 +- tests/test_metabolism_homeostat.py | 2 +- ...t_reconnect_persistence_client_contract.py | 18 +- ...avatar_reasoning_motion_client_contract.py | 10 +- tests/test_runtime_stream_tokens.py | 96 +- tests/test_runtime_todo.py | 2 +- tests/test_save_session_l3_client_contract.py | 61 - tests/test_search_flow.py | 18 +- tests/test_stream_validator.py | 4 +- tests/test_ui_fix_bundle_contract.py | 2 +- ...pdate_l4_facts_message_display_contract.py | 8 +- tests/test_websocket_pending_usage.py | 127 +- 40 files changed, 396 insertions(+), 3713 deletions(-) delete mode 100644 tests/test_l2_memory.py delete mode 100644 tests/test_l3_session_memory.py delete mode 100644 tests/test_save_session_l3_client_contract.py diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 0d116ba5..77b5d19e 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, @@ -425,7 +424,7 @@ def test_apply_runtime_action_calls_emits_save_active_memory_bubble(self): ) self.assertEqual( context.emitter.events[0]["text"], - "SAVE_ACTIVE_MEMORY: remind later", + "SAVE_ACTIVE_MEMORY", ) self.assertEqual( context.emitter.events[0]["display_name"], @@ -449,6 +448,7 @@ def test_apply_runtime_action_calls_emits_save_active_memory_bubble(self): r"\[ created_jin_message_number: 3 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " + r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) @@ -555,8 +555,8 @@ def test_apply_runtime_action_calls_emits_one_bubble_per_saved_active_memory(sel if not event.get("status") ], [ - "SAVE_ACTIVE_MEMORY: first reminder", - "SAVE_ACTIVE_MEMORY: second reminder", + "SAVE_ACTIVE_MEMORY", + "SAVE_ACTIVE_MEMORY", ], ) self.assertEqual( @@ -619,7 +619,7 @@ def test_save_active_memory_replaces_model_runtime_suffixes(self): ) self.assertEqual( context.emitter.events[0]["text"], - "SAVE_ACTIVE_MEMORY: Experiment Progress: 2m elapsed", + "SAVE_ACTIVE_MEMORY", ) self.assertEqual( context.runtime_action_events[0]["payload"], @@ -639,6 +639,7 @@ def test_save_active_memory_replaces_model_runtime_suffixes(self): r"\[ created_jin_message_number: 8 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " + r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) @@ -768,7 +769,7 @@ def test_extracts_update_active_memory_self_closing_attribute_marker(self): ) - def test_save_active_memory_materializes_hidden_custom_state_fields(self): + def test_save_active_memory_materializes_json_custom_state_fields(self): context = FakeContext() context.emitter = FakeEmitter() @@ -783,9 +784,9 @@ def test_save_active_memory_materializes_hidden_custom_state_fields(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_photo_count: 1)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_photo_count":1}' ), ), ), @@ -805,7 +806,7 @@ def test_save_active_memory_materializes_hidden_custom_state_fields(self): self.assertNotIn("(current_photo_count:", record) self.assertEqual( context.emitter.events[0]["text"], - "SAVE_ACTIVE_MEMORY: Once a day ask for a photo.", + "SAVE_ACTIVE_MEMORY", ) self.assertEqual( context.emitter.events[0]["payload"], @@ -828,9 +829,9 @@ def test_update_active_memory_changes_only_fixed_fields_and_adds_updated_at(self RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_photo_count: 1)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_photo_count":1}' ), ), ), @@ -903,9 +904,9 @@ def test_update_active_memory_accepts_numbered_slot_key_reference(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_photo_count: 1)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_photo_count":1}' ), ), ), @@ -963,9 +964,9 @@ def test_update_active_memory_accepts_numbered_slot_key_inside_raw_marker(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_date: 2026-08-21)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_date":"2026-08-21"}' ), ), ), @@ -1024,9 +1025,9 @@ def test_update_active_memory_accepts_json_numbered_slot_key(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_date: 2026-08-21)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_date":"2026-08-21"}' ), ), ), @@ -1086,9 +1087,9 @@ def test_update_active_memory_json_fields_ignores_creation_time(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(current_photo_id: qamzck) " - "(current_photo_count: 1)" + '{"conditions":"Once a day ask for a photo.",' + '"current_photo_id":"qamzck",' + '"current_photo_count":1}' ), ), ), @@ -1158,9 +1159,9 @@ def test_update_active_memory_accepts_json_inside_raw_marker(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck) " - "(current_date: 2026-08-21)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck",' + '"current_date":"2026-08-21"}' ), ), ), @@ -1302,8 +1303,8 @@ def test_update_active_memory_rejects_new_field_atomically(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck"}' ), ), ), @@ -1360,8 +1361,8 @@ def test_update_active_memory_slot_key_rejects_new_field_atomically(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck"}' ), ), ), @@ -1421,8 +1422,8 @@ def test_update_active_memory_slot_key_rejects_runtime_managed_field(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck"}' ), ), ), @@ -1471,8 +1472,8 @@ def test_update_active_memory_json_rejects_runtime_managed_field(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Once a day ask for a photo. " - "(last_photo_id: qamzck)" + '{"conditions":"Once a day ask for a photo.",' + '"last_photo_id":"qamzck"}' ), ), ), @@ -1508,7 +1509,7 @@ def test_update_active_memory_json_rejects_runtime_managed_field(self): ) - def test_save_active_memory_rejects_more_than_three_custom_fields(self): + def test_save_active_memory_caps_custom_fields_at_three(self): context = FakeContext() context.emitter = FakeEmitter() @@ -1523,22 +1524,23 @@ def test_save_active_memory_rejects_more_than_three_custom_fields(self): RuntimeActionCall( name="SAVE_ACTIVE_MEMORY", payload=( - "Track a small state. " - "(field_one: 1) " - "(field_two: 2) " - "(field_three: 3) " - "(field_four: 4)" + '{"conditions":"Track a small state.",' + '"field_one":1,' + '"field_two":2,' + '"field_three":3,' + '"field_four":4}' ), ), ), ) ) - self.assertEqual(applied_count, 0) - self.assertEqual( - getattr(context, "active_memory_records", []), - [], - ) + self.assertEqual(applied_count, 1) + record = context.active_memory_records[0] + self.assertIn("[ field_one: 1 ]", record) + self.assertIn("[ field_two: 2 ]", record) + self.assertIn("[ field_three: 3 ]", record) + self.assertNotIn("field_four", record) def test_save_active_memory_accepts_flat_json_and_keeps_last_duplicate(self): @@ -1660,6 +1662,7 @@ def test_apply_runtime_action_calls_queues_active_memory_record(self): r"\[ created_jin_message_number: 7 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " + r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) @@ -1673,7 +1676,7 @@ def test_apply_runtime_action_calls_queues_active_memory_record(self): ) self.assertEqual( context.emitter.events[0]["text"], - "SAVE_ACTIVE_MEMORY: Drink coffee | Trigger in 5 minutes | coffee", + "SAVE_ACTIVE_MEMORY", ) self.assertEqual( context.emitter.events[0]["display_name"], @@ -1749,7 +1752,7 @@ def test_apply_runtime_action_calls_skips_exact_active_memory_copy(self): "action": "save_active_memory", "id": "save_active_memory_001", "display_name": "SAVE_ACTIVE_MEMORY", - "text": "SAVE_ACTIVE_MEMORY: remember cuckoo", + "text": "SAVE_ACTIVE_MEMORY", "payload": "remember cuckoo", "close_tag": True, }, @@ -1823,7 +1826,7 @@ def test_apply_runtime_action_calls_skips_active_memory_copy_from_runtime_memory "action": "save_active_memory", "id": "save_active_memory_001", "display_name": "SAVE_ACTIVE_MEMORY", - "text": "SAVE_ACTIVE_MEMORY: remember cuckoo", + "text": "SAVE_ACTIVE_MEMORY", "payload": "remember cuckoo", "close_tag": True, }, diff --git a/tests/runtime_actions/test_asset_actions.py b/tests/runtime_actions/test_asset_actions.py index 706f7dac..0caf6a42 100644 --- a/tests/runtime_actions/test_asset_actions.py +++ b/tests/runtime_actions/test_asset_actions.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index da808b98..d0650768 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index 52771449..e6a1608c 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, diff --git a/tests/runtime_actions/test_skill_actions.py b/tests/runtime_actions/test_skill_actions.py index 804f3d29..1c48203b 100644 --- a/tests/runtime_actions/test_skill_actions.py +++ b/tests/runtime_actions/test_skill_actions.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, @@ -462,7 +461,6 @@ def preserve_duplicate_load_skill(_raw_marker, action): return False text = ( - "\n" "\n" "\n" "\n" @@ -474,7 +472,6 @@ def preserve_duplicate_load_skill(_raw_marker, action): result = extract_runtime_actions( text, enabled_actions=[ - "CAN_SAVE_SESSION", "CAN_USE_ASSETS", ], preserve_action_marker=preserve_duplicate_load_skill, @@ -483,9 +480,6 @@ def preserve_duplicate_load_skill(_raw_marker, action): self.assertEqual( result.actions, ( - RuntimeActionCall( - name="SAVE_SESSION", - ), RuntimeActionCall( name="LOAD_SKILL", payload="file_manager", @@ -518,7 +512,7 @@ def preserve_duplicate_load_skill(_raw_marker, action): ) self.assertEqual( len(result.removed_markers), - 5, + 4, ) diff --git a/tests/runtime_actions/test_skill_marker_semantics.py b/tests/runtime_actions/test_skill_marker_semantics.py index 5a1accbb..2052fcdb 100644 --- a/tests/runtime_actions/test_skill_marker_semantics.py +++ b/tests/runtime_actions/test_skill_marker_semantics.py @@ -279,7 +279,7 @@ def test_skill_marker_ui_contract_keeps_rows_separate_and_uncounted(self): runtime_source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") index_source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("keepSkillMarkerSeparate", logger_source) + self.assertIn("keepActionInstanceSeparate", logger_source) self.assertIn('"LOAD_SKILLS"', logger_source) self.assertIn('"load_skills"', runtime_source) self.assertIn("suppressMarkerCount", runtime_source) diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index dd358606..6b3fa4e0 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, @@ -52,7 +51,6 @@ TOOL_RESULT_KIND_ASSET, TOOL_RESULT_KIND_DELAYED_MEMORY, TOOL_RESULT_KIND_SEARCH, - TOOL_RESULT_KIND_SESSION, begin_runtime_tool_results_turn, record_runtime_tool_result, ) @@ -524,31 +522,6 @@ def test_ignores_placeholder_bracketed_web_search_marker(self): ) - def test_extracts_bracketed_save_session_marker(self): - - result = extract_runtime_actions( - "", - enabled_actions=[ - "CAN_SAVE_SESSION", - ], - ) - - self.assertEqual( - result.text, - "", - ) - self.assertEqual( - result.count("SAVE_SESSION"), - 1, - ) - self.assertEqual( - result.removed_markers, - ( - "", - ), - ) - - def test_extracts_clean_tool_results_marker(self): result = extract_runtime_actions( @@ -626,7 +599,6 @@ def test_repeated_clean_tool_results_markers_remain_countable(self): def test_extracts_self_closing_runtime_markers_without_blocks(self): cases = ( - ("", "SAVE_SESSION", ""), ( "", "WEB_SEARCH", @@ -843,24 +815,6 @@ def test_dedupes_duplicate_runtime_action_markers_by_payload(self): ), "Before middle after", ), - ( - ( - "Before " - "" - " middle " - "" - " after" - ), - [ - "CAN_SAVE_SESSION", - ], - ( - RuntimeActionCall( - name="SAVE_SESSION", - ), - ), - "Before middle after", - ), ( ( "Before\n" @@ -879,23 +833,6 @@ def test_dedupes_duplicate_runtime_action_markers_by_payload(self): ), "Before\nAfter", ), - ( - ( - "Before\n" - "\n" - "\n" - "After" - ), - [ - "CAN_SAVE_SESSION", - ], - ( - RuntimeActionCall( - name="SAVE_SESSION", - ), - ), - "Before\nAfter", - ), ) for text, enabled_actions, expected_actions, expected_text in cases: @@ -1938,181 +1875,6 @@ def test_apply_runtime_action_calls_keeps_distinct_search_queries(self): ) - def test_bracketed_save_session_marker_allowed_by_save_request(self): - - Context = FakeContext - - context = Context() - result = extract_runtime_actions( - "", - enabled_actions=[ - "CAN_SAVE_SESSION", - ], - ) - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - result.actions, - user_message="save session", - ) - ) - - self.assertEqual( - result.text, - "", - ) - self.assertEqual( - applied_count, - 1, - ) - self.assertTrue( - context.runtime_save_session_requested, - ) - - - def test_save_session_marker_is_allowed_after_same_turn_l3_commit(self): - - Context = FakeContext - - context = Context() - # A previous SAVE_SESSION may already have completed earlier in the - # same user turn (for example via the deterministic direct trigger). - # A marker emitted by the next internal follow-up is a new model - # message and must be allowed to request a fresh snapshot. - context.runtime_save_session_memory_committed_this_turn = True - result = extract_runtime_actions( - "", - enabled_actions=[ - "CAN_SAVE_SESSION", - ], - ) - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - result.actions, - user_message="save session", - runtime_message_id="followup-2", - ) - ) - - self.assertEqual( - applied_count, - 1, - ) - self.assertTrue( - getattr( - context, - "runtime_save_session_requested", - False, - ), - ) - - - def test_bracketed_save_session_marker_allowed_by_trigger(self): - - Context = FakeContext - - context = Context() - result = extract_runtime_actions( - "", - enabled_actions=[ - "CAN_SAVE_SESSION", - ], - ) - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - result.actions, - user_message="save session", - ) - ) - - self.assertEqual( - applied_count, - 1, - ) - self.assertTrue( - context.runtime_save_session_requested, - ) - - - def test_bracketed_save_session_marker_blocked_by_meta_request(self): - - Context = FakeContext - - context = Context() - result = extract_runtime_actions( - "", - enabled_actions=[ - "CAN_SAVE_SESSION", - ], - ) - - applied_count = asyncio.run( - apply_runtime_action_calls( - context, - result.actions, - user_message="show tag", - ) - ) - - self.assertEqual( - result.text, - "", - ) - self.assertEqual( - applied_count, - 0, - ) - self.assertFalse( - hasattr( - context, - "runtime_save_session_requested", - ) - ) - self.assertEqual( - context.runtime_action_events[-1]["status"], - "failed", - ) - - - def test_save_session_guard_intents(self): - - self.assertTrue( - should_execute_save_session( - "save session" - ) - ) - self.assertFalse( - should_execute_save_session( - "show tag" - ) - ) - self.assertFalse( - should_execute_save_session( - "normal message" - ) - ) - self.assertTrue( - should_execute_save_session( - "давай зафиналим сессию, от души посидели" - ) - ) - self.assertTrue( - should_execute_save_session( - "да, сохраняй сессию" - ) - ) - self.assertTrue( - should_execute_save_session( - "финализируй сессию" - ) - ) - - def test_apply_runtime_action_calls_repairs_backslash_separated_content(self): Emitter = FakeEmitter @@ -2283,12 +2045,8 @@ def test_recorded_tool_results_include_individual_age_suffixes(self): ) record_runtime_tool_result( context, - TOOL_RESULT_KIND_SESSION, - { - "ok": True, - "action": "save_session", - "session_snapshot": "session saved", - }, + TOOL_RESULT_KIND_SEARCH, + "fresh search result", created_at=999.0, ) @@ -2305,7 +2063,7 @@ def test_recorded_tool_results_include_individual_age_suffixes(self): tool_results, ) self.assertIn( - '', + '', tool_results, ) diff --git a/tests/runtime_actions/test_todo.py b/tests/runtime_actions/test_todo.py index 786927bf..8c85b9d4 100644 --- a/tests/runtime_actions/test_todo.py +++ b/tests/runtime_actions/test_todo.py @@ -8,7 +8,6 @@ from unittest.mock import patch from clients.brain_client import apply_runtime_action_calls -from clients.brain_client import should_execute_save_session from contracts.rules_assembler import ( RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_JIN_COLOR, diff --git a/tests/test_anonymous_mode.py b/tests/test_anonymous_mode.py index a1a10445..8a95e15d 100644 --- a/tests/test_anonymous_mode.py +++ b/tests/test_anonymous_mode.py @@ -188,7 +188,7 @@ async def test_dispatcher_rejects_l4_write_before_execution_and_emits_failure(se self.assertEqual(emitter.events[0]["error"], RESTRICTED_WRITE_ERROR) self.assertEqual( emitter.events[0]["detail"], - "", + RESTRICTED_WRITE_REASON, ) self.assertEqual( context.runtime_action_events[0]["failure_reason"], diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index d495a719..2811029b 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -7,7 +7,11 @@ from agent.nodes.brain import replay_session_restore_resource_actions from rules.brain_context_builder import build_brain_context -from rules.runtime import SESSION_RESTORE_MESSAGE +from rules.runtime import ( + SESSION_RESTORE_MESSAGE, + SESSION_RESTORE_REASONING_CHAR_LIMIT, + SESSION_RESTORE_REASONING_COUNT, +) from runtime.client import RuntimeClient from runtime.runtime_context import RuntimeContext from utils.context.session_actions import build_session_actions_history_context @@ -589,7 +593,7 @@ def test_restore_reasoning_dump_is_newest_first_limited_and_middle_cropped(self) self.assertIsNotNone(payload) dump = payload["restore_reasoning_dump"] - self.assertEqual(dump.count("", 1)[0] latest_body = latest_block.split("\n", 1)[1].rstrip("\n") - self.assertLessEqual(len(latest_body), 5000) + self.assertLessEqual(len(latest_body), SESSION_RESTORE_REASONING_CHAR_LIMIT) self.assertEqual( payload["restore_l4_fact_ids"], ["F99", "F42"], @@ -644,8 +648,6 @@ def test_browser_session_bootstrap_is_enriched_from_raw_archive(self): "attached_file_ids": ["file1"], "runtime_memory": "archive runtime", "runtime_memory_updates": 9, - "session_memory": "archive session", - "session_memory_updates": 1, "loaded_memory_ids": ["abc123"], "active_memory_records": [], } @@ -659,8 +661,6 @@ def test_browser_session_bootstrap_is_enriched_from_raw_archive(self): { "type": "session_bootstrap", "source_session_id": "archive-session", - "session_memory": "browser L3 checkpoint", - "session_memory_updates": 1, "runtime_memory": "browser L1 checkpoint", "runtime_memory_updates": 9, "loaded_memory_ids": ["abc123"], @@ -674,7 +674,6 @@ def test_browser_session_bootstrap_is_enriched_from_raw_archive(self): self.assertIn("raw", context.runtime_session_restore_reasoning_dump) self.assertEqual(context.runtime_session_restore_l4_fact_ids, ["F7"]) self.assertIn("browser L1 checkpoint", context.runtime_memory) - self.assertEqual(context.session_memory, "browser L3 checkpoint") self.assertEqual( context.runtime_session_restore_pending_loaded_memory_ids, ["abc123"], @@ -1073,10 +1072,6 @@ def test_restore_prompt_is_clean_one_shot_context(self): context.runtime_loaded_delayed_memory_ids = ["abc123"] context.delayed_memory_reports = dict(context.runtime_loaded_delayed_memory) context.runtime_attached_file_ids = ["file1"] - context.session_memory = ( - "session_status: STALE CHECKPOINT\n" - "open_question: STALE OLD QUESTION" - ) with patch( "runtime.L4_memory.build_runtime_l4_memory_context", @@ -1100,6 +1095,7 @@ def test_restore_prompt_is_clean_one_shot_context(self): l4_builder.assert_called_once_with( context=context, fact_ids=["F42"], + user_input="", ) context.runtime_session_restore_priming = False @@ -1109,8 +1105,8 @@ def test_restore_prompt_is_clean_one_shot_context(self): ) self.assertNotIn("RAW REASONING", ordinary_prompt) self.assertIn("HEAVY DELAYED BODY", ordinary_prompt) - self.assertIn("", console_stream) assert console_stream < attached_files < console_end - assert "/static/js/dragdrop.js?v=persistent-files-3-max5-1" in source + assert "/static/js/dragdrop.js?v=console-auto-expand-2&file-restore-2&empty-files-plaque=1&unpin-logger-card=1" in source def test_dragdrop_uses_persistent_api_and_max_five_context_files(): @@ -80,11 +80,12 @@ def test_files_panel_shows_file_count_and_reuses_100px_image_hover_preview(): assert "bindPersistentFileHoverPreview" in runtime_view assert "window.bindJinAttachmentHoverPreview(element, record" in runtime_view assert "bindPersistentFileHoverPreview(pinButton, record);" in runtime_view - assert 'pinButton.title = String(record.id || "")' in runtime_view + assert "bindRuntimeMemoryHoverTitle(" in runtime_view + assert 'String(record.id || "")' in runtime_view assert "hoverPreviewMaxPx: 100" in runtime_view assert "window.bindJinAttachmentHoverPreview" in attachments - assert "/static/js/runtime/runtime-memory-view.js?v=files-panel-hover-3-delayed-attachments-1-avatar-l1-sync-1" in index - assert "/static/js/chat-attachments.js?v=attached-files-3" in index + assert "/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1&delayed-fact-paste=2&numeric-fact-order=1&l4-fact-age=1&hold-delete-1&modal-title-editor=1&active-citations=4&runtime-memory-title=2&lazy-rows=5&hover-title=1&row-payload=1&l4-citation-gate=1&delayed-pin-link-isolation=1&delayed-highlight-contract=1&active-memory-activity-order=1&l4-two-line-layout=1&l4-value-fill=1&l4-priority-bubble=1" in index + assert "/static/js/chat-attachments.js?v=attached-files-3-attachment-modal-id-1&hold-delete-1&runtime-action-preview-2" in index def test_delayed_memory_modal_links_existing_files_by_original_name(): diff --git a/tests/test_behavior_contract.py b/tests/test_behavior_contract.py index 47489026..8d0e9897 100644 --- a/tests/test_behavior_contract.py +++ b/tests/test_behavior_contract.py @@ -50,7 +50,7 @@ def test_behavior_contract_loads_split_contracts(self): contract["action_guards"], dict, ) - self.assertIn( + self.assertNotIn( "save_session", contract["action_guards"], ) @@ -243,24 +243,6 @@ def test_runtime_default_messages_are_formatted(self): ), ) - def test_save_session_guard_exists(self): - - guard = get_action_guard( - "save_session" - ) - - self.assertEqual( - guard["runtime_action"], - "SAVE_SESSION", - ) - self.assertEqual( - guard["private_marker"], - "", - ) - self.assertTrue( - guard["effects"]["emit_followup"], - ) - def test_save_delayed_memory_contract_has_close_tag(self): guard = get_action_guard( @@ -319,11 +301,11 @@ def test_save_active_memory_contract_is_autonomous(self): ) ) self.assertIn( - "active memory is an autonomous runtime action", + "Follow-up: false", instructions, ) self.assertIn( - "briefly notify the user in natural text", + '{"conditions":"","custom_field_name":""}', instructions, ) @@ -374,48 +356,48 @@ def test_runtime_action_instruction_blocks_are_separated(self): def test_configured_triggers_require_confirmation_and_allow_matching_text(self): - save_session_triggers = get_action_guard_triggers( - "save_session" + save_delayed_memory_triggers = get_action_guard_triggers( + "save_delayed_memory" ) - if not save_session_triggers: + if not save_delayed_memory_triggers: self.skipTest( - "save_session contract has no triggers configured" + "save_delayed_memory contract has no triggers configured" ) self.assertTrue( should_pause_action_guard_for_confirmation( - "save_session", + "save_delayed_memory", "normal message", ) ) self.assertTrue( should_execute_action_guard( - "save_session", - save_session_triggers[0], + "save_delayed_memory", + save_delayed_memory_triggers[0], ) ) def test_exact_trigger_match_requires_bare_contract_trigger(self): trigger = get_action_guard_triggers( - "save_session" + "save_delayed_memory" )[0] self.assertTrue( action_guard_has_exact_trigger_match( - "save_session", + "save_delayed_memory", f" {trigger} ", ) ) self.assertFalse( action_guard_has_exact_trigger_match( - "save_session", + "save_delayed_memory", f"{trigger}!", ) ) self.assertFalse( action_guard_has_exact_trigger_match( - "save_session", + "save_delayed_memory", f"пожалуйста, {trigger}", ) ) @@ -423,22 +405,22 @@ def test_exact_trigger_match_requires_bare_contract_trigger(self): def test_matching_blocker_skips_without_confirmation(self): blockers = get_action_guard_blockers( - "save_session" + "save_delayed_memory" ) if not blockers: self.skipTest( - "save_session contract has no blockers configured" + "save_delayed_memory contract has no blockers configured" ) self.assertFalse( should_pause_action_guard_for_confirmation( - "save_session", + "save_delayed_memory", blockers[0], ) ) self.assertFalse( should_execute_action_guard( - "save_session", + "save_delayed_memory", blockers[0], ) ) diff --git a/tests/test_bootstrap_latest_session_fix_client_contract.py b/tests/test_bootstrap_latest_session_fix_client_contract.py index df1f1a4f..1827d233 100644 --- a/tests/test_bootstrap_latest_session_fix_client_contract.py +++ b/tests/test_bootstrap_latest_session_fix_client_contract.py @@ -15,39 +15,24 @@ class BootstrapLatestSessionFixClientContractTests(unittest.TestCase): - def test_bootstrap_prefers_newest_per_session_runtime_over_global_pointer(self): + def test_bootstrap_selects_runtime_record_for_common_checkpoint_session(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") end = source.index("function clearPersistedSessionBootstrap()", start) block = source[start:end] - selection_start = block.index("let runtimeMemory =") - selection_end = block.index("if (!runtimeMemory)", selection_start) - selection = block[selection_start:selection_end] - - self.assertLess( - selection.index("browserLatestRuntimeMemory"), - selection.index("browserLatestSavedRuntimeMemory"), - ) - self.assertIn( - "compatibility fallback when no per-session predecessor exists", - block, - ) + self.assertIn("const checkpointSessionId =", block) + self.assertIn("const runtimeCandidates = [", block) + self.assertIn("browserLatestSavedRuntimeMemory", block) + self.assertIn("latestCompletedConversationRuntime", block) + self.assertIn("...(", block) + self.assertIn("browserLatestRuntimeMemory", block) + self.assertIn("runtimeCandidates.find(record => (", block) + self.assertIn("=== checkpointSessionId", block) + self.assertIn("if (!runtimeMemory && !conversationCheckpoint)", block) def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") - helper_start = source.index( - "function isCurrentRuntimeCheckpointFreshest(" - ) - helper_end = source.index( - "function prefersReducedSceneTintMotion()", - helper_start, - ) - helper = source[helper_start:helper_end] - - self.assertIn("collectOtherLatestRuntimeMemorySnapshots()", helper) - self.assertIn("return otherSavedAt > currentSavedAt", helper) - persist_start = source.index("function persistLiveSessionCheckpoint(data)") persist_end = source.index( "function clearPersistedToolResultsCheckpoint()", @@ -55,7 +40,8 @@ def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): ) persist = source[persist_start:persist_end] - guard = "if (!isCurrentRuntimeCheckpointFreshest(currentRuntime))" + self.assertIn("const sessionMoved = Boolean(", persist) + guard = "previousCheckpoint\n && !sameSession\n && !sessionMoved" self.assertIn(guard, persist) self.assertLess( persist.index(guard), @@ -65,6 +51,7 @@ def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): persist.index(guard), persist.index("writeLatestSavedSessionSnapshot({"), ) + self.assertIn("hasUnsavedSessionActivity = false", persist) if __name__ == "__main__": diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 8a68ece9..05e9f917 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -4,6 +4,7 @@ from pathlib import Path from types import SimpleNamespace from unittest.mock import patch +from xml.sax.saxutils import escape from agent.nodes.brain import ( BrainNode, @@ -78,70 +79,21 @@ def _assert_latest_request_payload( payload = call_kwargs["brain_payload"] system_prompt = call_kwargs["system_prompt"] - test_case.assertEqual( - payload, - "", - ) - test_case.assertTrue( - call_kwargs.get("followup_tick"), - call_kwargs, - ) - test_case.assertNotIn( - "", - system_prompt, - ) - test_case.assertLess( - system_prompt.index( - "" - ), - system_prompt.index( - "" - ), - ) - test_case.assertLess( - system_prompt.index( - expected_followup_message - ), - system_prompt.index( - "" - ), - ) - test_case.assertLess( - system_prompt.index( - "" - ), - system_prompt.index( - "", system_prompt) + test_case.assertIn("", system_prompt) + test_case.assertIn("" - ), - system_prompt.index( - "" - ), - ) - test_case.assertIn( - f"user_message: {user_input}", - system_prompt, - ) - test_case.assertIn( - f"INITIAL_SEQUENCE_INSTRUCTION: {user_input}", - system_prompt, - ) - test_case.assertNotIn( - "", - system_prompt, - ) - test_case.assertNotIn( - "MANDATORY: THIS IS NOT CURRENT COMMAND", - system_prompt, - ) - test_case.assertNotIn( - "", - system_prompt, + system_prompt.index(""), + system_prompt.index(""), ) + if latest_action_fragment: + test_case.assertIn(latest_action_fragment, system_prompt) + test_case.assertNotIn("", system_prompt) + test_case.assertNotIn("MANDATORY: THIS IS NOT CURRENT COMMAND", system_prompt) + test_case.assertNotIn("", system_prompt) class BrainAssetFlowTests(unittest.IsolatedAsyncioTestCase): @@ -278,21 +230,8 @@ async def test_followup_places_generic_instruction_without_followup_header(self) async def test_followup_places_confirm_result_inside_tool_results(self): messages = ( - ( - "User accepted an action and didn't provide any of action " - "trigger words: save session" - ), - ( - "Action failed. User rejected an action and didn't provide " - "any of trigger words: save session" - ), - ) - - followup_message = ( - "This is follow-up tick for JIN latest action: " - "save_session.\n" - "Requested and available information provided in tool " - "results section." + "User accepted a delayed-memory action.", + "Action failed. User rejected a delayed-memory action.", ) for message in messages: @@ -305,41 +244,27 @@ async def test_followup_places_confirm_result_inside_tool_results(self): prompt = BrainNode.build_followup_system_prompt( "\n", - "save the session", + "save delayed memory", context=context, - latest_action="save_session", + latest_action="save_delayed_memory", ) tools_start = prompt.index("") confirm_start = prompt.index("") confirm_end = prompt.index("") tools_end = prompt.index("") - followup_start = prompt.index(followup_message) + request_flow_end = prompt.index("") - self.assertLess( - tools_start, - confirm_start, - ) - self.assertLess( - confirm_start, - confirm_end, - ) - self.assertLess( - confirm_end, - tools_end, - ) - self.assertLess( - followup_start, - tools_start, - ) - self.assertEqual( - prompt.count(""), - 1, - ) - self.assertEqual( - context.runtime_action_failure_followup_messages, - [], + self.assertLess(request_flow_end, tools_start) + self.assertLess(tools_start, confirm_start) + self.assertLess(confirm_start, confirm_end) + self.assertLess(confirm_end, tools_end) + self.assertIn( + "save_delayed_memory", + prompt, ) + self.assertIn(message, prompt) + async def test_current_sequence_starts_with_original_user_message(self): @@ -368,33 +293,17 @@ async def test_current_sequence_starts_with_original_user_message(self): context=context, ) - self.assertIn( - "\n" - "user_message: keep <this> in delayed memory\n" - "", - prompt, - ) + self.assertIn("", prompt) + self.assertIn('', prompt) + self.assertIn("keep <this> in delayed memory", prompt) + self.assertIn("action_1: LIST_SKILLS ( 5s ago )", prompt) self.assertLess( - prompt.index(""), - prompt.index("\n" - "INITIAL_SEQUENCE_INSTRUCTION: keep <this> in delayed memory ( 10s ago )\n" - "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" - " --- Sequence started ---\n" - " JIN message 1 executed: LIST_SKILLS ( 5s ago )\n" - "", - prompt, - ) - self.assertNotIn( - "SEQUENCE_ORIGIN_REQUEST", - prompt, - ) - self.assertNotIn( - "MANDATORY: THIS IS NOT CURRENT COMMAND", - prompt, + prompt.index(""), + prompt.index(""), ) + self.assertNotIn("SEQUENCE_ORIGIN_REQUEST", prompt) + self.assertNotIn("MANDATORY: THIS IS NOT CURRENT COMMAND", prompt) + async def test_followup_collects_scattered_tool_results_below_current_sequence(self): @@ -420,7 +329,7 @@ async def test_followup_collects_scattered_tool_results_below_current_sequence(s prompt, ) self.assertLess( - prompt.index(""), + prompt.index(""), prompt.index(""), ) self.assertEqual( @@ -801,13 +710,11 @@ async def test_loaded_delayed_memory_is_under_latest_request(self): prompt, ) self.assertLess( - prompt.index(""), + prompt.index(""), prompt.index(""), ) - self.assertIn( - "INITIAL_SEQUENCE_INSTRUCTION: append the delayed memory", - prompt, - ) + self.assertIn("" + "" ), prompt.index( loaded_delayed_memory @@ -931,17 +838,10 @@ async def test_followup_places_current_sequence_under_latest_request(self): }, ], runtime_recent_turns=[], - runtime_loaded_delayed_memory={}, - ) - base_prompt = ( - "\nstate\n\n\n" - "\n" - " 1. SAVE_ACTIVE_MEMORY\n" - " 2. LIST_SKILLS\n" - " 3. LOAD_SKILL\n" - "\n\n" - "RULES" + runtime_memory="state", + runtime_memory_stable="state", ) + base_prompt = "\n 1. SAVE_ACTIVE_MEMORY\n 2. LIST_SKILLS\n 3. LOAD_SKILL\n\n\nRULES" with patch( "utils.context.context_exports.time.time", @@ -953,36 +853,16 @@ async def test_followup_places_current_sequence_under_latest_request(self): context=context, ) - self.assertIn( - "turn_000002", - context.runtime_action_sequence_turn_ids, - ) - self.assertIn( - "\n" - "INITIAL_SEQUENCE_INSTRUCTION: first list skills, then append one ( 1m ago )\n" - "DO NOT FOLLOW INITIAL_SEQUENCE_INSTRUCTION EXPLICITLY, CHECK CURRENT_SEQUENCE HISTORY BELOW!\n" - " --- Sequence started ---\n" - " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed: LOAD_SKILL ( 2s ago )\n" - "", - prompt, - ) - self.assertNotIn( - "", - prompt, - ) - self.assertNotIn( - "Sequence ended", - prompt, - ) + self.assertIn("turn_000002", context.runtime_action_sequence_turn_ids) + self.assertIn("", prompt) + self.assertIn("first list skills, then append one", prompt) + self.assertIn("action_1: LIST_SKILLS ( 55s ago )", prompt) + self.assertIn("action_2: LOAD_SKILL ( 2s ago )", prompt) self.assertLess( - prompt.index(""), + prompt.index(""), prompt.index(""), ) - self.assertNotIn( - "", - prompt, - ) + self.assertNotIn("", prompt) async def test_list_skills_followup_text_is_emitted_when_no_asset_action_follows(self): @@ -1168,11 +1048,9 @@ async def fake_run_brain_stream(**kwargs): kwargs["brain_payload"], "", ) - self.assertIn( - "INITIAL_SEQUENCE_INSTRUCTION: что на скриншоте?\n\n" - "Attached context:", - kwargs["system_prompt"], - ) + self.assertIn("" ), kwargs["system_prompt"].index( - "" + "" ), ) self.assertLess( kwargs["system_prompt"].index( - "" + "" ), kwargs["system_prompt"].index( "" diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 11ee0166..bff65403 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -58,7 +58,7 @@ def test_first_brain_prompt_includes_default_runtime_memory(self): prompt, ) self.assertIn( - DEFAULT_RUNTIME_MEMORY, + DEFAULT_RUNTIME_MEMORY.strip(), prompt, ) @@ -120,7 +120,7 @@ def test_brain_prompt_places_user_idle_in_runtime_memory(self): prompt, ) self.assertIn( - f"note: {DEFAULT_RUNTIME_MEMORY}", + f"note: {DEFAULT_RUNTIME_MEMORY.strip()}", prompt, ) self.assertIn( @@ -146,7 +146,7 @@ def test_brain_prompt_places_user_idle_in_runtime_memory(self): 0, ) self.assertIn( - f"note: {DEFAULT_RUNTIME_MEMORY}", + DEFAULT_RUNTIME_MEMORY.strip(), snapshot["raw_memory"], ) self.assertIn( @@ -729,7 +729,7 @@ def test_current_action_age_starts_at_one_second(self): ) self.assertIn( - "JIN message 1 executed: ASSET_ACTION ( 1s ago )", + "action_1: ASSET_ACTION ( 1s ago )", history, ) self.assertNotIn( @@ -776,10 +776,10 @@ def test_action_context_hides_single_emission_counts(self): "runtime_turn_id": "turn_000002", }, { - "text": "SAVE_SESSION", + "text": "CLEAN_TOOL_RESULTS", "parts": [ { - "text": "SAVE_SESSION", + "text": "CLEAN_TOOL_RESULTS", "count": 1, }, ], @@ -803,18 +803,18 @@ def test_action_context_hides_single_emission_counts(self): history, ) self.assertIn( - "JIN message 1 executed: LIST_SKILLS ( 5s ago )", + "action_1: LIST_SKILLS ( 5s ago )", history, ) self.assertIn( ( - "JIN message 2 executed: LOAD_SKILL: file_manager (count: 3), " + "action_2: LOAD_SKILL: file_manager (count: 3), " "CLEAN_TOOL_RESULTS ( 2s ago )" ), history, ) self.assertIn( - "JIN message 3 executed: SAVE_SESSION ( 1s ago )", + "action_3: CLEAN_TOOL_RESULTS ( 1s ago )", history, ) @@ -866,14 +866,14 @@ def test_current_sequence_includes_jin_content_for_marker_message_only(self): self.assertIn( ( - "JIN message 1 content: " + "assistant_output_1: " f"{jin_content} ( 1s ago )" ), current_sequence, ) self.assertIn( ( - "JIN message 1 executed: WEB_SEARCH: " + "action_1: WEB_SEARCH: " "Ouroboros AI project framework competitor LLM agents " "( 1s ago )" ), @@ -887,7 +887,7 @@ def test_current_sequence_includes_jin_content_for_marker_message_only(self): session_history, ) self.assertNotIn( - "JIN message 1 content", + "assistant_output_1", session_history, ) self.assertNotIn( @@ -936,15 +936,17 @@ def test_current_actions_history_filters_older_session_actions(self): current_sequence=True, ) - self.assertEqual( + self.assertIn( + "", + history, + ) + self.assertIn( + "action_1: LIST_SKILLS ( 55s ago )", + history, + ) + self.assertIn( + "action_2: LOAD_SKILL ( 2s ago )", history, - ( - "\n" - " --- Sequence started ---\n" - " JIN message 1 executed: LIST_SKILLS ( 55s ago )\n" - " JIN message 2 executed: LOAD_SKILL ( 2s ago )\n" - "" - ), ) self.assertNotIn( "SAVE_ACTIVE_MEMORY", @@ -999,7 +1001,7 @@ def test_current_sequence_expands_memory_action_payloads_without_counts(self): self.assertIn( ( - "JIN message 1 executed: RESOLVE_ACTIVE_MEMORY: " + "action_1: RESOLVE_ACTIVE_MEMORY: " "id: enrrqo; content: word: кукушка, " "RESOLVE_ACTIVE_MEMORY: id: yfpywn; " "content: word: кулёк ( 2s ago )" @@ -1275,96 +1277,6 @@ def test_brain_prompt_keeps_user_feedback_out_of_runtime_state(self): runtime_memory, ) - def test_brain_prompt_places_runtime_memory_above_session_memory(self): - - context = SimpleNamespace( - session_memory=( - "session_snapshot_first_turn: 0\n" - "session_snapshot_last_turn: 6\n" - "decision: Continue the memory architecture work." - ), - runtime_memory=( - "topic: live runtime state" - ), - deep_thought_count=0, - runtime_search_result="", - runtime_search_result_id="", - ) - - prompt = build_brain_context( - context=context, - runtime_actions={ - "CAN_WEB_SEARCH": False, - }, - ) - - self.assertIn( - "", - prompt, - ) - self.assertIn( - "Continue the memory architecture work", - prompt, - ) - self.assertIn( - "session_snapshot_first_turn", - prompt, - ) - self.assertIn( - "session_snapshot_last_turn", - prompt, - ) - self.assertLess( - prompt.index( - "", - prompt, - ) - self.assertIn( - "current factual work", - prompt, - ) - self.assertIn( - "compares implementation paths", - prompt, - ) - self.assertNotIn( - "image/action tool", - prompt, - ) - self.assertNotIn( - "Choose the best available visual representation of the request instead of description", - prompt, - ) - def test_brain_prompt_includes_conditional_zero_diff_alert(self): context = SimpleNamespace( diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index ed16f760..bbf39d21 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -76,9 +76,6 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: if bool(runtime_actions.get("CAN_WEB_SEARCH", False)): expected_actions.append("WEB_SEARCH") - if bool(runtime_actions.get("CAN_SAVE_SESSION", False)): - expected_actions.append("SAVE_SESSION") - if bool(runtime_actions.get("CAN_CLEAN_TOOL_RESULTS", False)): expected_actions.append( "CLEAN_TOOL_RESULTS" @@ -154,6 +151,16 @@ def expected_enabled_runtime_actions(runtime_actions: dict) -> tuple[str, ...]: class BrainRuntimeActionTests(unittest.TestCase): + def setUp(self): + self._search_actions_patcher = patch( + "rules.brain_context_builder.search_actions_available", + return_value=True, + ) + self._search_actions_patcher.start() + self.addCleanup( + self._search_actions_patcher.stop + ) + def test_stream_save_active_memory_bubble_starts_on_open_tag_and_reuses_id(self): class FakeEmitter: @@ -600,198 +607,23 @@ def test_brain_system_prompt_keeps_runtime_rule_sentences_separated(self): context=context, runtime_actions={ "CAN_WEB_SEARCH": True, - "CAN_SAVE_SESSION": True, "CAN_SAVE_DELAYED_MEMORY": True, "CAN_SAVE_ACTIVE_MEMORY": True, }, ) - assert_not_contains_text( - self, - prompt, + for broken_join in ( "final answer.Emit markers", - ) - assert_not_contains_text( - self, - prompt, "specific cases.DO NOT invent", - ) - assert_not_contains_text( - self, - prompt, "memory conditions.You need", - ) - assert_contains_text( - self, - prompt, - "RUNTIME ACTION EXECUTION RULES:", - ) - assert_contains_text( - self, - prompt, - "Use follow-up system ticks in sequence for multi-step tasks.\n" - "In case of conflict", - ) - assert_contains_text( - self, - prompt, - "When no actions needed or sequence is done stop instantly and notify user naturally.\n\n" - "MEMORY AND SESSION PROPOSALS:", - ) - assert_contains_text( - self, - prompt, - "Active memory does not require explicit confirmation.", - ) - assert_contains_text( - self, - prompt, - "never a gate in front of an explicit user command", - ) - assert_contains_text( - self, - prompt, - "A direct user request to save/finalize the session", - ) - assert_contains_text( - self, - prompt, - "Do not ask for confirmation again", - ) - assert_contains_text( - self, - prompt, - "Follow CURRENT_SEQUENCE intent rather than forcing the user to repeat a magic phrase.", - ) - assert_not_contains_text( - self, - prompt, - ( - "Never emit a save or memory marker during proposal until " - "the user clearly accepts it." - ), - ) - assert_not_contains_text( - self, - prompt, - "Propose active memory when", - ) - assert_not_contains_text( - self, - prompt, - ( - "Never emit a save or update marker until the user " - "explicitly accepts the proposal." - ), - ) - - def test_non_stream_blocks_save_session_meta_request_in_reasoning(self): - - class FakeBrainClient: - async def ask(self, **_kwargs): - return { - "model": config.BRAIN_MODEL_UID, - "choices": [ - { - "message": { - "reasoning": ( - "The user asked for internal syntax.\n" - "" - ), - "content": "ok", - }, - }, - ], - } - - class Context: - pass - - context = Context() - original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN - config.USE_SERVICE_AS_BRAIN = False - - try: - answer = asyncio.run( - ask_brain( - client=FakeBrainClient(), - text=( - "\u043d\u0430\u043f\u0438\u0448\u0438 " - "\u043f\u043e\u043b\u043d\u044b\u0439 " - "\u0442\u0435\u0433 " - "\u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f " - "\u0441\u0435\u0441\u0441\u0438\u0438" - ), - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ) - ) - finally: - config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain - - self.assertEqual( - answer, - "ok", - ) - self.assertFalse( - hasattr( - context, - "runtime_save_session_requested", - ) - ) - - def test_non_stream_preserves_save_session_marker_without_trigger(self): - - class FakeBrainClient: - async def ask(self, **_kwargs): - return { - "model": config.BRAIN_MODEL_UID, - "choices": [ - { - "message": { - "reasoning": "", - "content": ( - "The literal marker is " - "." - ), - }, - }, - ], - } - - class Context: - pass - - context = Context() - original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN - config.USE_SERVICE_AS_BRAIN = False - - try: - answer = asyncio.run( - ask_brain( - client=FakeBrainClient(), - text="what marker saves the session?", - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ) - ) - finally: - config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain + ): + assert_not_contains_text(self, prompt, broken_join) - self.assertEqual( - answer, - "The literal marker is .", - ) - self.assertFalse( - hasattr( - context, - "runtime_save_session_requested", - ) - ) + assert_contains_text(self, prompt, "") + assert_contains_text(self, prompt, "") + assert_contains_text(self, prompt, "") + assert_contains_text(self, prompt, "RESOLVE_ACTIVE_MEMORY:") + assert_contains_text(self, prompt, "Follow-up: false") def test_non_stream_preserves_delayed_memory_marker_without_trigger(self): @@ -848,157 +680,6 @@ class Context: ) ) - def test_non_stream_ignores_save_session_marker_in_reasoning(self): - - class FakeBrainClient: - async def ask(self, **_kwargs): - return { - "model": config.BRAIN_MODEL_UID, - "choices": [ - { - "message": { - "reasoning": ( - "The user asked to save.\n" - "" - ), - "content": "ok", - }, - }, - ], - } - - class Context: - pass - - context = Context() - original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN - config.USE_SERVICE_AS_BRAIN = False - - try: - answer = asyncio.run( - ask_brain( - client=FakeBrainClient(), - text="save session", - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ) - ) - finally: - config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain - - self.assertEqual( - answer, - "ok", - ) - self.assertFalse( - hasattr( - context, - "runtime_save_session_requested", - ) - ) - - def test_stream_ignores_save_session_marker_in_thinking(self): - - class FakeBrainClient: - async def stream(self, **_kwargs): - yield { - "type": "thinking", - "content": ( - "The user asked to save.\n" - "" - ), - } - yield { - "type": "thinking", - "content": ( - "Again\n" - "" - ), - } - yield { - "type": "content", - "content": "ok", - } - - class Context: - pass - - async def collect(context): - chunks = [] - - async for chunk in ask_brain_stream( - client=FakeBrainClient(), - text="save session", - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ): - chunks.append( - chunk - ) - - return chunks - - context = Context() - original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN - config.USE_SERVICE_AS_BRAIN = False - - try: - chunks = asyncio.run( - collect( - context - ) - ) - finally: - config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain - - self.assertIn( - { - "type": "content", - "content": "ok", - }, - chunks, - ) - self.assertEqual( - chunks[-1], - { - "type": "raw_model_output", - "content": "ok", - }, - ) - self.assertFalse( - hasattr( - context, - "runtime_save_session_requested", - ) - ) - self.assertEqual( - [ - chunk - for chunk in chunks - if chunk["type"] == "thinking" - ], - [ - { - "type": "thinking", - "content": ( - "The user asked to save.\n" - "" - ), - }, - { - "type": "thinking", - "content": ( - "Again\n" - "" - ), - }, - ], - ) - def test_stream_preserves_explicit_empty_brain_payload(self): class FakeBrainClient: @@ -1061,69 +742,6 @@ async def collect(client, context): }, ) - def test_stream_applies_save_session_marker_from_content_tail(self): - - class FakeBrainClient: - async def stream(self, **_kwargs): - yield { - "type": "content", - "content": "", - } - - class Context: - pass - - async def collect(context): - chunks = [] - - async for chunk in ask_brain_stream( - client=FakeBrainClient(), - text="save session", - context=context, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ): - chunks.append( - chunk - ) - - return chunks - - context = Context() - original_use_service_as_brain = config.USE_SERVICE_AS_BRAIN - config.USE_SERVICE_AS_BRAIN = False - - try: - chunks = asyncio.run( - collect( - context - ) - ) - finally: - config.USE_SERVICE_AS_BRAIN = original_use_service_as_brain - - self.assertEqual( - chunks, - [ - { - "type": "raw_model_output", - "content": "", - }, - ], - ) - self.assertTrue( - context.runtime_save_session_requested, - ) - self.assertEqual( - context.runtime_action_events, - [ - { - "name": "save_session", - }, - ], - ) - def test_legacy_stream_runtime_actions_include_message_scope(self): class FakeEmitter: @@ -1536,7 +1154,7 @@ async def stream(self, **_kwargs): yield { "type": "content", "content": ( - "\n" + "\n" " #112233 " ), } @@ -1549,10 +1167,10 @@ async def collect(context): chunk async for chunk in ask_brain_stream( client=FakeBrainClient(), - text="save session and set color", + text="clean tool results and set color", context=context, runtime_actions={ - "CAN_SAVE_SESSION": True, + "CAN_CLEAN_TOOL_RESULTS": True, "CAN_JIN_COLOR": True, }, ) @@ -1569,18 +1187,18 @@ async def collect(context): self.assertEqual( [item["text"] for item in context.runtime_session_action_history], - ["SAVE_SESSION, JIN_COLOR"], + ["CLEAN_TOOL_RESULTS, JIN_COLOR"], ) prompt = build_brain_context( context=context, runtime_actions={ - "CAN_SAVE_SESSION": True, + "CAN_CLEAN_TOOL_RESULTS": True, "CAN_JIN_COLOR": True, }, ) self.assertIn("", prompt) - self.assertIn("1. SAVE_SESSION, JIN_COLOR", prompt) + self.assertIn("1. CLEAN_TOOL_RESULTS, JIN_COLOR", prompt) def test_stream_history_preserves_duplicate_markers_after_action_dedup(self): @@ -1589,8 +1207,8 @@ async def stream(self, **_kwargs): yield { "type": "content", "content": ( - "\n" - "" + "\n" + "" ), } @@ -1602,10 +1220,10 @@ async def collect(context): async for chunk in ask_brain_stream( client=FakeBrainClient(), - text="save session", + text="clean tool results", context=context, runtime_actions={ - "CAN_SAVE_SESSION": True, + "CAN_CLEAN_TOOL_RESULTS": True, }, ): chunks.append( @@ -1631,7 +1249,7 @@ async def collect(context): context.runtime_action_events, [ { - "name": "save_session", + "name": "clean_tool_results", }, ], ) @@ -1641,7 +1259,7 @@ async def collect(context): for item in context.runtime_session_action_history ], [ - "SAVE_SESSION (count: 2)", + "CLEAN_TOOL_RESULTS (count: 2)", ], ) @@ -1781,6 +1399,7 @@ def test_jin_color_history_preserves_ordered_color_swatches(self): "#00ff00", "#ff0000", ], + "context_detail": "#ff0000, #00ff00", "count": 3, }, ], @@ -1799,6 +1418,7 @@ def test_jin_color_history_preserves_ordered_color_swatches(self): "#00ff00", "#ff0000", ], + "context_detail": "#ff0000, #00ff00", "count": 3, }, ], @@ -1830,6 +1450,7 @@ def test_jin_color_history_separates_marker_count_from_applied_colors(self): "colors": [ "#ff0000", ], + "context_detail": "#ff0000", "count": 4, }], ) @@ -2057,7 +1678,7 @@ def test_session_history_includes_saved_content_title(self): ) self.assertIn( ( - "JIN message 1 executed: " + "action_1: " f"{expected_text.replace(' - ', ': ', 1)}" ), build_session_actions_history_context( @@ -2340,6 +1961,7 @@ async def collect(context): "colors": [ "#ff0000", ], + "context_detail": "#ff0000", "count": 4, }], ) @@ -2443,6 +2065,7 @@ async def collect(context): "colors": [ "#ff0000", ], + "context_detail": "#ff0000", "count": 5, }], ) @@ -3739,14 +3362,6 @@ def test_prompt_lists_available_delayed_memory_below_tool_results(self): "" ), ) - self.assertLess( - prompt.index( - expected_inventory - ), - prompt.index( - "RUNTIME ACTION EXECUTION RULES:" - ), - ) self.assertFalse( prompt.rstrip().endswith( expected_inventory @@ -3906,8 +3521,7 @@ def test_prompt_adds_resolve_active_memory_rules_from_active_records_only(self): assert_contains_text( self, prompt, - '{"conditions":"CONDITIONS",' - '"custom_field_name":"VALUE"}', + '{"conditions":"","custom_field_name":""}', ) assert_contains_text( self, @@ -4123,7 +3737,7 @@ def test_prompt_omits_resolve_active_memory_rules_without_active_records(self): assert_contains_text( self, prompt, - " CONDITIONS ", + '{"conditions":"","custom_field_name":""}', ) assert_not_contains_text( self, @@ -4139,103 +3753,19 @@ def test_prompt_uses_passed_agent_runtime_actions(self): } ) - self.assertNotIn( - "CAN_WEB_SEARCH", - prompt, - ) - - - - self.assertNotIn( - '{"query":"..."}' , - prompt, - ) - - assert_contains_text( - self, - prompt, - "Use WEB_SEARCH when freshness", - ) - - self.assertNotIn( - "", - prompt, - ) - - self.assertNotIn( - "", - prompt, - ) - - self.assertIn( - ( - "SERVICE as BRAIN" - if settings.USE_SERVICE_AS_BRAIN - else "BRAIN" - ), - prompt, - ) - + self.assertNotIn("CAN_WEB_SEARCH", prompt) + assert_contains_text(self, prompt, "") + assert_contains_text(self, prompt, "Use WEB_SEARCH when freshness") + self.assertNotIn("", prompt) self.assertIn( f"{config.SERVICE_MODEL_UID}", prompt, ) - - if settings.USE_SERVICE_AS_BRAIN: - self.assertNotIn( - "", - prompt, - ) - else: - self.assertIn( - f"{config.BRAIN_MODEL_UID}", - prompt, - ) - - self.assertNotIn( - "", - prompt, - ) - - self.assertNotIn( - "", - prompt, - ) - - self.assertNotIn( - "CURRENT_DATE", - prompt, - ) - - self.assertNotIn( - "CURRENT_TIME", - prompt, - ) - - self.assertNotIn( - "", - prompt, - ) - - self.assertNotIn( - "RUNTIME_STATE", - prompt, - ) - - self.assertNotIn( - "INITIAL_STATE", - prompt, - ) + self.assertNotIn("", prompt) + self.assertNotIn("", prompt) + self.assertNotIn("", prompt) def test_prompt_can_flip_agent_actions_dynamically(self): @@ -4287,59 +3817,6 @@ def test_search_prompt_requires_plain_query_and_exact_subject(self): ) - def test_prompt_includes_save_session_only_when_enabled(self): - - prompt = build_brain_context( - runtime_actions={ - "CAN_WEB_SEARCH": False, - "CAN_SAVE_SESSION": True, - "CAN_SAVE_ACTIVE_MEMORY": True, - } - ) - - self.assertNotIn( - '' , - prompt, - ) - self.assertNotIn( - '' , - prompt, - ) - self.assertNotIn( - "enabled=\"true\"", - prompt, - ) - self.assertIn( - "explicitly ends", - prompt, - ) - assert_contains_text( - self, - prompt, - "SAVE_SESSION:", - ) - self.assertNotIn( - " CONDITIONS ", - ) - self.assertIn( - "SAVE_ACTIVE_MEMORY", - prompt, - ) - def test_prompt_does_not_render_legacy_memory_recall_block(self): context = SimpleNamespace( diff --git a/tests/test_chat_stream_avatar_client_contract.py b/tests/test_chat_stream_avatar_client_contract.py index 09bc066b..b1048fa9 100644 --- a/tests/test_chat_stream_avatar_client_contract.py +++ b/tests/test_chat_stream_avatar_client_contract.py @@ -72,7 +72,7 @@ def test_stream_avatar_assets_are_cache_busted(self): self.assertIn("/static/css/base.css?v=stream-avatar-1", source) self.assertIn("/static/css/chat.css?v=stream-avatar-1", source) self.assertIn("/static/js/chat.js?v=stream-avatar-2", source) - self.assertIn("/static/js/socket.js?v=stream-avatar-1", source) + self.assertIn("/static/js/socket.js?v=avatar-geometry-1&stream-avatar-1-archived-session-restore-3&anonymous-mode=1&input-focus=2&bubble-utility-retry=1", source) self.assertIn( "/static/js/socket/event-handlers.js?v=stream-avatar-1", source, diff --git a/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py index 37b9a275..ca280879 100644 --- a/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py +++ b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py @@ -24,7 +24,7 @@ def test_clean_only_mutates_tool_results_in_existing_browser_checkpoint(self): self.assertIn("...previousCheckpoint", block) self.assertIn("...previousSessionSnapshot", block) self.assertIn("tool_results: []", block) - self.assertNotIn("new Date().toISOString()", block) + self.assertIn("tool_results_cleared_at: new Date().toISOString()", block) self.assertNotIn("saved_at:", block) self.assertNotIn("writeLatestSavedRuntimeMemory", block) diff --git a/tests/test_current_context_window.py b/tests/test_current_context_window.py index aae2768d..9d125db1 100644 --- a/tests/test_current_context_window.py +++ b/tests/test_current_context_window.py @@ -74,7 +74,7 @@ async def test_prepare_inserts_detected_context_window_under_brain_model(self): ) self.assertLess( prepared.system_prompt.index( - "" + "" ), prepared.system_prompt.index( "" diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 993bc76c..671620e9 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -111,7 +111,7 @@ def test_delayed_memory_modal_shortens_session_ids_with_full_hover_text(self): source, ) self.assertIn( - "item.title =\n sessionId;", + "bindRuntimeMemoryHoverTitle(\n item,\n sessionId\n );", source, ) @@ -179,12 +179,12 @@ def test_delayed_memory_delete_sync_and_loaded_state_are_registered(self): runtime_source, ) self.assertIn( - "isDelayedMemoryReportLoaded(reportId, report)", - l4_source, + "function isDelayedMemoryReportLoaded(reportId)", + runtime_source, ) self.assertIn( - "loadedIds.forEach((factId) => {", - l4_source, + "loadedDelayedMemoryReportIds.has(normalizedId)", + runtime_source, ) self.assertIn( "deleted_delayed_memory_report_ids: deletedReportIds", @@ -307,13 +307,13 @@ def test_pinning_delayed_memory_does_not_create_secondary_report_glow(self): secondary_block = memory_view_source[secondary_start:secondary_end] self.assertIn( - ".filter(isDelayedMemoryReportExplicitlyLoaded)", - secondary_block, - ) - self.assertNotIn( ".filter(isDelayedMemoryReportInContext)", secondary_block, ) + self.assertIn( + "if (Boolean(report.pinned))", + memory_view_source, + ) self.assertIn( "delayed-pin-link-isolation=1", index_source, @@ -346,7 +346,7 @@ def test_delayed_memory_unpin_resyncs_all_avatar_link_state(self): pin_block, ) self.assertIn( - '/static/js/runtime/runtime.js?v=delayed-unpin-avatar-sync-1', + 'delayed-unpin-avatar-sync-1', index_source, ) @@ -410,7 +410,7 @@ def test_loaded_delayed_memory_bubbles_open_their_own_reports(self): runtime_actions_source, ) self.assertIn( - "counterOnly\n && reportScopedDelayedAction", + "counterOnly\n && (\n reportScopedDelayedAction", runtime_actions_source, ) self.assertIn( @@ -554,7 +554,7 @@ def test_socket_runtime_actions_keep_message_scope_for_all_actions(self): ) self.assertIn( - "runtimeMessageId:\n getRuntimeActionMessageId(data)", + "const runtimeMessageId =\n getRuntimeActionMessageId(data);", source, ) self.assertNotIn( @@ -811,7 +811,7 @@ def test_delayed_memory_fact_ids_support_filtered_batch_paste(self): index_source, ) self.assertIn( - "/static/js/runtime/runtime.js?v=delayed-unpin-avatar-sync-1&delayed-fact-paste=2", + "delayed-unpin-avatar-sync-1&delayed-fact-paste=2", index_source, ) self.assertIn( @@ -950,11 +950,11 @@ def test_session_snapshot_history_and_loaded_ids_are_persisted(self): storage_source, ) self.assertIn( - "archiveLatestSavedSessionMemory", + "retiredSavedSessionHistoryStorageKey", storage_source, ) self.assertIn( - "readSavedSessionMemoryHistory", + "removeBrowserMemory(\n retiredSavedSessionHistoryStorageKey", storage_source, ) self.assertIn( diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index 7750e468..fb0ce6a7 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,11 +47,11 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=l4-report-link-1", source) - self.assertIn("/static/js/runtime/runtime.js?v=delayed-load-highlight-1", source) - self.assertIn("/static/js/chat-attachments.js?v=attached-files-1", source) - self.assertIn("/static/js/logger/trace-modal.js?v=attached-files-1", source) - self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) + self.assertIn("/static/css/runtime-memory.css?v=delayed-collapsible-cards-2", source) + self.assertIn("/static/js/runtime/runtime.js?v=facts-memory-server-sync-1", source) + self.assertIn("/static/js/chat-attachments.js?v=attached-files-3-attachment-modal-id-1", source) + self.assertIn("/static/js/logger/trace-modal.js?v=context-session-actions-1", source) + self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-13", source) if __name__ == "__main__": diff --git a/tests/test_facts_memory_client_contract.py b/tests/test_facts_memory_client_contract.py index 45f05fb6..a2cd7cf4 100644 --- a/tests/test_facts_memory_client_contract.py +++ b/tests/test_facts_memory_client_contract.py @@ -73,11 +73,11 @@ def test_facts_memory_can_be_rekeyed_only_when_current_snapshot_is_empty(self): source, ) self.assertIn( - "writeBrowserMemory(\n targetStorageKey,\n signals", + "writeFactsStorageMemory(\n targetStorageKey,\n signals", source, ) self.assertIn( - "removeBrowserMemory(\n storageKey", + "removeFactsStorageMemory(\n storageKey", source, ) @@ -112,7 +112,7 @@ def test_facts_memory_logger_exposes_dynamic_append_action(self): source, ) - def test_explicit_session_save_uses_active_facts_memory_session_id(self): + def test_bootstrap_activates_source_facts_memory_without_forking_runtime_session(self): source = ( ROOT / "ui" @@ -123,7 +123,7 @@ def test_explicit_session_save_uses_active_facts_memory_session_id(self): ).read_text(encoding="utf-8") self.assertIn( - "getCurrentFactsMemorySessionId,", + "activateFactsMemorySession,", source, ) self.assertIn( @@ -131,15 +131,11 @@ def test_explicit_session_save_uses_active_facts_memory_session_id(self): source, ) self.assertIn( - "getCurrentFactsMemorySessionId()", + "getCurrentRuntimeSessionId()", source, ) - self.assertGreaterEqual( - source.count("getCurrentSavedSessionId(),"), - 3, - ) self.assertIn( - "function buildSessionSaveRuntimeSnapshot(snapshot)", + "activateFactsMemorySession(\n bootstrap.source_session_id", source, ) self.assertIn( @@ -148,5 +144,6 @@ def test_explicit_session_save_uses_active_facts_memory_session_id(self): ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_jin_motion_client_contract.py b/tests/test_jin_motion_client_contract.py index c2a35e48..6a6eaaaf 100644 --- a/tests/test_jin_motion_client_contract.py +++ b/tests/test_jin_motion_client_contract.py @@ -16,7 +16,7 @@ def test_avatar_snapshot_reports_live_geometry_and_window(self): self.assertIn("window_width: Math.max(1, Math.round(window.innerWidth))", source) self.assertIn("window_height: Math.max(1, Math.round(window.innerHeight))", source) self.assertIn("rect.left + (rect.width / 2)", source) - self.assertIn("rect.top + (rect.height / 2)", source) + self.assertIn("rect.top\n + (rect.height / 2)\n - headerShift", source) def test_position_coordinates_target_avatar_center(self): source = LOGGER_JS.read_text(encoding="utf-8") diff --git a/tests/test_l1_memory.py b/tests/test_l1_memory.py index 20ed136b..828b98dc 100644 --- a/tests/test_l1_memory.py +++ b/tests/test_l1_memory.py @@ -149,7 +149,7 @@ def test_runtime_memory_user_prompt_omits_empty_session_fallback(self): prompt, ) - def test_runtime_memory_user_prompt_omits_default_note_line(self): + def test_runtime_memory_user_prompt_keeps_default_note_line(self): prompt = build_runtime_memory_user_prompt( current_memory=( @@ -159,11 +159,11 @@ def test_runtime_memory_user_prompt_omits_default_note_line(self): assistant_message="hi", ) - self.assertNotIn( - DEFAULT_RUNTIME_MEMORY, + self.assertIn( + f"note: {DEFAULT_RUNTIME_MEMORY.strip()}", prompt, ) - self.assertNotIn( + self.assertIn( "Current runtime memory:", prompt, ) diff --git a/tests/test_l2_memory.py b/tests/test_l2_memory.py deleted file mode 100644 index c7c1aaf6..00000000 --- a/tests/test_l2_memory.py +++ /dev/null @@ -1,880 +0,0 @@ -import unittest -from types import ( - SimpleNamespace, -) -from runtime.L2_memory_rules import ( - BEHAVIOR_VS_INTENT, - CONFIRMABLE_KEYS, - EVIDENCE_LINE_LIFECYCLE, - OCCURRENCE_COUNTING, - OUTPUT_FORMAT, - PATTERN_EVIDENCE_LINES, - PATTERN_FAMILY_DEDUPLICATION, - ROLE, - RUNTIME_L2_MEMORY_SYSTEM_PROMPT, - SELF_LEARNING_GUARD, - SPAN_METADATA, -) -from runtime.L1_memory_utils import ( - build_runtime_memory_context_text, -) -from runtime.L2_memory_utils import ( - build_runtime_l2_memory_system_prompt, - extract_runtime_l2_pattern_evidence_lines, - merge_runtime_l2_pattern_evidence_memory, - normalize_l2_pattern_evidence_example, - remove_runtime_l2_pattern_evidence_lines, -) -from runtime.L2_memory import ( - maybe_summarize_runtime_l2_memory, - record_runtime_l1_diff, -) -from runtime.memory_common import ( - build_runtime_summarizer_response_details, - extract_runtime_memory_text, -) -from config_loader import ( - config, -) -from tests.helpers.memory import ( - FakeLogger, - FakeServiceClient, -) - -class L2MemoryTests( - unittest.IsolatedAsyncioTestCase -): - - def test_runtime_memory_extractor_does_not_fallback_to_reasoning(self): - - response = { - "model": "fake-service", - "choices": [ - { - "index": 0, - "message": { - "content": "", - "reasoning_content": ( - "PRIVATE SUMMARIZER REASONING" - ), - }, - "finish_reason": "stop", - } - ], - } - - self.assertEqual( - extract_runtime_memory_text( - response - ), - "", - ) - - details = build_runtime_summarizer_response_details( - response, - extracted_memory="", - ) - - self.assertIn( - '"reasoning_generated": true', - details, - ) - self.assertIn( - '"reasoning_length": 28', - details, - ) - self.assertNotIn( - "PRIVATE SUMMARIZER REASONING", - details, - ) - self.assertNotIn( - "reasoning_content", - details, - ) - - def test_runtime_l2_memory_prompt_defines_pattern_layer(self): - - prompt = build_runtime_l2_memory_system_prompt() - - self.assertEqual( - prompt, - RUNTIME_L2_MEMORY_SYSTEM_PROMPT, - ) - - # Verify that the builder keeps every dedicated L2 rules section. - for rules_section in ( - ROLE, - OUTPUT_FORMAT, - BEHAVIOR_VS_INTENT, - SPAN_METADATA, - OCCURRENCE_COUNTING, - PATTERN_EVIDENCE_LINES, - EVIDENCE_LINE_LIFECYCLE, - PATTERN_FAMILY_DEDUPLICATION, - SELF_LEARNING_GUARD, - CONFIRMABLE_KEYS, - ): - self.assertIn( - rules_section, - prompt, - ) - - def test_l2_pattern_evidence_merge_preserves_first_seen_and_deduplicates(self): - - merged = merge_runtime_l2_pattern_evidence_memory( - previous_memory=( - "possible pattern: old line. Occurrences: 1; " - "first_seen_snapshot: 5; last_seen_snapshot: 5; " - "evidence summary: banana question; confidence: medium\n" - 'L2_pattern_evidence_1: user repeatedly sending message - "что такое бананы" ' - "[ first_seen_turn_snapshot: 5 ] [ last_seen_turn_snapshot: 5 ] [ occurrences: 1 ]" - ), - candidate_memory=( - "possible pattern: updated line. Occurrences: 2; " - "first_seen_snapshot: 5; last_seen_snapshot: 10; " - "evidence summary: banana question; confidence: medium\n" - 'L2_pattern_evidence_2: user repeatedly sending message - "что такое бананы" ' - "[ last_seen_turn_snapshot: 10 ] [ occurrences: 2 ]" - ), - ) - - self.assertIn( - "possible pattern: updated line", - merged, - ) - self.assertEqual( - 1, - merged.count( - "L2_pattern_evidence_" - ), - ) - self.assertIn( - "L2_pattern_evidence_1:", - merged, - ) - self.assertIn( - "что такое бананы", - merged, - ) - self.assertIn( - "[ first_seen_turn_snapshot: 5 ]", - merged, - ) - self.assertIn( - "[ last_seen_turn_snapshot: 10 ]", - merged, - ) - - def test_l2_candidate_evidence_lines_are_removed_before_deterministic_merge(self): - - cleaned = remove_runtime_l2_pattern_evidence_lines( - "possible pattern: repeated message. Occurrences: 4\n" - 'L2_pattern_evidence_1: user repeatedly sending one message [ quote: "ping" ] ' - "[ first_seen_turn_snapshot: 9 ] [ last_seen_turn_snapshot: 10 ] [ occurrences: 4 ]\n" - "scope: current session" - ) - - self.assertEqual( - "possible pattern: repeated message. Occurrences: 4\n" - "scope: current session", - cleaned, - ) - - def test_embedded_l2_pattern_evidence_is_extracted_for_runtime_display(self): - - runtime_l2_memory = ( - "possible pattern: User initiates a request for abstract creative content. " - 'Occurrences: 1; evidence: [ user_message: "draw something unusual" ]; ' - "L2_pattern_evidence_3: User initiates a request for abstract creative content. " - '[ quote: "draw something unusual" ] ' - "[ first_seen_turn_snapshot: 4 ] " - "[ last_seen_turn_snapshot: 4 ]" - ) - - evidence_lines = extract_runtime_l2_pattern_evidence_lines( - runtime_l2_memory - ) - - self.assertEqual( - [ - "L2_pattern_evidence_3: User initiates a request for abstract creative content. " - '[ quote: "draw something unusual" ] ' - "[ first_seen_turn_snapshot: 4 ] " - "[ last_seen_turn_snapshot: 4 ]", - ], - evidence_lines, - ) - - rendered = build_runtime_memory_context_text( - "current_request: waiting", - SimpleNamespace( - runtime_l2_memory=runtime_l2_memory, - ), - ) - - self.assertIn( - "L2_pattern_evidence_3:", - rendered, - ) - - def test_l2_pattern_evidence_example_normalizer_strips_spaces_commas_and_dots(self): - - self.assertEqual( - "чтотакоебананы", - normalize_l2_pattern_evidence_example( - " Что, такое. бананы " - ), - ) - - async def test_runtime_l1_diff_log_formats_float_noise(self): - - logger = FakeLogger() - context = SimpleNamespace( - logger=logger, - runtime_l2_pending_patches=[ - { - "total_diff": 4.65, - }, - { - "total_diff": 296.85, - }, - ], - runtime_l2_last_turn=2, - user_message_count=5, - ) - - await record_runtime_l1_diff( - context=context, - snapshot={ - "index": 3, - "total_diff": 167.29999999999998, - "patch": {}, - }, - turns=[], - ) - - self.assertEqual( - len(logger.service_logs), - 1, - ) - self.assertIn( - "[MEMORY:L1] L1 diff +167.3", - logger.service_logs[0], - ) - self.assertNotIn( - "167.29999999999998", - logger.service_logs[0], - ) - - async def test_l2_memory_waits_for_repeated_patch_keys(self): - - service_client = FakeServiceClient( - "possible pattern: should not run" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - logger=logger, - runtime_l2_memory="", - runtime_l1_diff_history=[ - { - "turn_number": 1, - "snapshot_index": 1, - "total_diff": 110, - "changes": { - "added": [ - { - "key": "topic", - "value": "one", - }, - ], - }, - }, - { - "turn_number": 2, - "snapshot_index": 2, - "total_diff": 254, - "changes": { - "added": [ - { - "key": "intent", - "value": "two", - }, - ], - }, - }, - { - "turn_number": 3, - "snapshot_index": 3, - "total_diff": 80, - "changes": { - "added": [ - { - "key": "choice", - "value": "three", - }, - ], - }, - }, - { - "turn_number": 4, - "snapshot_index": 4, - "total_diff": 140, - "changes": { - "added": [ - { - "key": "status", - "value": "four", - }, - ], - }, - }, - { - "turn_number": 5, - "snapshot_index": 5, - "total_diff": 90, - "changes": { - "added": [ - { - "key": "reference", - "value": "five", - }, - ], - }, - }, - ], - runtime_l2_last_turn=0, - user_message_count=5, - ) - - updated_memory = await maybe_summarize_runtime_l2_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "", - ) - self.assertEqual( - len(service_client.calls), - 0, - ) - self.assertEqual( - context.runtime_l2_memory, - "", - ) - - async def test_l2_memory_keeps_evidence_but_drops_unconfirmed_pattern(self): - - service_client = FakeServiceClient( - "possible pattern: user may be repeating one message. " - "Occurrences: 4; first_seen_snapshot: 9; last_seen_snapshot: 10; " - "evidence summary: duplicate rows for one snapshot; confidence: medium\n" - 'L2_pattern_evidence_1: user repeatedly sending one message [ quote: "ping" ] ' - "[ first_seen_turn_snapshot: 9 ] [ last_seen_turn_snapshot: 10 ] [ occurrences: 4 ]" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - logger=logger, - runtime_memory="", - runtime_memory_snapshots=[], - runtime_memory_snapshot_index=0, - runtime_l2_memory="", - runtime_l1_diff_history=[ - { - "turn_number": 1, - "snapshot_index": 1, - "total_diff": 120, - "changes": { - "added": [ - { - "key": "diagnostic_signal", - "value": "first repeated key episode", - }, - ], - }, - }, - { - "turn_number": 2, - "snapshot_index": 2, - "total_diff": 80, - "changes": { - "added": [ - { - "key": "status", - "value": "separator episode", - }, - ], - }, - }, - { - "turn_number": 3, - "snapshot_index": 3, - "total_diff": 140, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "first repeated key episode", - "current_key": "diagnostic_signal", - "current_value": "second repeated key episode", - }, - ], - }, - }, - ], - runtime_l2_last_turn=0, - user_message_count=3, - ) - - updated_memory = await maybe_summarize_runtime_l2_memory( - context=context, - ) - - self.assertNotIn( - "possible pattern", - updated_memory, - ) - self.assertIn( - "L2_pattern_evidence_1:", - updated_memory, - ) - self.assertIn( - 'quote: "ping"', - updated_memory, - ) - self.assertIn( - "[ first_seen_turn_snapshot: 9 ]", - updated_memory, - ) - self.assertIn( - "[ last_seen_turn_snapshot: 10 ]", - updated_memory, - ) - - async def test_l2_memory_runs_after_repeated_patch_keys_even_with_noisy_diff(self): - - service_client = FakeServiceClient( - "possible pattern: user revisits the same implementation tradeoff", - ) - logger = FakeLogger() - emitter = SimpleNamespace( - events=[], - emit=None, - ) - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=emitter, - logger=logger, - runtime_memory="", - runtime_memory_snapshots=[], - runtime_memory_snapshot_index=0, - runtime_l2_memory="", - runtime_l1_diff_history=[ - { - "turn_number": 1, - "snapshot_index": 1, - "total_diff": 110, - "changes": { - "added": [ - { - "key": "diagnostic_signal", - "value": "early broad update", - }, - ], - }, - }, - { - "turn_number": 2, - "snapshot_index": 2, - "total_diff": 254, - "changes": { - "changed": [ - { - "previous_key": "topic", - "previous_value": "early broad update", - "current_key": "topic", - "current_value": "large rewrite", - }, - ], - }, - }, - { - "turn_number": 3, - "snapshot_index": 3, - "total_diff": 199.05, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "large rewrite", - "current_key": "diagnostic_signal", - "current_value": "memory mechanics", - }, - ], - }, - }, - { - "turn_number": 4, - "snapshot_index": 4, - "total_diff": 151, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "memory mechanics", - "current_key": "diagnostic_signal", - "current_value": "pattern trigger", - }, - ], - }, - }, - { - "turn_number": 5, - "snapshot_index": 5, - "total_diff": 144.9, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "pattern trigger", - "current_key": "diagnostic_signal", - "current_value": "L2 window", - }, - ], - }, - }, - { - "turn_number": 6, - "snapshot_index": 6, - "total_diff": 77.6, - "changes": { - "changed": [ - { - "previous_key": "intent", - "previous_value": "inspect diff", - "current_key": "intent", - "current_value": "adjust trigger", - }, - ], - }, - }, - { - "turn_number": 7, - "snapshot_index": 7, - "total_diff": 104.69, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "L2 window", - "current_key": "diagnostic_signal", - "current_value": "repeated keys", - }, - ], - }, - }, - ], - runtime_l2_last_turn=0, - user_message_count=7, - session_id="test-session", - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_l2_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "possible pattern: user revisits the same implementation tradeoff", - ) - self.assertEqual( - len(service_client.calls), - 1, - ) - self.assertEqual( - service_client.calls[0]["timeout"], - config.SERVICE_REQUEST_TIMEOUT, - ) - self.assertEqual( - context.runtime_l2_memory, - "possible pattern: user revisits the same implementation tradeoff", - ) - self.assertEqual( - context.runtime_l2_last_turn, - 7, - ) - self.assertEqual( - len(context.runtime_l1_diff_history), - 7, - ) - self.assertIn( - "Selected L1 recurrence evidence", - service_client.calls[0]["user_prompt"], - ) - self.assertIn( - "signal added: diagnostic_signal: early broad update", - service_client.calls[0]["user_prompt"], - ) - self.assertIn( - "signal changed: diagnostic_signal: memory mechanics", - service_client.calls[0]["user_prompt"], - ) - self.assertNotIn( - "large rewrite", - service_client.calls[0]["user_prompt"], - ) - self.assertNotIn( - "adjust trigger", - service_client.calls[0]["user_prompt"], - ) - self.assertEqual( - logger.summarizer_logs[0][0], - "[MEMORY:L2] L2 summarizer request", - ) - self.assertIn( - '"messages"', - logger.summarizer_logs[0][1], - ) - self.assertIn( - "signal added: diagnostic_signal: early broad update", - logger.summarizer_logs[0][1], - ) - self.assertEqual( - logger.summarizer_logs[1][0], - "[MEMORY:L2] L2 pattern memory summarizer result", - ) - self.assertEqual( - logger.summarizer_logs[1][1], - "possible pattern: user revisits the same implementation tradeoff", - ) - self.assertEqual( - context.runtime_memory_snapshots, - [], - ) - memory_events = [ - event - for event in context.emitter.events - if event["type"] == "runtime_memory_update" - ] - - self.assertEqual( - len(memory_events), - 0, - ) - - async def test_l2_memory_ignores_reasoning_only_summarizer_response(self): - - private_reasoning = ( - "Analysis of Evidence:\n" - "PRIVATE L2 CHAIN OF THOUGHT" - ) - - class ReasoningOnlyServiceClient: - model_uid = "fake-service" - - def __init__(self): - self.calls = [] - - async def ask( - self, - *, - system_prompt: str, - user_prompt: str, - temperature: float, - max_tokens: int, - timeout: float | None = None, - ): - - self.calls.append({ - "system_prompt": system_prompt, - "user_prompt": user_prompt, - "temperature": temperature, - "max_tokens": max_tokens, - "timeout": timeout, - }) - - return { - "model": self.model_uid, - "choices": [ - { - "message": { - "content": "", - "reasoning": private_reasoning, - }, - "finish_reason": "stop", - } - ], - } - - service_client = ReasoningOnlyServiceClient() - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_memory="", - runtime_memory_snapshots=[], - runtime_memory_snapshot_index=0, - runtime_l2_memory=( - "possible pattern: previous safe memory" - ), - runtime_l1_diff_history=[ - { - "turn_number": 1, - "snapshot_index": 1, - "total_diff": 110, - "changes": { - "added": [ - { - "key": "diagnostic_signal", - "value": "early broad update", - }, - ], - }, - }, - { - "turn_number": 2, - "snapshot_index": 2, - "total_diff": 254, - "changes": { - "changed": [ - { - "previous_key": "topic", - "previous_value": "early broad update", - "current_key": "topic", - "current_value": "large rewrite", - }, - ], - }, - }, - { - "turn_number": 3, - "snapshot_index": 3, - "total_diff": 199.05, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "large rewrite", - "current_key": "diagnostic_signal", - "current_value": "memory mechanics", - }, - ], - }, - }, - { - "turn_number": 4, - "snapshot_index": 4, - "total_diff": 151, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "memory mechanics", - "current_key": "diagnostic_signal", - "current_value": "pattern trigger", - }, - ], - }, - }, - { - "turn_number": 5, - "snapshot_index": 5, - "total_diff": 144.9, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "pattern trigger", - "current_key": "diagnostic_signal", - "current_value": "L2 window", - }, - ], - }, - }, - { - "turn_number": 6, - "snapshot_index": 6, - "total_diff": 77.6, - "changes": { - "changed": [ - { - "previous_key": "intent", - "previous_value": "inspect diff", - "current_key": "intent", - "current_value": "adjust trigger", - }, - ], - }, - }, - { - "turn_number": 7, - "snapshot_index": 7, - "total_diff": 104.69, - "changes": { - "changed": [ - { - "previous_key": "diagnostic_signal", - "previous_value": "L2 window", - "current_key": "diagnostic_signal", - "current_value": "repeated keys", - }, - ], - }, - }, - ], - runtime_l2_last_turn=0, - user_message_count=7, - session_id="test-session", - ) - - updated_memory = await maybe_summarize_runtime_l2_memory( - context=context, - ) - - self.assertEqual( - len(service_client.calls), - 1, - ) - self.assertEqual( - updated_memory, - "possible pattern: previous safe memory", - ) - self.assertEqual( - context.runtime_l2_memory, - "possible pattern: previous safe memory", - ) - - log_text = "\n".join( - str(item) - for item in logger.summarizer_logs - ) - - self.assertNotIn( - "PRIVATE L2 CHAIN OF THOUGHT", - log_text, - ) - self.assertNotIn( - "Analysis of Evidence", - context.runtime_l2_memory, - ) diff --git a/tests/test_l3_session_memory.py b/tests/test_l3_session_memory.py deleted file mode 100644 index fd6f66de..00000000 --- a/tests/test_l3_session_memory.py +++ /dev/null @@ -1,1214 +0,0 @@ -import unittest -from types import ( - SimpleNamespace, -) -from runtime.L3_memory_utils import ( - build_l3_session_memory_max_tokens, - build_runtime_session_memory_user_prompt, -) -from runtime.L3_memory import ( - maybe_summarize_runtime_session_memory, -) -from runtime.L3_memory_rules import ( - L3_OUTPUT_MAX_TOKENS, -) -from config_loader import ( - config, -) -from tests.helpers.memory import ( - FakeLogger, - FakeServiceClient, -) - -class L3SessionMemoryTests( - unittest.IsolatedAsyncioTestCase -): - - def test_l3_session_memory_prompt_uses_all_runtime_snapshots(self): - - prompt = build_runtime_session_memory_user_prompt( - current_session_memory="decision: old handoff", - runtime_memory_snapshots=[ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - { - "index": 1, - "raw_memory": "decision: final direction", - "total_diff": 80, - }, - ], - diff_history=[ - { - "snapshot_index": 1, - "total_diff": 80, - "changes": { - "added": [ - { - "key": "decision", - } - ], - }, - }, - ], - ) - - self.assertIn( - "Selected L1 runtime memory snapshot history", - prompt, - ) - self.assertIn( - "runtime_memory_id:", - prompt, - ) - self.assertIn( - "topic: first topic", - prompt, - ) - self.assertIn( - "decision: final direction", - prompt, - ) - self.assertIn( - '"total_diff": 80', - prompt, - ) - self.assertIn( - "omitted_middle_snapshots: 0", - prompt, - ) - self.assertIn( - "Recent L1 diff history", - prompt, - ) - self.assertIn( - "omitted_older_diffs: 0", - prompt, - ) - - async def test_l3_session_memory_skips_service_when_turn_aborted(self): - - service_client = FakeServiceClient( - "decision: should not be requested" - ) - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=FakeLogger(), - runtime_save_session_requested=True, - runtime_turn_abort_requested=True, - runtime_turn_discard_requested=False, - runtime_l3_session_memory="decision: existing", - session_memory="decision: existing", - runtime_memory_snapshots=[ - { - "index": 1, - "raw_memory": "decision: pending save", - "total_diff": 80, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - result = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - result, - "decision: existing", - ) - self.assertEqual( - service_client.calls, - [], - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertEqual( - context.runtime_save_session_result["status"], - "aborted", - ) - self.assertEqual( - context.runtime_save_session_result["reason"], - "turn_aborted", - ) - self.assertEqual( - context.emitter.events, - [], - ) - - async def test_l3_session_memory_discards_service_response_when_turn_aborts(self): - - class AbortingServiceClient( - FakeServiceClient - ): - - async def ask( - self, - **kwargs, - ): - - response = await super().ask( - **kwargs - ) - context.runtime_turn_abort_requested = True - return response - - service_client = AbortingServiceClient( - "decision: should not be committed" - ) - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=FakeLogger(), - runtime_save_session_requested=True, - runtime_turn_abort_requested=False, - runtime_turn_discard_requested=False, - runtime_l3_session_memory="decision: existing", - session_memory="decision: existing", - session_memory_source="", - runtime_session_memory_updates=0, - runtime_l1_diff_history=[], - runtime_memory_snapshot_index=1, - timestamp="2026-06-05T13:38:50", - current_date="2026-06-05", - current_time="13:38:50", - weekday="Friday", - year=2026, - runtime_memory_snapshots=[ - { - "index": 1, - "raw_memory": "decision: pending save", - "total_diff": 80, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - result = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - result, - "decision: existing", - ) - self.assertEqual( - len(service_client.calls), - 1, - ) - self.assertEqual( - context.session_memory, - "decision: existing", - ) - self.assertEqual( - context.runtime_session_memory_updates, - 0, - ) - self.assertEqual( - context.runtime_save_session_result["status"], - "aborted", - ) - self.assertFalse( - any( - event.get("type") == "runtime_session_memory_update" - for event in context.emitter.events - ) - ) - self.assertFalse( - any( - event.get("type") == "runtime_action" - and event.get("action") == "save_session" - and event.get("status") == "completed" - for event in context.emitter.events - ) - ) - - def test_l3_session_memory_prompt_bounds_long_snapshot_history(self): - - prompt = build_runtime_session_memory_user_prompt( - current_session_memory="decision: old handoff", - runtime_memory_snapshots=[ - { - "index": index, - "raw_memory": f"topic: snapshot {index}", - "total_diff": index, - } - for index in range(30) - ], - diff_history=[ - { - "snapshot_index": index, - "total_diff": index, - "changes": { - "changed": [ - { - "current_key": f"decision_{index}", - } - ], - }, - } - for index in range(40) - ], - ) - - self.assertIn( - "omitted_middle_snapshots: 0", - prompt, - ) - self.assertIn( - "topic: snapshot 0", - prompt, - ) - self.assertIn( - "topic: snapshot 29", - prompt, - ) - self.assertIn( - "topic: snapshot 10", - prompt, - ) - self.assertIn( - "omitted_older_diffs: 32", - prompt, - ) - - def test_l3_session_memory_prompt_uses_compact_digest_not_raw_archive(self): - - prompt = build_runtime_session_memory_user_prompt( - current_session_memory="\n".join( - f"old narrative {index}: {'a' * 300}" - for index in range(20) - ), - runtime_memory_snapshots=[ - { - "index": index, - "raw_memory": ( - f"decision: keep snapshot {index}\n" - f"narrative: {'d' * 1200}" - ), - "total_diff": index, - } - for index in range(12) - ], - diff_history=[ - { - "snapshot_index": index, - "total_diff": index, - "changes": { - "changed": [ - { - "previous_key": "narrative", - "previous_value": "e" * 1000, - "current_key": "decision", - "current_value": "f" * 1000, - } - ], - }, - } - for index in range(20) - ], - ) - - self.assertIn( - "L3 compact digest minimal: False", - prompt, - ) - self.assertNotIn( - "Compact L2 pattern context:", - prompt, - ) - self.assertNotIn( - "Current L2 pattern memory for context only:", - prompt, - ) - self.assertEqual( - prompt.count("snapshot:"), - 12, - ) - self.assertNotIn( - "c" * 500, - prompt, - ) - self.assertNotIn( - "e" * 500, - prompt, - ) - self.assertIn( - "omitted_older_diffs:", - prompt, - ) - - def test_l3_session_memory_prompt_filters_noisy_l1_diff_keys(self): - - prompt = build_runtime_session_memory_user_prompt( - current_session_memory="decision: old handoff", - runtime_memory_snapshots=[ - { - "index": 1, - "raw_memory": "decision: keep useful snapshot", - "total_diff": 80, - }, - ], - diff_history=[ - { - "snapshot_index": 1, - "total_diff": 240.95, - "changes": { - "added": [ - { - "key": "last_jin_response", - }, - { - "key": "user_name", - }, - { - "key": "active_memory_temporal_continuity", - }, - ], - "changed": [ - { - "current_key": "user_message", - }, - { - "current_key": "user_idle", - }, - ], - "removed": [ - { - "key": "last_jin_response", - }, - ], - }, - }, - { - "snapshot_index": 2, - "total_diff": 172.2, - "changes": { - "added": [ - { - "key": "last_jin_response", - }, - ], - "changed": [ - { - "current_key": "active_memory_temporal_continuity", - }, - { - "current_key": "user_idle", - }, - ], - "removed": [ - { - "key": "last_jin_response", - }, - ], - }, - }, - ], - ) - - self.assertIn( - '"user_name"', - prompt, - ) - self.assertNotIn( - "last_jin_response", - prompt, - ) - self.assertNotIn( - "active_memory_temporal_continuity", - prompt, - ) - self.assertNotIn( - "user_message", - prompt, - ) - self.assertNotIn( - "user_idle", - prompt, - ) - self.assertIn( - '"snapshot_index": 1', - prompt, - ) - self.assertNotIn( - '"snapshot_index": 2', - prompt, - ) - - def test_l3_session_memory_budget_uses_detected_context_window(self): - - system_prompt = "system " * 2000 - user_prompt = "user " * 2000 - - configured_budget = build_l3_session_memory_max_tokens( - system_prompt=system_prompt, - user_prompt=user_prompt, - ) - detected_budget = build_l3_session_memory_max_tokens( - system_prompt=system_prompt, - user_prompt=user_prompt, - context_window=8192, - ) - - self.assertEqual( - configured_budget, - 128, - ) - self.assertGreater( - detected_budget, - configured_budget, - ) - - async def test_l3_session_memory_updates_from_snapshot_history(self): - - service_client = FakeServiceClient( - "decision: Continue session memory implementation\n" - "next step: Verify browser persistence" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="", - session_memory="", - session_memory_source="", - runtime_session_memory_updates=0, - runtime_l2_memory="", - timestamp="2026-06-05T13:38:50", - current_date="2026-06-05", - current_time="13:38:50", - weekday="Friday", - year=2026, - runtime_l1_diff_history=[ - { - "snapshot_index": 1, - "total_diff": 80, - }, - ], - runtime_memory_snapshot_index=1, - runtime_memory_snapshots=[ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - { - "index": 1, - "raw_memory": "decision: final direction", - "total_diff": 80, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertIn( - "Continue session memory implementation", - updated_memory, - ) - self.assertTrue( - updated_memory.startswith( - "session_saved_at: 2026-06-05 13:38, Friday\n" - "session_snapshot_first_turn: 0\n" - ) - ) - self.assertEqual( - context.runtime_session_memory_updates, - 1, - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertEqual( - context.runtime_memory_snapshots, - [ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - { - "index": 1, - "raw_memory": "decision: final direction", - "total_diff": 80, - }, - ], - ) - self.assertEqual( - context.runtime_memory_snapshot_index, - 1, - ) - self.assertEqual( - context.session_memory_source, - "L3", - ) - self.assertIn( - "topic: first topic", - service_client.calls[0]["user_prompt"], - ) - self.assertIn( - "decision: final direction", - service_client.calls[0]["user_prompt"], - ) - self.assertIn( - "2026-06-05 13:38, Friday", - service_client.calls[0]["user_prompt"], - ) - self.assertLess( - service_client.calls[0]["user_prompt"].index( - "" - ), - service_client.calls[0]["user_prompt"].index( - "Current L3 session memory:" - ), - ) - self.assertEqual( - service_client.calls[0]["timeout"], - config.SERVICE_REQUEST_TIMEOUT, - ) - self.assertGreater( - service_client.calls[0]["max_tokens"], - 0, - ) - self.assertLessEqual( - service_client.calls[0]["max_tokens"], - 2048, - ) - self.assertGreaterEqual( - service_client.calls[0]["max_tokens"], - 128, - ) - self.assertEqual( - service_client.calls[0]["max_tokens"], - L3_OUTPUT_MAX_TOKENS, - ) - self.assertIn( - ( - "[MEMORY:L3] L3 session output token budget capped at " - f"{L3_OUTPUT_MAX_TOKENS}" - ), - logger.runtime_logs, - ) - self.assertEqual( - context.emitter.events[-2]["type"], - "runtime_session_memory_update", - ) - self.assertTrue( - context.emitter.events[-2]["persist"], - ) - self.assertEqual( - context.emitter.events[-1], - { - "type": "runtime_action", - "action": "save_session", - "status": "completed", - }, - ) - - async def test_l3_session_memory_uses_timestamp_when_date_fields_are_empty(self): - - service_client = FakeServiceClient( - "decision: Continue restored session" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="", - session_memory="", - session_memory_source="", - runtime_session_memory_updates=0, - runtime_l2_memory="", - timestamp="2026-06-05T13:38:50", - current_date="", - current_time="", - weekday="", - year=2026, - runtime_l1_diff_history=[], - runtime_memory_snapshot_index=1, - runtime_memory_snapshots=[ - { - "index": 1, - "raw_memory": "decision: restored tail", - "total_diff": 80, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertTrue( - updated_memory.startswith( - "session_saved_at: 2026-06-05 13:38, Friday\n" - ) - ) - self.assertNotIn( - "session_saved_at: ,", - updated_memory, - ) - - async def test_l3_session_memory_uses_current_runtime_update_steps_after_restore(self): - - service_client = FakeServiceClient( - "decision: Continue restored current session" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory=( - "session_saved_at: 2026-06-01 09:00, Monday\n" - "session_snapshot_first_turn: 42\n" - "session_snapshot_last_turn: 99\n" - "decision: restored previous session handoff" - ), - session_memory_source="browser_localStorage", - session_memory="", - runtime_session_memory_updates=1, - runtime_l3_saved_runtime_snapshot_index=None, - runtime_l2_memory="", - timestamp="2026-06-05T13:38:50", - current_date="", - current_time="", - weekday="", - year=2026, - turn_number=97, - user_message_count=97, - assistant_message_count=108, - runtime_memory_updates=13, - runtime_l1_diff_history=[ - { - "snapshot_index": 1, - "total_diff": 80, - }, - ], - runtime_memory_snapshot_index=1, - runtime_memory_snapshots=[ - { - "index": 1, - "raw_memory": "decision: current restored session tail", - "total_diff": 80, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertIn( - "session_saved_at: 2026-06-05 13:38, Friday", - updated_memory, - ) - self.assertIn( - "session_snapshot_first_turn: 1", - updated_memory, - ) - self.assertIn( - "session_snapshot_last_turn: 13", - updated_memory, - ) - self.assertEqual( - context.runtime_l3_session_first_turn, - 1, - ) - self.assertEqual( - context.runtime_l3_session_last_turn, - 13, - ) - - async def test_l3_session_memory_merges_previous_snapshot_with_unsaved_tail_only(self): - - service_client = FakeServiceClient( - "decision: merged handoff after new tail" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory=( - "session_snapshot_first_turn: 0\n" - "session_snapshot_last_turn: 15\n" - "decision: old consolidated handoff" - ), - session_memory="", - session_memory_source="", - runtime_session_memory_updates=1, - runtime_l3_saved_runtime_snapshot_index=15, - runtime_l3_session_first_turn=0, - runtime_l3_session_last_turn=15, - runtime_l2_memory="", - timestamp="2026-06-05T13:38:50", - current_date="2026-06-05", - current_time="13:38:50", - weekday="Friday", - year=2026, - runtime_l1_diff_history=[ - { - "snapshot_index": 15, - "total_diff": 80, - }, - { - "snapshot_index": 16, - "total_diff": 20, - }, - { - "snapshot_index": 20, - "total_diff": 70, - }, - ], - runtime_memory_snapshot_index=20, - runtime_memory_snapshots=[ - { - "index": 14, - "raw_memory": "topic: old stale page", - "total_diff": 30, - }, - { - "index": 15, - "raw_memory": "decision: old saved boundary", - "total_diff": 80, - }, - { - "index": 16, - "raw_memory": "topic: fresh tail starts", - "total_diff": 20, - }, - { - "index": 20, - "raw_memory": "decision: fresh tail ends", - "total_diff": 70, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - prompt = service_client.calls[0]["user_prompt"] - - self.assertIn( - "decision: old consolidated handoff", - prompt, - ) - self.assertIn( - "topic: fresh tail starts", - prompt, - ) - self.assertIn( - "decision: fresh tail ends", - prompt, - ) - self.assertNotIn( - "topic: old stale page", - prompt, - ) - self.assertNotIn( - "decision: old saved boundary", - prompt, - ) - self.assertIn( - "session_snapshot_first_turn: 0", - updated_memory, - ) - self.assertTrue( - updated_memory.startswith( - "session_saved_at: 2026-06-05 13:38, Friday\n" - "session_snapshot_first_turn: 0\n" - ) - ) - self.assertIn( - "session_snapshot_last_turn: 20", - updated_memory, - ) - self.assertEqual( - context.runtime_l3_saved_runtime_snapshot_index, - 20, - ) - self.assertEqual( - context.runtime_l3_session_last_turn, - 20, - ) - - async def test_l3_session_memory_logs_when_response_reaches_max_tokens(self): - - service_client = FakeServiceClient( - "decision: incomplete", - finish_reasons=[ - "length", - ], - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="decision: keep current", - session_memory="decision: keep current", - session_memory_source="", - runtime_session_memory_updates=0, - runtime_l2_memory="", - runtime_l1_diff_history=[], - runtime_memory_snapshots=[ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "decision: keep current", - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertFalse( - context.runtime_save_session_action_emitted, - ) - calls_after_skip = len( - service_client.calls - ) - repeated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - self.assertEqual( - repeated_memory, - "decision: keep current", - ) - self.assertEqual( - len(service_client.calls), - calls_after_skip, - ) - self.assertEqual( - context.runtime_memory_snapshots, - [ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - ], - ) - self.assertIn( - "[MEMORY:L3] L3 session summarizer reached max_tokens", - logger.runtime_logs, - ) - self.assertEqual( - logger.errors[-1][0], - "[MEMORY:L3] L3 session memory update skipped", - ) - self.assertIn( - "truncated by max_tokens", - logger.errors[-1][1], - ) - - async def test_l3_session_memory_skips_when_minimal_digest_exceeds_budget(self): - - class TinyContextServiceClient(FakeServiceClient): - - async def resolve_request_context_window(self): - return 600 - - service_client = TinyContextServiceClient( - "should not be called" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="decision: keep current", - session_memory="decision: keep current", - session_memory_source="", - runtime_session_memory_updates=0, - runtime_l2_memory="", - runtime_l1_diff_history=[], - runtime_memory_snapshots=[ - { - "index": 0, - "raw_memory": "decision: keep latest", - "total_diff": 1, - } - ], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "decision: keep current", - ) - self.assertEqual( - service_client.calls, - [], - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertEqual( - context.emitter.events[-1], - { - "type": "runtime_action", - "action": "save_session", - "status": "completed", - }, - ) - self.assertEqual( - logger.errors[-1][0], - "[MEMORY:L3] L3 session memory update skipped", - ) - self.assertIn( - "compact digest still exceeds safe input budget", - logger.errors[-1][1], - ) - self.assertEqual( - context.runtime_memory_snapshots, - [ - { - "index": 0, - "raw_memory": "decision: keep latest", - "total_diff": 1, - } - ], - ) - - async def test_l3_session_memory_preserves_snapshots_when_update_fails(self): - - service_client = FakeServiceClient( - RuntimeError("service unavailable") - ) - logger = FakeLogger() - snapshots = [ - { - "index": 0, - "raw_memory": "topic: first topic", - "total_diff": 30, - }, - { - "index": 1, - "raw_memory": "decision: final direction", - "total_diff": 80, - }, - ] - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="decision: keep current", - session_memory="decision: keep current", - session_memory_source="", - runtime_l2_memory="", - runtime_l1_diff_history=[], - runtime_memory_snapshots=list(snapshots), - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "decision: keep current", - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertEqual( - context.runtime_memory_snapshots, - snapshots, - ) - self.assertEqual( - logger.errors[-1][0], - "[MEMORY:L3] L3 session memory update failed", - ) - - async def test_l3_session_memory_no_snapshots_clears_save_request(self): - - service_client = FakeServiceClient( - "should not be called" - ) - logger = FakeLogger() - context = SimpleNamespace( - clients={ - "service": service_client, - }, - emitter=SimpleNamespace( - events=[], - emit=None, - ), - logger=logger, - runtime_save_session_requested=True, - runtime_l3_session_memory="decision: keep current", - session_memory="decision: keep current", - runtime_memory_snapshots=[], - ) - - async def emit(event): - context.emitter.events.append( - event - ) - - context.emitter.emit = emit - - updated_memory = await maybe_summarize_runtime_session_memory( - context=context, - ) - - self.assertEqual( - updated_memory, - "decision: keep current", - ) - self.assertEqual( - service_client.calls, - [], - ) - self.assertEqual( - context.runtime_memory_snapshots, - [], - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertEqual( - logger.runtime_logs, - [ - "[MEMORY:L3] L3 session save skipped: no snapshots", - ], - ) - diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index 7bd6b75f..02c72aea 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -98,12 +98,12 @@ def test_delete_marks_local_l4_store_deleted_before_sync(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=l4-report-link-1', source) - self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=delayed-loaded-facts-1', source) - self.assertIn('/static/js/logger/logger.js?v=jin-size-5', source) - self.assertIn('/static/js/logger/trace-modal.js?v=attached-files-1', source) - self.assertIn('/static/js/logger/log-entries.js?v=delayed-fact-unlink-1', source) - self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-2', source) + self.assertIn('/static/css/runtime-memory.css?v=delayed-collapsible-cards-2', source) + self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=server-l4-scheduler-1', source) + self.assertIn('/static/js/logger/logger.js?v=delayed-context-plaque-3', source) + self.assertIn('/static/js/logger/trace-modal.js?v=context-session-actions-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=update-l4-message-1', source) + self.assertIn('/static/js/socket/event-handlers.js?v=stream-avatar-1', source) if __name__ == "__main__": diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index f41f22db..4b77fe08 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -127,30 +127,6 @@ def test_every_runtime_l1_persists_the_live_session_checkpoint(self): self.assertIn("session.persistLiveSessionCheckpoint(", block) self.assertIn("Number(data.updates || 0) <= 0", block) - def test_explicit_save_merges_l3_into_existing_live_session_record(self): - source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") - - live_start = source.index("function persistLiveSessionCheckpoint(data)") - live_end = source.index("function buildSessionSaveRuntimeSnapshot", live_start) - live_block = source[live_start:live_end] - - self.assertIn("writeLatestSavedRuntimeMemory({", live_block) - self.assertIn("writeLatestSavedSessionMemory({", live_block) - self.assertIn("session_snapshot: sessionSnapshot", live_block) - self.assertIn("archive: false", live_block) - - save_start = source.index("function persistSessionMemory(data)") - save_end = source.index("function getRuntimeMemoryForSoftReconnect", save_start) - save_block = source[save_start:save_end] - - self.assertIn("liveSessionCheckpoint", save_block) - self.assertIn("? liveSessionCheckpoint", save_block) - self.assertIn("session_memory_saved_at: savedAt", save_block) - self.assertIn("session_memory: sessionMemory", save_block) - self.assertNotIn( - "latestSavedRuntimeMemoryStorageKey", - save_block, - ) def test_bootstrap_prefers_continuous_last_saved_runtime(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") @@ -162,7 +138,11 @@ def test_bootstrap_prefers_continuous_last_saved_runtime(self): previous_runtime_read = block.index("readLatestPreviousRuntimeMemory") self.assertLess(saved_runtime_read, previous_runtime_read) self.assertIn( - "lastSavedRuntime is now a continuously refreshed pointer", + "const conversationCheckpoint =", + block, + ) + self.assertIn( + "browserCheckpoint || legacyCompletedCheckpoint", block, ) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 76ad425e..99fd3e22 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -150,7 +150,7 @@ def test_memory_highlight_reorder_transition_is_light_and_bounded(self): css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") self.assertIn( - "const MEMORY_ROW_REORDER_TRANSITION_MAX_ROWS = 10;", + "const MEMORY_ROW_REORDER_TRANSITION_FALLBACK_MS = 230;", source, ) self.assertIn( @@ -170,7 +170,7 @@ def test_memory_highlight_reorder_transition_is_light_and_bounded(self): source, ) self.assertIn( - "rows.length <= MEMORY_ROW_REORDER_TRANSITION_MAX_ROWS", + "rows.length > 1", source, ) self.assertIn( @@ -292,7 +292,7 @@ def test_default_collapsed_avatar_expands_without_reset_delay(self): reset_end = source.index("function finishCollapsedAvatarResetAndExpand", reset_start) reset_body = source[reset_start:reset_end] handler_start = source.index( - 'memoryDragHandle.addEventListener("dblclick"' + 'memoryDragHandle.addEventListener("click"' ) handler_end = source.index( "togglePanelCollapseFromHeader(", @@ -489,7 +489,7 @@ def test_delayed_memory_fact_ids_render_inline_with_fact_titles(self): source, ) self.assertIn( - 'item.title =', + 'bindRuntimeMemoryHoverTitle(\n item,\n title', source, ) self.assertIn( @@ -786,7 +786,7 @@ def test_runtime_memory_value_presentation_hides_suffixes_and_truncates_text(sel created_at: "2026-08-12T11:59:55Z", }); -if (presentation.text !== `${"x".repeat(100)}...`) { +if (presentation.text !== `${"x".repeat(50)}...`) { throw new Error(`unexpected visible text: ${presentation.text}`); } @@ -848,11 +848,11 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=l4-report-link-1', + '/static/css/runtime-memory.css?v=delayed-collapsible-cards-2', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=files-panel-1', + '/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1', source, ) self.assertIn( @@ -860,11 +860,11 @@ def test_cache_versions_are_bumped(self): source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=delayed-load-highlight-1', + '/static/js/runtime/runtime.js?v=facts-memory-server-sync-1', source, ) self.assertIn( - '/static/js/chat.js?v=jin-size-1', + '/static/js/chat.js?v=stream-avatar-2-reference-ids-1', source, ) diff --git a/tests/test_metabolism_homeostat.py b/tests/test_metabolism_homeostat.py index e7d1eb0d..8bfefcf7 100644 --- a/tests/test_metabolism_homeostat.py +++ b/tests/test_metabolism_homeostat.py @@ -213,7 +213,7 @@ def test_brain_context_uses_generated_instruction_without_channel_labels(self): user_input="продолжим наш JIN", ) - self.assertIn("Instructions: Use shared history only where it helps", prompt) + self.assertIn("Possible reaction: Use shared history only where it helps", prompt) self.assertNotIn("Oxytocin is elevated", prompt) self.assertNotIn("Serotonin is elevated", prompt) self.assertIn('role="silent_homeostat"', prompt) diff --git a/tests/test_reconnect_persistence_client_contract.py b/tests/test_reconnect_persistence_client_contract.py index f37b08ad..36f4faf7 100644 --- a/tests/test_reconnect_persistence_client_contract.py +++ b/tests/test_reconnect_persistence_client_contract.py @@ -17,7 +17,7 @@ class ReconnectPersistenceClientContractTests(unittest.TestCase): - def test_soft_reconnect_carries_saved_session_state(self): + def test_soft_reconnect_carries_live_runtime_state_without_removed_l3_memory(self): source = RUNTIME_SESSION_JS.read_text( encoding="utf-8" @@ -32,19 +32,23 @@ def test_soft_reconnect_carries_saved_session_state(self): block = source[start:end] self.assertIn( - "getPersistedSessionBootstrap()", + "runtime_memory: runtimeText,", block, ) self.assertIn( - "session_memory:", + "runtime_snapshot:", block, ) self.assertIn( - "session_memory_updates:", + "loaded_memory_ids:", block, ) - self.assertIn( - "loaded_memory_ids:", + self.assertNotIn( + "session_memory:", + block, + ) + self.assertNotIn( + "session_memory_updates:", block, ) @@ -66,7 +70,7 @@ def test_runtime_session_cache_version_is_bumped(self): ) self.assertIn( - "runtime-session.js?v=delayed-load-contract-1-reconnect-persistence-1", + "runtime-session.js?v=avatar-motion-1&delayed-load-contract-1-reconnect-persistence-1", source, ) diff --git a/tests/test_runtime_avatar_reasoning_motion_client_contract.py b/tests/test_runtime_avatar_reasoning_motion_client_contract.py index b922f510..4a0f7fb6 100644 --- a/tests/test_runtime_avatar_reasoning_motion_client_contract.py +++ b/tests/test_runtime_avatar_reasoning_motion_client_contract.py @@ -24,8 +24,8 @@ def test_reasoning_stream_switches_avatar_motion_mode_at_visible_boundaries(self self.assertGreater(thinking_start, 0) answer_pos = source.index("stream.answer += chunk;") - answer_stop = source.rfind("stopStreamRuntimeAvatarReasoning(", 0, answer_pos) - self.assertGreater(answer_stop, thinking_pos) + answer_start = source.rfind("startStreamRuntimeAvatarReasoning(", 0, answer_pos) + self.assertGreater(answer_start, thinking_pos) finish_start = source.index("function finishStreamMessage(") finish_end = source.index("window.normalizeJinLoopInput", finish_start) @@ -44,7 +44,7 @@ def test_repeated_snapshot_render_keeps_existing_avatar_svg(self): self.assertIn("syncMemorySignalLayer(kind, { applyGlows: false })", source) render_start = source.index("function renderAvatar(snapshot, options = {})") - render_end = source.index("function applyCenterColor(color)", render_start) + render_end = source.index("function applyCenterColor(color, options = {})", render_start) render_body = source[render_start:render_end] guard_pos = render_body.index("if (runtimeGeometryUnchanged)") replace_pos = render_body.index("avatarRoot.replaceChildren(svg);") @@ -190,7 +190,7 @@ def test_avatar_rebuild_preserves_rotation_phase_during_reasoning(self): self.assertIn('"data-avatar-rotation-key": rotationKey', source) render_start = source.index("function renderAvatar(snapshot, options = {})") - render_end = source.index("function applyCenterColor(color)", render_start) + render_end = source.index("function applyCenterColor(color, options = {})", render_start) render_body = source[render_start:render_end] capture_pos = render_body.index("captureAvatarRotationPhases()") replace_pos = render_body.index("avatarRoot.replaceChildren(svg);") @@ -261,7 +261,7 @@ def test_reasoning_rotation_ramp_reuses_cached_animation_handles(self): self.assertNotIn("getAnimations()", ramp_body) render_start = source.index("function renderAvatar(snapshot, options = {})") - render_end = source.index("function applyCenterColor(color)", render_start) + render_end = source.index("function applyCenterColor(color, options = {})", render_start) render_body = source[render_start:render_end] invalidate_pos = render_body.index("invalidateAvatarRotationAnimationsCache();") replace_pos = render_body.index("avatarRoot.replaceChildren(svg);") diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index f3f5d083..542203fe 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -1003,31 +1003,31 @@ async def test_sentence_loop_content_interrupts_and_arms_recovery(self): ) self.assertIn( - "JIN message 1 executed: WEB_SEARCH", + "action_1: WEB_SEARCH (", sequence_context, ) self.assertIn( ( - "JIN message 2 executed: stuck in a reasoning loop with " + "action_2: stuck in a reasoning loop with " '"* Wait, I\'ll use the search marker."' ), sequence_context, ) self.assertIn( - "JIN message 3 executed: WEB_SEARCH", + "action_3: WEB_SEARCH (", sequence_context, ) self.assertLess( - sequence_context.index("JIN message 1 executed: WEB_SEARCH"), + sequence_context.index("action_1: WEB_SEARCH ("), sequence_context.index( - "JIN message 2 executed: stuck in a reasoning loop" + "action_2: stuck in a reasoning loop" ), ) self.assertLess( sequence_context.index( - "JIN message 2 executed: stuck in a reasoning loop" + "action_2: stuck in a reasoning loop" ), - sequence_context.index("JIN message 3 executed: WEB_SEARCH"), + sequence_context.index("action_3: WEB_SEARCH ("), ) errors = [ @@ -1951,7 +1951,11 @@ async def emit( ) self.assertIn( - "JIN message 1 executed: SAVE_DELAYED_MEMORY: failed: Unrequested report", + "1. SAVE_DELAYED_MEMORY: failed: Unrequested report", + followup_prompt, + ) + self.assertIn( + "action_1: SAVE_DELAYED_MEMORY: failed: Unrequested report", followup_prompt, ) self.assertFalse( @@ -2516,6 +2520,7 @@ async def color_generator(): "colors": [ "#0000ff", ], + "context_detail": "#0000ff", }], ) self.assertEqual( @@ -2526,6 +2531,7 @@ async def color_generator(): "#0000ff", "#ff0000", ], + "context_detail": "#0000ff, #ff0000", "count": 2, }], ) @@ -2638,6 +2644,7 @@ async def color_generator(): "#0000ff", "#ff0000", ], + "context_detail": "#0000ff, #ff0000", "count": 9, }], ) @@ -2750,75 +2757,6 @@ async def color_generator(): }, ) - async def test_matching_blocker_skips_action_without_confirmation(self): - - async def save_session_generator(): - - yield { - "type": "content", - "content": "", - } - - context = SimpleNamespace( - websocket=FakeWebSocket(), - logger=FakeLogger(), - emitter=FakeEmitter(), - active_streams={}, - runtime_action_events=[], - runtime_usage_events=[], - runtime_asset_results=[], - runtime_delayed_memory_results=[], - runtime_session_action_history=[], - runtime_action_guard_confirmations={}, - delayed_memory_reports={}, - active_memory_records=[], - runtime_turn_user_message="покажи тег", - runtime_current_turn_id="turn_save_session_blocker", - runtime_turn_started_at=0, - session_id="session-1", - timestamp="2026-07-20T18:00:00", - ) - - stream = RuntimeStream( - context=context, - runtime_id=settings.SERVICE_MODEL_UID, - role="service", - context_window=settings.SERVICE_CONTEXT_WINDOW, - log_method=context.logger.log_service, - runtime_actions={ - "CAN_SAVE_SESSION": True, - }, - ) - - await stream.run( - save_session_generator() - ) - - confirmation_events = [ - event - for event in context.emitter.events - if event.get("type") - == "runtime_action_guard_confirmation" - ] - runtime_events = [ - event - for event in context.emitter.events - if event.get("type") == "runtime_action" - ] - - self.assertEqual( - confirmation_events, - [], - ) - self.assertEqual( - [event.get("status") for event in runtime_events], - ["counted", "failed", "counter_final"], - ) - self.assertEqual( - context.runtime_action_events[-1]["error"], - "behavior_contract_blocker_matched", - ) - async def test_runtime_groups_inner_and_outer_markers_from_one_message(self): async def mixed_marker_generator(): @@ -2933,14 +2871,14 @@ async def emit( self.assertIn( ( - "JIN message 1 executed: SAVE_ACTIVE_MEMORY: " + "action_1: SAVE_ACTIVE_MEMORY: " "current session context and task status, " "SAVE_DELAYED_MEMORY: failed: Unrequested report" ), sequence_context, ) self.assertNotIn( - "JIN message 2 executed:", + "action_2:", sequence_context, ) diff --git a/tests/test_runtime_todo.py b/tests/test_runtime_todo.py index d5642fdf..51319572 100644 --- a/tests/test_runtime_todo.py +++ b/tests/test_runtime_todo.py @@ -223,7 +223,7 @@ def test_context_snapshot_hides_internal_action_rules_when_todo_active(self): self.assertTrue( snapshot["hide_internal_action_rules"], ) - self.assertIn( + self.assertNotIn( "RUNTIME ACTION EXECUTION RULES:", snapshot["system_prompt"], ) diff --git a/tests/test_save_session_l3_client_contract.py b/tests/test_save_session_l3_client_contract.py deleted file mode 100644 index 753471c4..00000000 --- a/tests/test_save_session_l3_client_contract.py +++ /dev/null @@ -1,61 +0,0 @@ -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -class SaveSessionL3ClientContractTests(unittest.TestCase): - - def test_terminal_save_session_event_cannot_rearm_l3_bubble(self): - - source = ( - ROOT - / "ui" - / "static" - / "js" - / "socket" - / "runtime-actions.js" - ).read_text(encoding="utf-8") - - force_start = source.index( - "const forceCompletePendingL3 =" - ) - force_end = source.index( - "if (", - force_start, - ) - force_block = source[force_start:force_end] - - self.assertIn( - 'action === "save_session"', - force_block, - ) - self.assertIn( - "&& terminalStatus", - force_block, - ) - self.assertNotIn( - "!['", - force_block, - ) - - completed_start = source.index( - 'if (\n status === "completed"' - ) - completed_end = source.index( - "return;", - completed_start, - ) - completed_block = source[ - completed_start:completed_end - ] - - self.assertIn( - "forceCompletePendingL3,", - completed_block, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_search_flow.py b/tests/test_search_flow.py index e2ffeb2f..e270f7e3 100644 --- a/tests/test_search_flow.py +++ b/tests/test_search_flow.py @@ -753,7 +753,15 @@ async def test_search_is_model_driven_for_explicit_user_request(self): logger_text, ) self.assertIn( - f"INITIAL_SEQUENCE_INSTRUCTION: {state.user_input}", + "", + brain_client.prompts[1]["system_prompt"], + ) + self.assertIn( + f"\n {state.user_input}", + brain_client.prompts[1]["system_prompt"], + ) + self.assertIn( + "WEB_SEARCH", brain_client.prompts[1]["system_prompt"], ) self.assertNotIn( @@ -1070,8 +1078,8 @@ async def test_deep_search_marker_runs_child_search_bubbles(self): ], ) self.assertEqual( - len(deep_search_events), - 2, + [event.get("status") for event in deep_search_events], + ["started", "running", "completed"], ) self.assertFalse( any( @@ -1081,6 +1089,10 @@ async def test_deep_search_marker_runs_child_search_bubbles(self): ) self.assertEqual( deep_search_lifecycle_events[0].get("text"), + "DEEP_WEB_SEARCH", + ) + self.assertEqual( + deep_search_lifecycle_events[1].get("text"), "DEEP_WEB_SEARCH: Research blue tomato varieties.", ) self.assertEqual( diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index d05f5dc0..f8859ca4 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -287,7 +287,7 @@ def test_stream_validator_stops_repeated_sentence_sequence_with_markers(): "* *Wait*, I'll just do the search." ) assert validator.last_failure_loop_preview == ( - "* *Wait*, I'll just do the search." + "* *Actually*, I'll do:\\n* *Wait*, I'll just do the search." ) @@ -656,7 +656,7 @@ def test_stream_validator_stops_reasoning_loop_with_changing_quoted_checks(): ) assert validator.last_failure_preview == ( '*Final check of the prompt: "I respect the consistency ' - 'and reliability of my context.*The response is good.' + 'and reliability of my context.\\n*The response is good.' ) diff --git a/tests/test_ui_fix_bundle_contract.py b/tests/test_ui_fix_bundle_contract.py index baff9f7b..4204be62 100644 --- a/tests/test_ui_fix_bundle_contract.py +++ b/tests/test_ui_fix_bundle_contract.py @@ -25,7 +25,7 @@ def test_live_turn_releases_top_lock_and_autoscrolls_on_overflow(self): self.assertIn("function liveUserTurnReachedViewportBottom()", source) self.assertIn("metrics.bottomSpace <= 1", source) - self.assertIn("if (liveUserTurnReachedViewportBottom()) {", source) + self.assertIn("&& liveUserTurnReachedViewportBottom()", source) self.assertIn("releaseLiveUserTurnTopLock();", source) self.assertIn("liveTurnOverflowAutoscroll =", source) diff --git a/tests/test_update_l4_facts_message_display_contract.py b/tests/test_update_l4_facts_message_display_contract.py index 678cbe56..188ac52e 100644 --- a/tests/test_update_l4_facts_message_display_contract.py +++ b/tests/test_update_l4_facts_message_display_contract.py @@ -74,7 +74,7 @@ def test_session_actions_ui_renders_message_and_uses_it_as_hover_text(self): self.assertIn('String(part.message || "").trim()', source) self.assertIn('`: ${part.message}`', source) - self.assertIn('part.message || part.detail', source) + self.assertIn('part.message\n || part.detail', source) self.assertIn('message: part.message,', source) def test_chat_bubble_and_logger_tooltips_use_update_message(self): @@ -85,14 +85,14 @@ def test_chat_bubble_and_logger_tooltips_use_update_message(self): self.assertIn('`${getRuntimeActionDisplayName(data, action)}: `', socket_source) self.assertIn('updateL4FactsMessage\n || buildRuntimeActionDetail(', socket_source) self.assertIn('function getInternalActionUpdateL4Message(data)', logger_source) - self.assertIn('logDiv.title =\n updateL4Message;', logger_source) + self.assertIn('logDiv.title =\n updateL4Message || jinSizeHover;', logger_source) def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/js/logger/session-actions.js?v=logger-session-actions-7', source) + self.assertIn('/static/js/logger/session-actions.js?v=logger-session-actions-13', source) self.assertIn('/static/js/logger/log-entries.js?v=update-l4-message-1', source) - self.assertIn('/static/js/socket/runtime-actions.js?v=update-l4-message-1', source) + self.assertIn('/static/js/socket/runtime-actions.js?v=size-sequence-1', source) if __name__ == "__main__": diff --git a/tests/test_websocket_pending_usage.py b/tests/test_websocket_pending_usage.py index 295dfb47..94dd51a1 100644 --- a/tests/test_websocket_pending_usage.py +++ b/tests/test_websocket_pending_usage.py @@ -2,7 +2,6 @@ import asyncio from types import SimpleNamespace -from runtime.L1_memory_utils import emit_runtime_session_memory_update from runtime.action_guard import confirm_runtime_action_guards from runtime.registry import runtime_state from config_loader import ( @@ -18,7 +17,6 @@ apply_runtime_resume, build_runtime_action_guard_retry_request, apply_session_bootstrap, - arm_save_session_from_user_text, cancel_current_task, emit_runtime_action_guard_confirmation_failure, reject_when_all_models_offline, @@ -306,14 +304,9 @@ async def test_action_guard_retry_bypasses_only_matching_guard_once(self): ) wrong_action = RuntimeActionCall( - name="SAVE_SESSION", + name="SAVE_DELAYED_MEMORY", + payload="title: Second report", ) - context.runtime_action_guard_retry_consumed = False - context.runtime_action_guard_retry = { - **context.runtime_action_guard_retry, - "action": "save_delayed_memory", - "guard": "save_delayed_memory", - } context.runtime_action_guard_confirmations = {} async def reject_new_confirmation(event): @@ -332,7 +325,7 @@ async def reject_new_confirmation(event): confirmed, rejected, _, _ = await confirm_runtime_action_guards( context, (wrong_action,), - user_message="сохрани сесию", + user_message="сделай ещё один отчёт", ) self.assertEqual(confirmed, set()) @@ -390,41 +383,6 @@ async def finished(): ) - - - - - async def test_arm_save_session_prearms_without_banner(self): - - context = SimpleNamespace( - emitter=FakeEmitter(), - logger=FakeLogger(), - runtime_save_session_armed=False, - runtime_save_session_requested=False, - ) - - armed = await arm_save_session_from_user_text( - context, - "\u0441\u043e\u0445\u0440\u0430\u043d\u0438 \u0441\u0435\u0441\u0441\u0438\u044e", - ) - - self.assertTrue( - armed, - ) - self.assertTrue( - context.runtime_save_session_armed, - ) - self.assertFalse( - context.runtime_save_session_requested, - ) - self.assertFalse( - context.runtime_save_session_action_emitted, - ) - self.assertEqual( - context.emitter.events, - [], - ) - async def test_rejects_user_request_when_all_models_are_offline(self): http_client = FakeStatusHttpClient( @@ -532,22 +490,6 @@ async def test_session_bootstrap_restores_browser_memory(self): self.assertTrue( restored ) - self.assertEqual( - context.session_memory, - "decision: Resume memory work", - ) - self.assertEqual( - context.runtime_l3_session_memory, - "decision: Resume memory work", - ) - self.assertEqual( - context.runtime_session_memory_updates, - 2, - ) - self.assertEqual( - context.session_memory_source, - "browser_localStorage", - ) self.assertEqual( context.runtime_memory, "topic: restored runtime state", @@ -701,25 +643,6 @@ async def test_runtime_resume_restores_persisted_session_and_turn_counter(self): context.turn_number, 11, ) - self.assertEqual( - context.session_memory, - "decision: keep reconnect persistence", - ) - self.assertEqual( - context.runtime_l3_session_memory, - "decision: keep reconnect persistence", - ) - self.assertEqual( - context.runtime_session_memory_updates, - 5, - ) - self.assertIsNone( - context.runtime_l3_saved_runtime_snapshot_index - ) - self.assertEqual( - context.session_memory_source, - "browser_soft_reconnect", - ) self.assertEqual( context.runtime_loaded_delayed_memory_ids, [ @@ -727,7 +650,7 @@ async def test_runtime_resume_restores_persisted_session_and_turn_counter(self): ], ) - async def test_runtime_resume_can_restore_session_without_live_l1(self): + async def test_runtime_resume_ignores_removed_l3_only_payload_without_live_l1(self): context = SimpleNamespace( runtime_memory="session status: New session", @@ -739,11 +662,6 @@ async def test_runtime_resume_can_restore_session_without_live_l1(self): turn_number=0, user_message_count=0, assistant_message_count=0, - session_memory="", - runtime_l3_session_memory="", - runtime_session_memory_updates=0, - runtime_l3_saved_runtime_snapshot_index=None, - session_memory_source="", delayed_memory_reports={}, ) @@ -752,24 +670,16 @@ async def test_runtime_resume_can_restore_session_without_live_l1(self): { "type": "runtime_resume", "runtime_memory": "", - "session_memory": "decision: restore L3 only", + "session_memory": "decision: restore legacy L3 only", "session_memory_source": "browser_soft_reconnect", "session_memory_updates": 2, }, ) - self.assertTrue(restored) - self.assertEqual( - context.session_memory, - "decision: restore L3 only", - ) - self.assertEqual( - context.runtime_l3_session_memory, - "decision: restore L3 only", - ) + self.assertFalse(restored) self.assertEqual( - context.runtime_session_memory_updates, - 2, + context.runtime_memory, + "session status: New session", ) async def test_runtime_resume_hydrates_active_memory_lifecycle_counters(self): @@ -890,27 +800,6 @@ async def test_session_bootstrap_hydrates_active_memory_elapsed_counter_floor(se context.active_memory_records[0], ) - async def test_runtime_session_memory_update_is_not_browser_persisted_by_default(self): - - context = SimpleNamespace( - emitter=FakeEmitter(), - runtime_l3_session_memory="topic: restored but not saved", - session_memory="", - session_memory_source="browser_localStorage", - runtime_session_memory_updates=1, - ) - - await emit_runtime_session_memory_update( - context - ) - - self.assertEqual( - context.emitter.events[-1]["type"], - "runtime_session_memory_update", - ) - self.assertFalse( - context.emitter.events[-1]["persist"], - ) async def test_pending_brain_usage_emits_before_stream_start(self): From 077fb10f7fc5f9869cbb00f3fb1e77499d09be73 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:30:05 +0300 Subject: [PATCH 103/168] chore: ui - add current bootstrap timestamp to restore prompt - keep archived restore dialogue/reasoning/L1 as one authority - render bounded restored dialogue tail with clean reasoning - bootstrap runtime status from window config --- docs/JIN_ARCHITECTURE.md | 6 +- docs/JIN_DECISIONS.md | 4 +- rules/brain_context_builder.py | 19 ++++- rules/runtime.py | 8 +- tests/test_anonymous_mode_client_contract.py | 4 + tests/test_archived_session_restore.py | 90 +++++++++++++++++++- tests/test_current_runtime_settings.py | 62 +++++++++++++- tests/test_live_session_checkpoint.py | 9 +- tests/test_runtime_panel_client_contract.py | 2 +- ui/static/js/runtime/runtime-panel.js | 21 +---- ui/static/js/session-restore.js | 42 ++++----- ui/static/js/status.js | 7 +- ui/templates/index.html | 10 +-- utils/session_restore.py | 90 +++++++++++++++----- 14 files changed, 287 insertions(+), 87 deletions(-) diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index 2dab1e25..e3541281 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -464,7 +464,7 @@ The client rebuilds the tail through the existing chat primitives. It keeps a re Archived restore is a distinct path: 1. HTTP endpoint `/api/sessions/{session_id}/restore` builds payload from logs via `utils/session_restore.py`. -2. Browser renders archived visible messages and sends a `session_bootstrap`/restore payload. +2. Browser renders the same bounded three-USER-move tail carried by the restore payload and sends a `session_bootstrap`/restore payload. Saved reasoning is attached to its owning JIN turn after archive-file metadata is removed; later visible JIN-only restore rows remain in order. 3. Backend sets `runtime_session_restore_priming` and stages historical resource IDs/metadata. 4. A hidden `archived_session_resume` Brain tick receives restore-specific context. 5. The restore prompt includes the newest complete visible dialog pairs plus a bounded recent reasoning dump. @@ -472,8 +472,12 @@ Archived restore is a distinct path: 7. `BrainNode` consumes the staged restore envelope and replays those resources through the normal runtime-action dispatcher. 8. The WebSocket tail performs defensive cleanup only; it must not apply the same resources a second time. +The restore instruction is stamped at prompt-build time with the current timezone-aware bootstrap time immediately before `Current session was bootstrapped in a browser tab!`. Archived USER timestamps remain historical dialogue metadata and are never reused as the bootstrap time. + Constants in the current source limit the restore reasoning dump to two recent reasoning items with a per-item character cap. Restored visible dialogue uses the normal three-pair limit and, like ordinary recent-message context, does not impose a per-message character cap. +The RESTORE endpoint owns archived dialogue, reasoning, and L1 for explicit URL checkout. A same-session browser checkpoint may contribute a newer room/avatar and Session Actions projection, but it cannot overwrite only `recent_turns`, reasoning, or runtime memory and thereby create a mixed conversation source. The restore instruction names `RESTORED_SESSION_DIALOG` as the newest conversational authority; L1 remains background state and may legitimately predate the final archived turn. + `utils/session_restore.py` still understands historical `SAVE_SESSION` labels in archived logs. That is restore compatibility, not proof of a current `SAVE_SESSION` runtime action. ### 9.5 Timestamp invariant diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index bcba48f5..b5210e7b 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -484,7 +484,9 @@ The first bootstrap color uses the one 2-second avatar-and-scene transition. Lat **Status:** Accepted / implemented -Normal bootstrap renders the three newest real USER moves with JIN/reasoning where present, then places the current-session date divider and starts the live viewport there. USER-only turns remain visible without an empty JIN bubble. Explicit archived restore uses its own history renderer and must not duplicate this tail. +Normal bootstrap renders the three newest real USER moves with JIN/reasoning where present, then places the current-session date divider and starts the live viewport there. USER-only turns remain visible without an empty JIN bubble. Explicit archived restore uses its own history renderer but projects the same bounded USER-owned tail, keeps later visible JIN-only continuation rows, and must not duplicate the normal-bootstrap tail. Its reasoning bubbles contain the reasoning body, not archive-file headers. + +For explicit URL restore, the server archive owns dialogue, reasoning, and L1 as one causal bundle. A same-session browser checkpoint can recover presentation state (room/avatar and Session Actions), but cannot replace individual conversation fields inside that bundle. `RESTORED_SESSION_DIALOG` is the newest conversation authority during the one-shot priming turn; restored L1 is background and may be one update behind the final visible turn. **Why:** the user can scroll slightly upward for immediate continuity while the current response begins from a clean, stable boundary. diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index f1fa07ff..866818d0 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -1171,9 +1171,24 @@ def build_brain_context( # This remains the first restore-specific prompt block, immediately # below optional CURRENT_RUNTIME_SETTINGS. It is a hidden restore tick, - # not a user request. + # not a user request. Timestamp the bootstrap itself; archived message + # timestamps describe the old dialogue and must not be used here. + bootstrap_timestamp = ( + datetime.now() + .astimezone() + .isoformat(timespec="seconds") + ) + restore_message = SESSION_RESTORE_MESSAGE.replace( + "Current session was bootstrapped in a browser tab!", + ( + f"{bootstrap_timestamp}\n" + "Current session was bootstrapped in a browser tab!" + ), + 1, + ) + prompt_parts.append( - SESSION_RESTORE_MESSAGE + restore_message ) enabled_actions = get_enabled_runtime_actions( diff --git a/rules/runtime.py b/rules/runtime.py index e4b7c629..1a4ee804 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -36,9 +36,11 @@ SESSION_RESTORE_MESSAGE = ( "\n" "Current session was bootstrapped in a browser tab!\n" - "Think deep and fresh, extract all vibe and re-enable the archived conversation as fluently as possible.\n" - "Respond briefly and naturally and make it easy for the user to continue from exactly where we left off and move on.\n" - "Answer in language of user message in PREVIOUS_CHAT_MESSAGES.\n" + "RESTORED_SESSION_DIALOG is the authoritative newest visible conversation state.\n" + "RUNTIME_MEMORY is background state and may predate the last restored dialogue turn; never treat its keys as a newer user message.\n" + "Continue from the latest USER entry in RESTORED_SESSION_DIALOG and answer in that user's language.\n" + "Think deep and fresh, extract the conversation's tone and resume it fluently.\n" + "Respond briefly and naturally without summarizing or re-introducing the restored exchange unless the user asks.\n" "" ) RUNTIME_ACTIONS_RULES = "" diff --git a/tests/test_anonymous_mode_client_contract.py b/tests/test_anonymous_mode_client_contract.py index 0542d9ef..190a47b0 100644 --- a/tests/test_anonymous_mode_client_contract.py +++ b/tests/test_anonymous_mode_client_contract.py @@ -53,6 +53,10 @@ def test_anonymous_switches_are_exposed_by_config_and_template(self): '"anonymousMode": anonymous_mode_config', template_source, ) + self.assertNotIn('"runtimeStatus": runtime_status', template_source) + self.assertNotIn('"runtimeConfig": runtime_config', template_source) + self.assertNotIn('"useServiceAsBrain": use_service_as_brain', template_source) + self.assertNotIn('"formatResponse": format_response', template_source) self.assertIn("build_anonymous_mode_config", app_source) self.assertIn( '"ENABLE_DEFAULT_ANONYMOUS_MODE"', diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index 2811029b..1431a998 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -8,7 +8,6 @@ from agent.nodes.brain import replay_session_restore_resource_actions from rules.brain_context_builder import build_brain_context from rules.runtime import ( - SESSION_RESTORE_MESSAGE, SESSION_RESTORE_REASONING_CHAR_LIMIT, SESSION_RESTORE_REASONING_COUNT, ) @@ -290,6 +289,83 @@ def test_payload_restores_dialog_reasoning_runtime_and_actions(self): self.assertEqual(payload["tool_results"][0]["kind"], "search") self.assertIn("People recommend cold noir music.", payload["tool_results"][0]["result"]) + def test_url_restore_ui_uses_same_bounded_tail_and_clean_reasoning(self): + root = Path(tempfile.mkdtemp()) + session_id = "bounded-url-restore" + session_dir = root / "2026-08-16" / session_id + reasoning_dir = session_dir / "reasoning" + reasoning_dir.mkdir(parents=True) + + entries = [] + for turn in range(1, 6): + turn_id = f"turn_{turn:06d}" + entries.extend([ + { + "ts": f"2026-08-16T20:{turn:02d}:00+03:00", + "turn": turn, + "turn_id": turn_id, + "session_id": session_id, + "role": "user", + "text": f"user {turn}", + }, + { + "ts": f"2026-08-16T20:{turn:02d}:30+03:00", + "turn": turn, + "turn_id": turn_id, + "session_id": session_id, + "role": "jin", + "text": f"jin {turn}", + "reasoning_path": ( + f"/logs/2026-08-16/{session_id}/reasoning/" + f"session_{turn_id}.txt" + ), + }, + ]) + (reasoning_dir / f"session_{turn_id}.txt").write_text( + "captured_at: now\n" + f"turn_id: {turn_id}\n\n" + "--- REASONING ---\n" + f"reasoning {turn}", + encoding="utf-8", + ) + + (session_dir / "session.jsonl").write_text( + "\n".join( + json.dumps(item, ensure_ascii=False) + for item in entries + ), + encoding="utf-8", + ) + + payload = build_archived_session_restore_payload( + session_id, + root=root, + ) + + self.assertEqual( + [item["text"] for item in payload["messages"]], + [ + "user 3", "jin 3", + "user 4", "jin 4", + "user 5", "jin 5", + ], + ) + self.assertEqual( + payload["messages"][3]["reasoning"], + "reasoning 4", + ) + self.assertNotIn( + "captured_at", + payload["messages"][3]["reasoning"], + ) + self.assertNotIn("captured_at", payload["dialog_context"]) + self.assertNotIn("captured_at", payload["restore_reasoning_dump"]) + self.assertEqual(payload["previous_reasoning"], "reasoning 5") + self.assertEqual( + [turn["user"] for turn in payload["recent_turns"]], + ["user 3", "user 4", "user 5"], + ) + def test_runtime_color_event_restores_color_and_structured_action(self): root, session_id = self._build_fixture() dialog_path = ( @@ -1082,7 +1158,17 @@ def test_restore_prompt_is_clean_one_shot_context(self): runtime_actions={"CAN_WEB_SEARCH": True}, ) - self.assertTrue(prompt.startswith(SESSION_RESTORE_MESSAGE)) + self.assertTrue( + prompt.startswith("\n") + ) + self.assertIn( + "\nCurrent session was bootstrapped in a browser tab!\n", + prompt, + ) + self.assertIn( + "RESTORED_SESSION_DIALOG is the authoritative newest visible conversation state.", + prompt, + ) self.assertIn("EXACT OLD FLOW", prompt) self.assertIn("RAW REASONING", prompt) self.assertIn("Old report [ id: abc123 ]", prompt) diff --git a/tests/test_current_runtime_settings.py b/tests/test_current_runtime_settings.py index 6c7afe12..478a99a0 100644 --- a/tests/test_current_runtime_settings.py +++ b/tests/test_current_runtime_settings.py @@ -1,10 +1,10 @@ import unittest +from datetime import datetime from types import SimpleNamespace from unittest.mock import patch from rules import brain_context_builder from rules.brain_context_builder import build_brain_context -from rules.runtime import SESSION_RESTORE_MESSAGE class CurrentRuntimeSettingsTests(unittest.TestCase): @@ -76,14 +76,68 @@ def test_runtime_settings_stay_before_restore_priming(self): include_runtime_action_instructions=False, ) - expected_prefix = ( + settings_prefix = ( "\n" "restore_mode: enabled\n" "\n\n" - f"{SESSION_RESTORE_MESSAGE}" ) self.assertTrue( - prompt.startswith(expected_prefix) + prompt.startswith( + settings_prefix + + "\n" + ) + ) + restore_lines = prompt[len(settings_prefix):].splitlines()[:3] + self.assertIsNotNone( + datetime.fromisoformat(restore_lines[1]).utcoffset() + ) + self.assertEqual( + restore_lines[2], + "Current session was bootstrapped in a browser tab!", + ) + + def test_restore_priming_prefixes_current_bootstrap_timestamp(self): + context = self._context( + restore_priming=True + ) + context.runtime_restored_session_dialog = ( + '\n' + 'first\n' + 'reply\n' + 'latest\n' + '' + ) + + before = datetime.now().astimezone() + prompt = build_brain_context( + context=context, + runtime_actions={}, + include_runtime_action_instructions=False, + ) + after = datetime.now().astimezone() + + prefix_lines = prompt.splitlines()[:3] + self.assertEqual( + prefix_lines[0], + "", + ) + self.assertEqual( + prefix_lines[2], + "Current session was bootstrapped in a browser tab!", + ) + bootstrap_time = datetime.fromisoformat(prefix_lines[1]) + self.assertIsNotNone(bootstrap_time.utcoffset()) + self.assertLessEqual( + before.replace(microsecond=0), + bootstrap_time, + ) + self.assertLessEqual( + bootstrap_time, + after, + ) + self.assertNotEqual( + prefix_lines[1], + "2026-08-26T00:32:28+03:00", ) diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index 4b77fe08..aef81bbc 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -211,14 +211,19 @@ def test_completed_turn_persists_server_and_room_as_one_snapshot(self): self.assertIn("runtimeSession.persistLiveSessionCheckpoint", block) self.assertNotIn("window.JinPanels.persistRoomStateNow()", block) - def test_restore_merges_checkpoint_session_actions(self): + def test_url_restore_merges_only_checkpoint_presentation_state(self): source = (ROOT / "ui" / "static" / "js" / "session-restore.js").read_text(encoding="utf-8") start = source.index("function mergeLatestVisualCheckpoint(") end = source.index("function restoreVisualState(", start) block = source[start:end] self.assertIn("const candidates = [];", block) - self.assertIn("Object.assign(merged, selected.snapshot);", block) + self.assertIn('"session_actions",', block) + self.assertIn('"room_state",', block) + self.assertNotIn("Object.assign(merged, selected.snapshot);", block) + self.assertNotIn("selected.record.runtime_memory", block) + self.assertNotIn('"recent_turns",', block) + self.assertNotIn('"previous_reasoning",', block) self.assertNotIn("actionSnapshot", block) diff --git a/tests/test_runtime_panel_client_contract.py b/tests/test_runtime_panel_client_contract.py index 7feda028..5b852c54 100644 --- a/tests/test_runtime_panel_client_contract.py +++ b/tests/test_runtime_panel_client_contract.py @@ -261,7 +261,7 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/js/runtime/runtime-panel.js?v=context-bar-resize-1", + "/static/js/runtime/runtime-panel.js?v=context-bar-resize-1&status-bootstrap=1", source, ) self.assertIn( diff --git a/ui/static/js/runtime/runtime-panel.js b/ui/static/js/runtime/runtime-panel.js index 5982fa90..4b020716 100644 --- a/ui/static/js/runtime/runtime-panel.js +++ b/ui/static/js/runtime/runtime-panel.js @@ -57,26 +57,7 @@ function readInitialRuntimeConfig() { - if (window.jinRuntimeConfig) { - return window.jinRuntimeConfig; - } - - const configTemplate = - document.getElementById( - "jin-runtime-config" - ); - - if (!configTemplate) { - return {}; - } - - try { - return JSON.parse( - configTemplate.textContent || "{}" - ); - } catch (error) { - return {}; - } + return window.jinRuntimeConfig || {}; } diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index 5e871fbd..c88860e6 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -460,25 +460,29 @@ return merged; } - Object.assign(merged, selected.snapshot); - - const runtimeMemory = - String(selected.record.runtime_memory || "").trim(); - if (runtimeMemory) { - merged.runtime_memory = runtimeMemory; - merged.runtime_memory_updates = Number( - selected.record.runtime_memory_updates || 0 - ); - } - if ( - selected.record.runtime_snapshot - && typeof selected.record.runtime_snapshot === "object" - && !Array.isArray(selected.record.runtime_snapshot) - ) { - merged.runtime_snapshot = { - ...selected.record.runtime_snapshot, - }; - } + // The RESTORE endpoint owns archived dialogue, reasoning and L1. A local + // checkpoint for the same session may contain a newer room/logger + // projection, but it must never replace only one side of the archived + // conversation and create a mixed bootstrap prompt. + [ + "session_actions", + "current_jin_color", + "current_jin_size", + "current_jin_position", + "current_jin_speed", + "current_jin_collapsed", + "current_window_size", + "room_state", + ].forEach(fieldName => { + if ( + Object.prototype.hasOwnProperty.call( + selected.snapshot, + fieldName + ) + ) { + merged[fieldName] = selected.snapshot[fieldName]; + } + }); return merged; } diff --git a/ui/static/js/status.js b/ui/static/js/status.js index 4d72d2ba..a2791324 100644 --- a/ui/static/js/status.js +++ b/ui/static/js/status.js @@ -1217,12 +1217,7 @@ brainStatusButton?.addEventListener( void loadBehaviorContract(); -void updateRuntime({ - showChecking: !( - window.jinRuntimeConfig - && window.jinRuntimeConfig.runtimeStatus - ), -}); +void updateRuntime(); window.addEventListener( "focus", diff --git a/ui/templates/index.html b/ui/templates/index.html index 81f00fa0..ef0f151c 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -496,11 +496,7 @@ @@ -512,11 +508,11 @@ - + - + @@ -531,7 +527,7 @@ - + diff --git a/utils/session_restore.py b/utils/session_restore.py index fe077520..6cfdca5a 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -165,6 +165,22 @@ def _load_dialog(path: Path) -> list[dict]: return entries +def _dialog_turn_key(entry: dict, index: int) -> object: + try: + turn_number = int(entry.get("turn", 0) or 0) + except (TypeError, ValueError): + turn_number = 0 + + turn_id = str(entry.get("turn_id", "") or "").strip() + if turn_number > 0: + return ("turn", turn_number) + if turn_id: + return ("turn_id", turn_id) + + # A legacy row without a turn identity cannot be paired safely. + return ("row", index) + + def _recent_restored_dialog_pairs(entries: list[dict]) -> list[tuple[dict, dict]]: """Return the newest complete USER/JIN pairs in chronological order.""" turns: dict[object, dict[str, dict]] = {} @@ -179,20 +195,7 @@ def _recent_restored_dialog_pairs(entries: list[dict]) -> list[tuple[dict, dict] if not text: continue - try: - turn_number = int(entry.get("turn", 0) or 0) - except (TypeError, ValueError): - turn_number = 0 - - turn_id = str(entry.get("turn_id", "") or "").strip() - if turn_number > 0: - key: object = ("turn", turn_number) - elif turn_id: - key = ("turn_id", turn_id) - else: - # Legacy rows without a turn identity cannot be paired safely. - # Keep them isolated rather than accidentally joining unrelated text. - key = ("row", index) + key = _dialog_turn_key(entry, index) if key not in turns: turns[key] = {} @@ -209,6 +212,43 @@ def _recent_restored_dialog_pairs(entries: list[dict]) -> list[tuple[dict, dict] return complete_pairs[-RECENT_MESSAGES_MAX_PAIRS:] +def _recent_visible_dialog_entries(entries: list[dict]) -> list[dict]: + """Project the same bounded USER-owned tail used by continuation state.""" + + user_turn_keys = [] + seen = set() + for index, entry in enumerate(entries): + if str(entry.get("role", "")).strip().lower() != "user": + continue + if not ( + str(entry.get("text", "") or "").strip() + or bool(entry.get("attachments", []) or []) + ): + continue + + key = _dialog_turn_key(entry, index) + if key in seen: + continue + seen.add(key) + user_turn_keys.append(key) + + selected_keys = user_turn_keys[-RECENT_MESSAGES_MAX_PAIRS:] + if not selected_keys: + return [] + + first_selected_key = selected_keys[0] + for index, entry in enumerate(entries): + if ( + str(entry.get("role", "")).strip().lower() == "user" + and _dialog_turn_key(entry, index) == first_selected_key + ): + # Keep later JIN-only continuation rows too: an earlier archived + # restore greeting is a visible move even though it has no USER row. + return entries[index:] + + return [] + + def _append_restored_dialog_entry(lines: list[str], entry: dict) -> None: role = str(entry.get("role", "")).strip().lower() tag = "USER" if role == "user" else "JIN" @@ -225,7 +265,9 @@ def _append_restored_reasoning_entry( ) -> None: turn_id = str(jin_entry.get("turn_id", "") or "").strip() reasoning = _crop_restore_reasoning( - reasoning_by_turn_id.get(turn_id, "") + _extract_reasoning_body( + reasoning_by_turn_id.get(turn_id, "") + ) ) if not reasoning: return @@ -455,7 +497,9 @@ def _build_restore_reasoning_dump( turn_id = str(entry.get("turn_id", "") or "").strip() reasoning = _crop_restore_reasoning( - reasoning_by_turn_id.get(turn_id, "") + _extract_reasoning_body( + reasoning_by_turn_id.get(turn_id, "") + ) ) if not reasoning: continue @@ -1383,7 +1427,9 @@ def build_archived_session_restore_payload( if not latest_jin_text: latest_jin_text = str(entry.get("text", "") or "").strip() turn_id = str(entry.get("turn_id", "") or "").strip() - latest_reasoning = reasoning_by_turn_id.get(turn_id, "") + latest_reasoning = _extract_reasoning_body( + reasoning_by_turn_id.get(turn_id, "") + ) if latest_reasoning: break @@ -1487,7 +1533,7 @@ def build_archived_session_restore_payload( archive_tail_at = str(entry.get("ts", "") or "").strip() ui_messages = [] - for entry in visible_entries: + for entry in _recent_visible_dialog_entries(visible_entries): role = str(entry.get("role", "")).strip().lower() if role not in {"user", "jin", "assistant", "brain", "service"}: continue @@ -1501,7 +1547,13 @@ def build_archived_session_restore_payload( "attachments": entry.get("attachments", []) or [], "delayed_memory_ids": entry.get("delayed_memory_ids", []) or [], "active_memory_ids": entry.get("active_memory_ids", []) or [], - "reasoning": reasoning_by_turn_id.get(turn_id, ""), + "reasoning": ( + _extract_reasoning_body( + reasoning_by_turn_id.get(turn_id, "") + ) + if role != "user" + else "" + ), }) runtime_mode = trusted_values.get("RUNTIME_MODE", "BRAIN").strip().upper() From 1481bda2d90b412f8f4ebf06ee6ce3a01ec8c998 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:28:03 +0300 Subject: [PATCH 104/168] chore: Live Avatar - cleanup svg elements from runtime metadata --- LIVE_AVATAR.md | 2 + .../test_delayed_memory_highlight_contract.py | 7 +- ..._runtime_avatar_l4_ring_client_contract.py | 28 +- ...ime_avatar_node_payload_client_contract.py | 82 +++ ui/static/js/runtime/runtime-avatar.js | 666 +++++++++--------- ui/templates/index.html | 2 +- 6 files changed, 429 insertions(+), 358 deletions(-) create mode 100644 tests/test_runtime_avatar_node_payload_client_contract.py diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index 0ce11a12..0ba6301f 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -65,6 +65,8 @@ Other visual/state knobs: The radar avatar is drawn as a single SVG inside `#jin-runtime-avatar`. A full render replaces the old SVG, but state-only changes are deliberately synchronized in place where possible so a pin/load transition does not unnecessarily reshuffle or restart visual geometry. +The SVG is intentionally only the visual projection. Rich matching/link payload (`citationIdentity`, citation key/text, reference aliases, delayed/L4 relation ids, file links, and the L4 archive angle) lives in the private `avatarNodeState` `WeakMap` in `runtime-avatar.js`, not in `data-*` attributes. Keep only lightweight canonical ids and synchronization hooks in the DOM; do not serialize full values or derived payload back into SVG nodes. Ring-wide metabolism/glow variables belong on the ring group rather than being duplicated on every memory dash. + The stack is built in this order: 1. SVG definitions: gradients and glow filters. diff --git a/tests/test_delayed_memory_highlight_contract.py b/tests/test_delayed_memory_highlight_contract.py index e8737e15..5cf3595b 100644 --- a/tests/test_delayed_memory_highlight_contract.py +++ b/tests/test_delayed_memory_highlight_contract.py @@ -63,7 +63,12 @@ def test_delayed_memory_uses_one_base_hue(self): avatar, ) self.assertIn("glowColor: color", avatar) - self.assertIn("const nextGlowColor = nextColor;", avatar) + self.assertIn( + 'getMemorySignalColors("delayed", overallColor)', + avatar, + ) + self.assertNotIn("data-metabolism-base-color", avatar) + self.assertNotIn("data-metabolism-glow-color", avatar) def test_direct_dm_links_all_own_l4_facts_but_secondary_does_not(self): avatar = AVATAR_JS.read_text(encoding="utf-8") diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 5f9b89ce..51e9e25e 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -45,16 +45,18 @@ def test_l4_facts_are_rendered_as_reference_aware_memory_dashes(self): self.assertIn("id,\n key,", source) self.assertIn("referenceAliases: record.referenceAliases", source) self.assertIn("dot: record.archived", source) - self.assertIn( - '"data-delayed-memory-fact-ids":', - source, - ) - self.assertIn( - '"data-delayed-memory-anchor-fact-ids":', - source, - ) - self.assertIn('"data-l4-fact-ids":', source) - self.assertIn('"data-avatar-memory-angle": options.angle', source) + self.assertIn("const avatarNodeState = new WeakMap();", source) + self.assertIn("delayedMemoryFactIds:", source) + self.assertIn("normalizeL4FactIds(options.delayedMemoryFactIds)", source) + self.assertIn("delayedMemoryAnchorFactIds:", source) + self.assertIn("normalizeL4FactIds(options.delayedMemoryAnchorFactIds)", source) + self.assertIn("l4FactIds:", source) + self.assertIn("normalizeL4FactIds(options.l4FactIds)", source) + self.assertIn("nodeState.avatarMemoryAngle = Number(options.angle);", source) + self.assertNotIn('"data-delayed-memory-fact-ids"', source) + self.assertNotIn('"data-delayed-memory-anchor-fact-ids"', source) + self.assertNotIn('"data-l4-fact-ids"', source) + self.assertNotIn('"data-avatar-memory-angle"', source) self.assertIn('class: "jin-avatar-center"', source) self.assertIn("function syncMemorySignalLayer(kind", source) self.assertIn("function syncL4MemoryArchiveState()", source) @@ -206,10 +208,8 @@ def test_paused_active_memory_keeps_its_ring_slot_but_draws_no_dash(self): "const slotDegrees = 360 / records.length;", source, ) - self.assertIn( - "title: `Active memory ${record.index + 1}: ${record.value || record.text}`", - source, - ) + self.assertNotIn('createSvgElement("title")', source) + self.assertNotIn("appendTitle(", source) def test_memory_ring_changes_can_sync_without_avatar_refresh(self): source = RUNTIME_JS.read_text(encoding="utf-8") diff --git a/tests/test_runtime_avatar_node_payload_client_contract.py b/tests/test_runtime_avatar_node_payload_client_contract.py new file mode 100644 index 00000000..7cfbe879 --- /dev/null +++ b/tests/test_runtime_avatar_node_payload_client_contract.py @@ -0,0 +1,82 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +AVATAR_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" +) +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimeAvatarNodePayloadClientContractTests(unittest.TestCase): + + def test_rich_avatar_payload_is_kept_out_of_svg_datasets(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertIn("const avatarNodeState = new WeakMap();", source) + self.assertIn("function setAvatarNodeState(node, values)", source) + self.assertIn("runtimeLineIdentity:", source) + self.assertIn("runtimeLineKey:", source) + self.assertIn("runtimeLineText:", source) + self.assertIn("referenceAliases", source) + self.assertIn("delayedMemoryFactIds:", source) + self.assertIn("delayedMemoryAnchorFactIds:", source) + self.assertIn("l4FactIds:", source) + self.assertIn("linkedDelayedMemoryIds:", source) + self.assertIn("nodeState.avatarMemoryAngle = Number(options.angle);", source) + + for attribute in ( + "data-runtime-line-key", + "data-runtime-line-text", + "data-runtime-line-identity", + "data-memory-reference-aliases", + "data-l4-fact-ids", + "data-delayed-memory-fact-ids", + "data-delayed-memory-anchor-fact-ids", + "data-avatar-memory-angle", + "data-linked-delayed-memory-ids", + "data-metabolism-channel", + "data-metabolism-base-color", + "data-metabolism-glow-color", + ): + self.assertNotIn(attribute, source) + + def test_svg_keeps_only_lightweight_identity_and_sync_hooks(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + for attribute in ( + "data-avatar-memory-hover-id", + "data-active-memory-id", + "data-delayed-memory-id", + "data-l4-fact-id", + "data-file-id", + "data-runtime-line-index", + "data-avatar-rotation-key", + ): + self.assertIn(attribute, source) + + self.assertIn("function getAvatarPayloadNodes(svg)", source) + self.assertIn("getAvatarNodeState(node)", source) + self.assertIn("getAvatarMemoryReferenceAliases(node)", source) + + def test_dead_svg_titles_and_per_record_metabolism_payload_are_gone(self): + source = AVATAR_JS.read_text(encoding="utf-8") + + self.assertNotIn('createSvgElement("title")', source) + self.assertNotIn("appendTitle(", source) + self.assertIn("function getMemorySignalColors(kind, overallColor)", source) + self.assertIn("setMemoryDashGlowVariables(\n ring,", source) + self.assertNotIn( + 'dashGroup.style.setProperty(\n "--jin-avatar-memory-dot-opacity"', + source, + ) + + def test_avatar_cache_key_is_bumped_for_node_state_cleanup(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("avatar-node-state=1", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index eb637c68..20571d5e 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -11,8 +11,6 @@ "jin:delayed-memory-report-active"; const MEMORY_REFERENCE_HIGHLIGHT_EVENT = "jin:memory-reference-highlight"; - const MEMORY_REFERENCE_ALIAS_DATASET_KEY = - "memoryReferenceAliases"; const CENTER = 180; const INNER_RING_SCALE = 0.90; @@ -155,6 +153,35 @@ const memoryReferenceHighlightState = { persistentText: "", }; + // Keep matching/link payload in JS. The SVG is a visual projection, not a + // second serialized copy of runtime/L4/delayed/file state. WeakMap also + // lets rebuilt rings release their payload together with detached nodes. + const avatarNodeState = new WeakMap(); + + function getAvatarNodeState(node, create = false) { + if (!node) { + return null; + } + + let state = avatarNodeState.get(node) || null; + + if (!state && create) { + state = Object.create(null); + avatarNodeState.set(node, state); + } + + return state; + } + + function setAvatarNodeState(node, values) { + if (!node || !values || typeof values !== "object") { + return null; + } + + const state = getAvatarNodeState(node, true); + Object.assign(state, values); + return state; + } function clamp(value, min, max) { return Math.max(min, Math.min(max, Number(value) || 0)); @@ -214,7 +241,6 @@ dashGroup.dataset.activeMemoryId ); const path = dashGroup.querySelector("path"); - const dot = dashGroup.querySelector(".jin-avatar-memory-dot"); if (path) { path.setAttribute( @@ -222,16 +248,6 @@ opacity.toFixed(3) ); } - if (dot) { - dot.setAttribute( - "fill-opacity", - opacity.toFixed(3) - ); - } - dashGroup.style.setProperty( - "--jin-avatar-memory-dot-opacity", - opacity.toFixed(3) - ); }); } @@ -292,32 +308,17 @@ const normalizedAliases = normalizeMemoryReferenceAliases(aliases); + const state = getAvatarNodeState(node, true); - if (!normalizedAliases.length) { - delete node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY]; - return; - } - - node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY] = - JSON.stringify(normalizedAliases); + state.referenceAliases = normalizedAliases; } function getAvatarMemoryReferenceAliases(node) { - if (!node || !node.dataset) { - return []; - } - - const raw = node.dataset[MEMORY_REFERENCE_ALIAS_DATASET_KEY]; - - if (!raw) { - return []; - } + const state = getAvatarNodeState(node); - try { - return normalizeMemoryReferenceAliases(JSON.parse(raw)); - } catch (error) { - return []; - } + return normalizeMemoryReferenceAliases( + state && state.referenceAliases + ); } function getAvatarMemoryReferenceDisplayKey(value) { @@ -1034,12 +1035,6 @@ deactivateReasoningMotion(); } - function appendTitle(node, text) { - const title = createSvgElement("title"); - title.textContent = String(text || ""); - node.appendChild(title); - } - function normalizeL4FactIds(value) { const source = Array.isArray(value) @@ -1108,20 +1103,6 @@ return factIds; } - function serializeL4FactIds(value) { - const factIds = - normalizeL4FactIds(value); - - return factIds.length - ? factIds.join(",") - : null; - } - - function addL4FactIds(target, value) { - normalizeL4FactIds(value) - .forEach(factId => target.add(factId)); - } - function l4FactIdSetsIntersect(left, right) { if (!left || !right || !left.size || !right.size) { return false; @@ -1167,14 +1148,6 @@ return ids; } - function serializeShortRuntimeIds(value) { - const ids = normalizeShortRuntimeIds(value); - - return ids.length - ? ids.join(",") - : null; - } - function shortRuntimeIdSetsIntersect(left, right) { if (!left || !right || !left.size || !right.size) { return false; @@ -1851,12 +1824,6 @@ ]), citationText: normalizeRuntimeCitationIdentity(lineText), - citationIdentity: - activeMemoryId - ? normalizeRuntimeCitationIdentity( - `active:${activeMemoryId}` - ) - : "", }; }) .filter(record => record && record.citationText); @@ -2013,8 +1980,6 @@ key, getAvatarMemoryReferenceDisplayKey(key), ]), - citationText: - normalizeRuntimeCitationIdentity(lineText), }; }) .filter(Boolean) @@ -2136,20 +2101,7 @@ ); const startAngle = options.angle - arcDegrees / 2; const endAngle = options.angle + arcDegrees / 2; - const color = options.color; - const glowColor = options.glowColor || color; - const metabolismChannel = - String(options.metabolismChannel || "").trim(); - const renderedColor = - getMetabolismTintedColor( - color, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - glowColor, - metabolismChannel - ); + const renderedColor = options.color; const isDot = Boolean(options.dot); const dotRadius = isDot @@ -2179,38 +2131,39 @@ const dashGroup = createSvgElement("g", { class: classNames.join(" "), "data-avatar-memory-hover-id": options.avatarMemoryHoverId || null, - "data-runtime-line-key": options.citationKey || null, - "data-runtime-line-text": options.citationText || null, - "data-runtime-line-identity": options.citationIdentity || null, "data-active-memory-id": options.activeMemoryId || null, "data-delayed-memory-id": options.delayedMemoryId || null, - "data-delayed-memory-fact-ids": - serializeL4FactIds(options.delayedMemoryFactIds), - "data-delayed-memory-anchor-fact-ids": - serializeL4FactIds(options.delayedMemoryAnchorFactIds), "data-l4-fact-id": options.l4FactId || null, - "data-l4-fact-ids": serializeL4FactIds(options.l4FactIds), - "data-avatar-memory-angle": options.angle, - "data-metabolism-channel": metabolismChannel || null, - "data-metabolism-base-color": color, - "data-metabolism-glow-color": glowColor, }); - setAvatarMemoryReferenceAliases( - dashGroup, - options.referenceAliases - ); + const nodeState = Object.create(null); - setMemoryDashGlowVariables( - dashGroup, - renderedGlowColor, - layout.strokeWidth + 0.75, - dotRadius - ); + if (options.citationKey) { + nodeState.runtimeLineKey = options.citationKey; + } + if (options.citationText) { + nodeState.runtimeLineText = options.citationText; + } + if (options.citationIdentity) { + nodeState.runtimeLineIdentity = options.citationIdentity; + } + if (options.kind === "delayed") { + nodeState.delayedMemoryFactIds = + normalizeL4FactIds(options.delayedMemoryFactIds); + nodeState.delayedMemoryAnchorFactIds = + normalizeL4FactIds(options.delayedMemoryAnchorFactIds); + } + if (options.kind === "l4") { + nodeState.l4FactIds = + normalizeL4FactIds(options.l4FactIds); + nodeState.avatarMemoryAngle = Number(options.angle); + } + + setAvatarNodeState(dashGroup, nodeState); - appendTitle( + setAvatarMemoryReferenceAliases( dashGroup, - options.title + options.referenceAliases ); dashGroup.appendChild(createSvgElement("path", { @@ -2225,11 +2178,6 @@ if (isDot) { const dotPoint = polarPoint(layout.radius, options.angle); - - dashGroup.style.setProperty( - "--jin-avatar-memory-dot-opacity", - Number(options.opacity || 0.28).toFixed(2) - ); dashGroup.appendChild(createSvgElement("circle", { class: "jin-avatar-memory-dot", cx: dotPoint.x.toFixed(3), @@ -2306,82 +2254,83 @@ } function refreshRenderedMetabolismTints() { - avatarRoot.querySelectorAll( - ".jin-avatar-memory-dash[data-metabolism-channel]" - ).forEach((dashGroup) => { - const channel = - String(dashGroup.dataset.metabolismChannel || "").trim(); - const baseColor = - String(dashGroup.dataset.metabolismBaseColor || "").trim(); - const glowColor = - String(dashGroup.dataset.metabolismGlowColor || baseColor).trim(); + const channel = getMetabolismChannel(); - if (!channel || !baseColor) { - return; - } - - const renderedColor = - getMetabolismTintedColor( - baseColor, - channel - ); - const renderedGlowColor = - getMetabolismTintedColor( - glowColor || baseColor, - channel - ); - const path = - dashGroup.querySelector("path"); + if (!channel) { + return; + } - if (path) { - path.setAttribute( - "stroke", - renderedColor - ); - } + const overallColor = + avatarRoot.style + .getPropertyValue("--jin-avatar-overall-color") + .trim() || DEFAULT_RING_COLOR; + + Object.entries(MEMORY_RING_LAYOUT) + .forEach(([kind, layout]) => { + const ring = + avatarRoot.querySelector( + `.jin-avatar-memory-ring-${kind}` + ); - dashGroup.querySelectorAll( - ".jin-avatar-memory-dot" - ).forEach((dot) => { - dot.setAttribute( - "fill", - renderedColor - ); - }); + if (!ring) { + return; + } - let layout = MEMORY_RING_LAYOUT.active; - if (dashGroup.classList.contains("jin-avatar-memory-dash-delayed")) { - layout = MEMORY_RING_LAYOUT.delayed; - } else if (dashGroup.classList.contains("jin-avatar-memory-dash-l4")) { - layout = MEMORY_RING_LAYOUT.l4; - } + const colors = + getMemorySignalColors(kind, overallColor); + const renderedColor = + getMetabolismTintedColor( + colors.color, + channel + ); + const renderedGlowColor = + getMetabolismTintedColor( + colors.glowColor, + channel + ); - const dotRadius = - dashGroup.classList.contains("is-memory-dot") - ? getMemoryDotRadius(layout) - : 0; + ring.querySelectorAll( + ".jin-avatar-memory-dash > path" + ).forEach((path) => { + path.setAttribute( + "stroke", + renderedColor + ); + }); + ring.querySelectorAll( + ".jin-avatar-memory-dot" + ).forEach((dot) => { + dot.setAttribute( + "fill", + renderedColor + ); + }); - setMemoryDashGlowVariables( - dashGroup, - renderedGlowColor, - layout.strokeWidth + 0.75, - dotRadius - ); - }); + setMemoryDashGlowVariables( + ring, + renderedGlowColor, + layout.strokeWidth + 0.75, + kind === "l4" + ? getMemoryDotRadius(layout) + : 0 + ); + }); avatarRoot.querySelectorAll( - ".jin-avatar-file-dot[data-metabolism-channel]" + ".jin-avatar-file-dot" ).forEach((dotGroup) => { - const channel = - String(dotGroup.dataset.metabolismChannel || "").trim(); + const active = Boolean( + dotGroup.classList.contains("is-memory-pinned") + || dotGroup.classList.contains("is-context-loaded") + ); const baseColor = - String(dotGroup.dataset.metabolismBaseColor || "").trim(); + active + ? FILE_RING_ACTIVE_COLOR + : FILE_RING_COLOR; const glowColor = - String(dotGroup.dataset.metabolismGlowColor || baseColor).trim(); - - if (!channel || !baseColor) { - return; - } + active + ? FILE_RING_ACTIVE_COLOR + : FILE_RING_LAYOUT.glowColor; const renderedColor = getMetabolismTintedColor( @@ -2390,7 +2339,7 @@ ); const renderedGlowColor = getMetabolismTintedColor( - glowColor || baseColor, + glowColor, channel ); const core = @@ -2435,6 +2384,38 @@ }; } + function getMemorySignalColors(kind, overallColor) { + if (kind === "active") { + return { + color: mixColors( + ACTIVE_MEMORY_RING_COLOR, + overallColor, + 0.18 + ), + glowColor: ACTIVE_MEMORY_RING_COLOR, + }; + } + + if (kind === "l4") { + return { + color: mixColors( + L4_MEMORY_RING_COLOR, + overallColor, + 0.08 + ), + glowColor: L4_MEMORY_RING_COLOR, + }; + } + + const color = + mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); + + return { + color, + glowColor: color, + }; + } + function appendMemorySignalRing(svg, records, layout, kind, overallColor) { if (!records.length) { return; @@ -2462,6 +2443,36 @@ kind === "delayed" ? 0.94 : 0.82 ); ring.appendChild(reasoningMotion.layer); + const ringColors = + getMemorySignalColors(kind, overallColor); + const metabolismChannel = getMetabolismChannel(); + const renderedColor = + getMetabolismTintedColor( + ringColors.color, + metabolismChannel + ); + const renderedGlowColor = + getMetabolismTintedColor( + ringColors.glowColor, + metabolismChannel + ); + const dotRadius = + kind === "l4" + ? getMemoryDotRadius(layout) + : 0; + + setMemoryDashGlowVariables( + ring, + renderedGlowColor, + layout.strokeWidth + 0.75, + dotRadius + ); + if (kind === "l4") { + ring.style.setProperty( + "--jin-avatar-memory-dot-opacity", + "0.26" + ); + } const slotDegrees = 360 / records.length; const arcDegrees = getMemoryDashArcDegrees( @@ -2477,13 +2488,6 @@ return; } - const color = - mixColors( - ACTIVE_MEMORY_RING_COLOR, - overallColor, - 0.18 - ); - appendMemoryDashSegment( reasoningMotion.content, layout, @@ -2491,31 +2495,19 @@ kind, angle, arcDegrees, - metabolismChannel: getMetabolismChannel(index, kind), - color, - glowColor: ACTIVE_MEMORY_RING_COLOR, + color: renderedColor, opacity: getActiveMemoryMetabolicOpacity(record.id), avatarMemoryHoverId: record.avatarMemoryHoverId, activeMemoryId: record.id, citationKey: normalizeRuntimeCitationIdentity(record.key), - citationText: record.citationText, - citationIdentity: record.citationIdentity, referenceAliases: record.referenceAliases, - title: `Active memory ${record.index + 1}: ${record.value || record.text}`, } ); return; } if (kind === "l4") { - const color = - mixColors( - L4_MEMORY_RING_COLOR, - overallColor, - 0.08 - ); - appendMemoryDashSegment( reasoningMotion.content, layout, @@ -2523,21 +2515,17 @@ kind, angle, arcDegrees, - metabolismChannel: getMetabolismChannel(index, kind), - color, - glowColor: L4_MEMORY_RING_COLOR, + color: renderedColor, opacity: record.archived ? 0.26 : 0.52, archived: record.archived, dot: record.archived, avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.key), - citationText: record.citationText, citationIdentity: record.citationIdentity, l4FactId: record.id, l4FactIds: record.l4FactIds, referenceAliases: record.referenceAliases, - title: `L4 ${record.id} · ${record.key}: ${record.value}`, } ); return; @@ -2548,9 +2536,6 @@ const active = pinned || contextLoaded; // Delayed memory has one base hue. Direct state and references are // expressed only through the two CSS highlight tiers. - const color = - mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); - appendMemoryDashSegment( reasoningMotion.content, layout, @@ -2558,9 +2543,7 @@ kind, angle, arcDegrees, - metabolismChannel: getMetabolismChannel(index, kind), - color, - glowColor: color, + color: renderedColor, opacity: active ? 0.82 : 0.36, pinned, contextLoaded, @@ -2571,7 +2554,6 @@ delayedMemoryFactIds: record.linkedFactIds, delayedMemoryAnchorFactIds: record.anchorFactIds, referenceAliases: record.referenceAliases, - title: `${record.title}${record.summary ? `: ${record.summary}` : ""}`, } ); }); @@ -2623,7 +2605,7 @@ ? FILE_RING_ACTIVE_COLOR : FILE_RING_LAYOUT.glowColor; const metabolismChannel = - getMetabolismChannel(index, "file"); + getMetabolismChannel(); const renderedColor = getMetabolismTintedColor( color, @@ -2638,14 +2620,24 @@ class: "jin-avatar-file-dot", "data-avatar-memory-hover-id": record.avatarMemoryHoverId || null, "data-file-id": record.id, - "data-linked-delayed-memory-ids": serializeShortRuntimeIds(record.linkedReportIds), - "data-runtime-line-key": normalizeRuntimeCitationIdentity(record.id), - "data-runtime-line-text": normalizeRuntimeCitationIdentity([record.name, record.contextPath].filter(Boolean).join(" · ")), - "data-metabolism-channel": metabolismChannel || null, - "data-metabolism-base-color": color, - "data-metabolism-glow-color": glowColor, }); + setAvatarNodeState( + dotGroup, + { + linkedDelayedMemoryIds: + normalizeShortRuntimeIds(record.linkedReportIds), + runtimeLineKey: + normalizeRuntimeCitationIdentity(record.id), + runtimeLineText: + normalizeRuntimeCitationIdentity( + [record.name, record.contextPath] + .filter(Boolean) + .join(" · ") + ), + } + ); + if (record.pinned) { dotGroup.classList.add("is-memory-pinned"); } @@ -2666,10 +2658,6 @@ dotGroup, renderedGlowColor ); - appendTitle( - dotGroup, - `${record.name} · ${record.id}` - ); dotGroup.appendChild(createSvgElement("circle", { class: "jin-avatar-file-dot-core", cx: point.x.toFixed(3), @@ -2836,32 +2824,27 @@ dashGroup.classList.contains("is-context-loaded"); const nextActive = nextPinned || nextContextLoaded; - const nextColor = - mixColors(DELAYED_MEMORY_RING_COLOR, overallColor, 0.12); + const colors = + getMemorySignalColors("delayed", overallColor); dashGroup.classList.toggle( "is-memory-pinned", nextPinned ); - const metabolismChannel = - String(dashGroup.dataset.metabolismChannel || "").trim(); - const nextGlowColor = nextColor; + const metabolismChannel = getMetabolismChannel(); const renderedColor = getMetabolismTintedColor( - nextColor, + colors.color, metabolismChannel ); const renderedGlowColor = getMetabolismTintedColor( - nextGlowColor, + colors.glowColor, metabolismChannel ); - dashGroup.dataset.metabolismBaseColor = nextColor; - dashGroup.dataset.metabolismGlowColor = nextGlowColor; - setMemoryDashGlowVariables( - dashGroup, + dashGroup.closest(".jin-avatar-memory-ring-delayed") || dashGroup, renderedGlowColor, MEMORY_RING_LAYOUT.delayed.strokeWidth + 0.75 ); @@ -2927,16 +2910,13 @@ const overallColor = avatarRoot.style.getPropertyValue("--jin-avatar-overall-color").trim() || DEFAULT_RING_COLOR; - const nextColor = - mixColors( - L4_MEMORY_RING_COLOR, - overallColor, - 0.08 - ); + const colors = + getMemorySignalColors("l4", overallColor); const dotRadius = getMemoryDotRadius(MEMORY_RING_LAYOUT.l4); + const state = getAvatarNodeState(dashGroup); const angle = - Number(dashGroup.dataset.avatarMemoryAngle); + Number(state && state.avatarMemoryAngle); if (nextArchived && !Number.isFinite(angle)) { return false; @@ -2951,27 +2931,23 @@ nextArchived ); - const metabolismChannel = - String(dashGroup.dataset.metabolismChannel || "").trim(); + const metabolismChannel = getMetabolismChannel(); const renderedColor = getMetabolismTintedColor( - nextColor, + colors.color, metabolismChannel ); const renderedGlowColor = getMetabolismTintedColor( - L4_MEMORY_RING_COLOR, + colors.glowColor, metabolismChannel ); - dashGroup.dataset.metabolismBaseColor = nextColor; - dashGroup.dataset.metabolismGlowColor = L4_MEMORY_RING_COLOR; - setMemoryDashGlowVariables( - dashGroup, + dashGroup.closest(".jin-avatar-memory-ring-l4") || dashGroup, renderedGlowColor, MEMORY_RING_LAYOUT.l4.strokeWidth + 0.75, - nextArchived ? dotRadius : 0 + dotRadius ); path.classList.toggle( @@ -2990,9 +2966,6 @@ if (!nextArchived) { dashGroup.querySelectorAll(".jin-avatar-memory-dot") .forEach(dot => dot.remove()); - dashGroup.style.removeProperty( - "--jin-avatar-memory-dot-opacity" - ); return true; } @@ -3011,10 +2984,6 @@ dashGroup.appendChild(dot); } - dashGroup.style.setProperty( - "--jin-avatar-memory-dot-opacity", - nextOpacity.toFixed(2) - ); dot.setAttribute("cx", dotPoint.x.toFixed(3)); dot.setAttribute("cy", dotPoint.y.toFixed(3)); dot.setAttribute("r", dotRadius.toFixed(2)); @@ -3059,10 +3028,19 @@ return; } - dashGroup.dataset.delayedMemoryFactIds = - serializeL4FactIds(record.linkedFactIds) || ""; - dashGroup.dataset.delayedMemoryAnchorFactIds = - serializeL4FactIds(record.anchorFactIds) || ""; + setAvatarNodeState( + dashGroup, + { + delayedMemoryFactIds: + normalizeL4FactIds(record.linkedFactIds), + delayedMemoryAnchorFactIds: + normalizeL4FactIds(record.anchorFactIds), + } + ); + setAvatarMemoryReferenceAliases( + dashGroup, + record.referenceAliases + ); dashGroup.classList.toggle( "is-context-loaded", Boolean(record.loaded) @@ -3117,14 +3095,17 @@ return; } - dashGroup.dataset.l4FactIds = - serializeL4FactIds(record.l4FactIds) || ""; - dashGroup.dataset.runtimeLineKey = - normalizeRuntimeCitationIdentity(record.key); - dashGroup.dataset.runtimeLineText = - record.citationText || ""; - dashGroup.dataset.runtimeLineIdentity = - record.citationIdentity || ""; + setAvatarNodeState( + dashGroup, + { + l4FactIds: + normalizeL4FactIds(record.l4FactIds), + runtimeLineKey: + normalizeRuntimeCitationIdentity(record.key), + runtimeLineIdentity: + record.citationIdentity || "", + } + ); setAvatarMemoryReferenceAliases( dashGroup, record.referenceAliases @@ -3239,16 +3220,21 @@ ); dotGroup.dataset.avatarMemoryHoverId = record.avatarMemoryHoverId || ""; - dotGroup.dataset.linkedDelayedMemoryIds = - serializeShortRuntimeIds(record.linkedReportIds) || ""; - dotGroup.dataset.runtimeLineKey = - normalizeRuntimeCitationIdentity(record.id); - dotGroup.dataset.runtimeLineText = - normalizeRuntimeCitationIdentity( - [record.name, record.contextPath] - .filter(Boolean) - .join(" · ") - ); + setAvatarNodeState( + dotGroup, + { + linkedDelayedMemoryIds: + normalizeShortRuntimeIds(record.linkedReportIds), + runtimeLineKey: + normalizeRuntimeCitationIdentity(record.id), + runtimeLineText: + normalizeRuntimeCitationIdentity( + [record.name, record.contextPath] + .filter(Boolean) + .join(" · ") + ), + } + ); setAvatarMemoryReferenceAliases( dotGroup, @@ -3262,8 +3248,7 @@ active ? FILE_RING_ACTIVE_COLOR : FILE_RING_LAYOUT.glowColor; - const metabolismChannel = - String(dotGroup.dataset.metabolismChannel || "").trim(); + const metabolismChannel = getMetabolismChannel(); const renderedColor = getMetabolismTintedColor( baseColor, @@ -3275,19 +3260,11 @@ metabolismChannel ); - dotGroup.dataset.metabolismBaseColor = baseColor; - dotGroup.dataset.metabolismGlowColor = glowColor; - setFileDotGlowVariables( dotGroup, renderedGlowColor ); - const title = dotGroup.querySelector("title"); - if (title) { - title.textContent = `${record.name} · ${record.id}`; - } - if (core) { core.setAttribute( "fill", @@ -3652,16 +3629,6 @@ "pointer-events": "none", }); - appendTitle( - markerGroup, - `${record.key} · ${isNew ? "new runtime line" : "runtime line changed"}` - + ( - isNew - ? "" - : ` · ${Math.round(ratio * 100)}%` - ) - ); - if (!isNew) { markerGroup.appendChild(createSvgElement("circle", { class: "jin-avatar-runtime-change-marker-ring", @@ -3754,10 +3721,15 @@ orbitGroup.dataset.avatarMemoryHoverId = record.avatarMemoryHoverId; } - orbitGroup.dataset.runtimeLineKey = - normalizeRuntimeCitationIdentity(record.key); - orbitGroup.dataset.runtimeLineText = - normalizeRuntimeCitationIdentity(record.text); + setAvatarNodeState( + orbitGroup, + { + runtimeLineKey: + normalizeRuntimeCitationIdentity(record.key), + runtimeLineText: + normalizeRuntimeCitationIdentity(record.text), + } + ); if (record.activeMemoryId) { orbitGroup.dataset.activeMemoryId = String(record.activeMemoryId).trim().toLowerCase(); @@ -3767,11 +3739,6 @@ record.referenceAliases ); - appendTitle( - orbitGroup, - `${record.key} · ${record.length} chars · ${Math.round(diffPercent)}% diff` - ); - const strokeWidth = 0.48 + random() * 2.25; const circumference = 2 * Math.PI * record.radius; const dashLength = 1 + random() * Math.max(2, record.radius * 0.14); @@ -3911,12 +3878,14 @@ ].filter(Boolean)); } - function getAvatarDashFactIdSet(node, datasetKey) { + function getAvatarDashFactIdSet(node, stateKey) { + const state = getAvatarNodeState(node); + return new Set( normalizeL4FactIds( - node && node.dataset - ? node.dataset[datasetKey] - : "" + state + ? state[stateKey] + : [] ) ); } @@ -3945,10 +3914,10 @@ + ".jin-avatar-memory-dash-delayed.is-context-loaded" ) ).forEach((node) => { - addL4FactIds( - factIds, - node.dataset.delayedMemoryFactIds - ); + getAvatarDashFactIdSet( + node, + "delayedMemoryFactIds" + ).forEach(factId => factIds.add(factId)); }); getFocusedMemoryDashNodes(svg) @@ -3958,10 +3927,10 @@ ) )) .forEach((node) => { - addL4FactIds( - factIds, - node.dataset.delayedMemoryFactIds - ); + getAvatarDashFactIdSet( + node, + "delayedMemoryFactIds" + ).forEach(factId => factIds.add(factId)); }); return factIds; @@ -3977,10 +3946,10 @@ ) )) .forEach((node) => { - addL4FactIds( - factIds, - node.dataset.l4FactIds - ); + getAvatarDashFactIdSet( + node, + "l4FactIds" + ).forEach(factId => factIds.add(factId)); }); return factIds; @@ -4119,12 +4088,14 @@ Array.from( svg.querySelectorAll(".jin-avatar-file-dot") ).forEach((node) => { + const state = getAvatarNodeState(node); + node.classList.toggle( "is-delayed-memory-linked-hit", shortRuntimeIdSetsIntersect( new Set( normalizeShortRuntimeIds( - node.dataset.linkedDelayedMemoryIds + state && state.linkedDelayedMemoryIds ) ), focusedDelayedIds @@ -4153,9 +4124,7 @@ const activeHoverIds = getActiveAvatarMemoryHoverIds(); - svg.querySelectorAll( - ".jin-avatar-orbit, .jin-avatar-counter-orbit, .jin-avatar-memory-dash, .jin-avatar-file-dot" - ).forEach((node) => { + getAvatarPayloadNodes(svg).forEach((node) => { const hoverId = String(node.dataset.avatarMemoryHoverId || ""); const matched = Boolean( @@ -4267,6 +4236,21 @@ }; } + function getAvatarPayloadNodes(svg) { + if (!svg) { + return []; + } + + return Array.from( + svg.querySelectorAll( + ".jin-avatar-orbit[data-runtime-line-index], " + + ".jin-avatar-counter-orbit[data-runtime-line-index], " + + ".jin-avatar-memory-dash, " + + ".jin-avatar-file-dot" + ) + ); + } + function applyThinkRuntimeCitationGlow() { const svg = avatarRoot.querySelector("svg"); @@ -4277,17 +4261,14 @@ const activeIdentities = getActiveThinkRuntimeCitationIdentitySets(); - const citationNodes = Array.from( - svg.querySelectorAll( - ".jin-avatar-orbit[data-runtime-line-key], .jin-avatar-counter-orbit[data-runtime-line-key], .jin-avatar-memory-dash, .jin-avatar-file-dot[data-runtime-line-key]" - ) - ); + const citationNodes = getAvatarPayloadNodes(svg); const lineKeyUsage = new Map(); citationNodes.forEach((node) => { + const state = getAvatarNodeState(node); const lineKey = normalizeRuntimeCitationIdentity( - node.dataset.runtimeLineKey + state && state.runtimeLineKey ); if (!lineKey) { @@ -4301,9 +4282,10 @@ }); citationNodes.forEach((orbitGroup) => { + const state = getAvatarNodeState(orbitGroup); const lineIdentity = normalizeRuntimeCitationIdentity( - orbitGroup.dataset.runtimeLineIdentity + state && state.runtimeLineIdentity ); const activeMemoryId = normalizeShortRuntimeIds( @@ -4311,11 +4293,11 @@ )[0] || ""; const lineKey = normalizeRuntimeCitationIdentity( - orbitGroup.dataset.runtimeLineKey + state && state.runtimeLineKey ); const lineText = normalizeRuntimeCitationIdentity( - orbitGroup.dataset.runtimeLineText + state && state.runtimeLineText ); const exactTextMatch = Boolean( lineText @@ -4361,6 +4343,8 @@ return ""; } + const state = getAvatarNodeState(node); + const activeMemoryId = normalizeShortRuntimeIds( node.dataset.activeMemoryId @@ -4399,7 +4383,7 @@ const lineIdentity = normalizeRuntimeCitationIdentity( - node.dataset.runtimeLineIdentity + state && state.runtimeLineIdentity ); if (lineIdentity) { @@ -4407,8 +4391,8 @@ } return [ - node.dataset.runtimeLineKey, - node.dataset.runtimeLineText, + state && state.runtimeLineKey, + state && state.runtimeLineText, node.dataset.avatarMemoryHoverId, ] .map(normalizeRuntimeCitationIdentity) @@ -4459,9 +4443,7 @@ } const sourceText = getActiveMemoryReferenceText(); - const recordNodes = Array.from( - svg.querySelectorAll("[data-memory-reference-aliases]") - ); + const recordNodes = getAvatarPayloadNodes(svg); const aliasUsage = buildAvatarMemoryReferenceAliasUsage(recordNodes); const canonicalActiveMemoryIds = new Set( diff --git a/ui/templates/index.html b/ui/templates/index.html index ef0f151c..d08dc318 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -511,7 +511,7 @@ - + From 4c4bac095fc499615eebd1a5ee34b417c9175f42 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:03:19 +0300 Subject: [PATCH 105/168] chore: ui - fix for context report modal stack - fix for invalid session id restore - fix delayed memory id citation highlight --- tests/test_archived_session_restore.py | 10 ++ tests/test_attached_files_client_contract.py | 2 +- ...delayed_memory_citation_client_contract.py | 46 ++++++++ tests/test_delayed_memory_client_contract.py | 17 +++ ui/static/css/chat.css | 15 +++ ui/static/css/runtime-memory.css | 2 +- ui/static/css/theme-win95.css | 12 ++ ui/static/js/runtime/runtime-memory-view.js | 15 +++ ui/static/js/session-restore.js | 53 ++++++++- ui/static/js/socket.js | 37 ++++++ ui/static/js/think-citations.js | 110 +++++++++++++++++- ui/templates/index.html | 14 +-- 12 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 tests/test_delayed_memory_citation_client_contract.py diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index 1431a998..4ebd7507 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -1380,6 +1380,13 @@ def test_delayed_memory_session_id_opens_archived_restore_tab(self): index.index("socket.js"), ) self.assertIn("jinArchivedSessionRestoreReady", restore_script) + self.assertIn("clearRestoreSessionParam", restore_script) + self.assertIn('url.searchParams.delete(\n "restore_session"', restore_script) + self.assertIn("window.history.replaceState", restore_script) + self.assertIn("status === 404", restore_script) + self.assertIn("jinArchivedSessionRestoreFailure", restore_script) + self.assertIn('"[SESSION ERROR]"', restore_script) + self.assertIn("session: ${failedSessionId}", restore_script) self.assertIn("jin-session-restore-divider", restore_script) self.assertIn("formatRestoreBoundaryTimestamp", restore_script) self.assertIn("appendRestoreBoundary(payload)", restore_script) @@ -1397,6 +1404,9 @@ def test_delayed_memory_session_id_opens_archived_restore_tab(self): ) self.assertIn("runtimeMemory.saved_at", runtime_session) self.assertIn('type: "archived_session_resume"', socket_script) + self.assertIn("logArchivedRestoreFallbackSession", socket_script) + self.assertIn("jinArchivedSessionRestoreFailure", socket_script) + self.assertIn("loaded session\\nsession: ${loadedSessionId}", socket_script) self.assertNotIn( 'bootstrap.archived_session_restore !== true', socket_script, diff --git a/tests/test_attached_files_client_contract.py b/tests/test_attached_files_client_contract.py index dd589e3f..8836bfee 100644 --- a/tests/test_attached_files_client_contract.py +++ b/tests/test_attached_files_client_contract.py @@ -84,7 +84,7 @@ def test_files_panel_shows_file_count_and_reuses_100px_image_hover_preview(): assert 'String(record.id || "")' in runtime_view assert "hoverPreviewMaxPx: 100" in runtime_view assert "window.bindJinAttachmentHoverPreview" in attachments - assert "/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1&delayed-fact-paste=2&numeric-fact-order=1&l4-fact-age=1&hold-delete-1&modal-title-editor=1&active-citations=4&runtime-memory-title=2&lazy-rows=5&hover-title=1&row-payload=1&l4-citation-gate=1&delayed-pin-link-isolation=1&delayed-highlight-contract=1&active-memory-activity-order=1&l4-two-line-layout=1&l4-value-fill=1&l4-priority-bubble=1" in index + assert "/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1&delayed-fact-paste=2&numeric-fact-order=1&l4-fact-age=1&hold-delete-1&modal-title-editor=1&active-citations=4&runtime-memory-title=2&lazy-rows=5&hover-title=1&row-payload=1&l4-citation-gate=1&delayed-pin-link-isolation=1&delayed-highlight-contract=1&active-memory-activity-order=1&l4-two-line-layout=1&l4-value-fill=1&l4-priority-bubble=1&delayed-id-citations=1" in index assert "/static/js/chat-attachments.js?v=attached-files-3-attachment-modal-id-1&hold-delete-1&runtime-action-preview-2" in index diff --git a/tests/test_delayed_memory_citation_client_contract.py b/tests/test_delayed_memory_citation_client_contract.py new file mode 100644 index 00000000..d727a966 --- /dev/null +++ b/tests/test_delayed_memory_citation_client_contract.py @@ -0,0 +1,46 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +THINK = ROOT / "ui" / "static" / "js" / "think-citations.js" +VIEW = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +CHAT_CSS = ROOT / "ui" / "static" / "css" / "chat.css" +INDEX = ROOT / "ui" / "templates" / "index.html" + + +class DelayedMemoryCitationClientContractTests(unittest.TestCase): + def test_delayed_report_id_is_an_exact_reasoning_citation_candidate(self): + source = THINK.read_text(encoding="utf-8") + + self.assertIn("function getDelayedMemoryCitationRecords()", source) + self.assertIn('"delayed",\n `delayedMemory[${record.id}]`', source) + self.assertIn('citationType: "delayed_memory_citation"', source) + self.assertIn('["runtime", "active", "delayed", "l4"]', source) + self.assertIn('match.sourceType === "delayed"', source) + self.assertIn('? "delayed"', source) + + def test_delayed_report_row_exposes_same_stable_citation_identity(self): + source = VIEW.read_text(encoding="utf-8") + + self.assertIn('`delayed:${reportId}`', source) + self.assertIn("runtimeMemoryLineIdentity:", source) + self.assertIn("runtimeMemoryLineKey:", source) + self.assertIn("normalizeRuntimeCitationIdentity(reportId)", source) + + def test_delayed_reasoning_citation_has_layer_specific_style(self): + css = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn(".think-citation-delayed.exact", css) + self.assertIn("rgba(103, 232, 249, 0.98)", css) + + def test_citation_assets_are_cache_bumped(self): + source = INDEX.read_text(encoding="utf-8") + + self.assertIn("runtime-memory-view.js?v=", source) + self.assertIn("think-citations.js?v=", source) + self.assertGreaterEqual(source.count("delayed-id-citations=1"), 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 671620e9..9aaeb7c3 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -966,6 +966,23 @@ def test_session_snapshot_history_and_loaded_ids_are_persisted(self): session_source, ) + def test_delayed_memory_report_modal_stays_above_context_modal(self): + css_source = ( + ROOT / "ui" / "static" / "css" / "runtime-memory.css" + ).read_text(encoding="utf-8") + index_source = ( + ROOT / "ui" / "templates" / "index.html" + ).read_text(encoding="utf-8") + + self.assertIn( + ".delayed-memory-report-modal {\n z-index: 60 !important;\n}", + css_source, + ) + self.assertIn( + "&report-modal-stack=1", + index_source, + ) + if __name__ == "__main__": unittest.main() diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 258eff09..d76d0602 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -725,6 +725,21 @@ body:not(.theme-win95) .jin-think-content.is-collapsed::after { text-shadow: 0 0 6px rgba(249, 115, 22, 0.17); } +.jin-think-content.has-rule-highlights .think-citation-delayed.exact { + color: rgba(103, 232, 249, 0.98); + text-shadow: 0 0 10px rgba(34, 211, 238, 0.38); +} + +.jin-think-content.has-rule-highlights .think-citation-delayed.near { + color: rgba(103, 232, 249, 0.88); + text-shadow: 0 0 8px rgba(34, 211, 238, 0.28); +} + +.jin-think-content.has-rule-highlights .think-citation-delayed.compressed { + color: rgba(103, 232, 249, 0.78); + text-shadow: 0 0 6px rgba(34, 211, 238, 0.19); +} + .jin-think-content.has-rule-highlights .think-citation-l4.exact { color: rgba(129, 230, 217, 0.98); text-shadow: 0 0 10px rgba(45, 212, 191, 0.38); diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index ae268d83..ea512e7a 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -894,7 +894,7 @@ } .delayed-memory-report-modal { - z-index: 60; + z-index: 60 !important; } .delayed-memory-modal-section > .delayed-memory-modal-fields { diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 94748322..c3400fc9 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -1091,6 +1091,18 @@ body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-runtime. color: #168041; } +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-delayed.exact { + color: #007f8f; +} + +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-delayed.near { + color: #0a8795; +} + +body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-delayed.compressed { + color: #208c97; +} + body.theme-win95 .jin-think-content.has-rule-highlights .think-citation-session.exact { color: #5f22b3; } diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 292776b2..96ed97c5 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -4743,6 +4743,21 @@ "delayed", reportId || report._storage_key ); + setRuntimeMemoryRowState( + row, + { + runtimeMemoryLineIdentity: + reportId + ? normalizeRuntimeCitationIdentity( + `delayed:${reportId}` + ) + : "", + runtimeMemoryLineKey: + normalizeRuntimeCitationIdentity(reportId), + runtimeMemoryLineText: + normalizeRuntimeCitationIdentity(hoverTitle), + } + ); setMemoryReferenceAliases( row, collectMemoryRecordReferenceAliases( diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index c88860e6..cb49d8d5 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -17,6 +17,26 @@ return; } + function clearRestoreSessionParam() { + const url = new URL( + window.location.href + ); + + if (!url.searchParams.has("restore_session")) { + return; + } + + url.searchParams.delete( + "restore_session" + ); + + window.history.replaceState( + window.history.state, + "", + `${url.pathname}${url.search}${url.hash}` + ); + } + const chatHistory = document.getElementById( "chat-history" @@ -818,9 +838,12 @@ ); if (!response.ok) { - throw new Error( + const error = new Error( `session restore failed: ${response.status}` ); + error.status = response.status; + error.sessionId = sourceSessionId; + throw error; } const payload = @@ -865,15 +888,39 @@ window.jinArchivedSessionRestoreReady = restoreArchivedSession() .catch((error) => { + const status = Number( + error && error.status + || 0 + ); + const failedSessionId = String( + error && error.sessionId + || sourceSessionId + || "" + ).trim(); + + if (status === 404) { + clearRestoreSessionParam(); + window.jinArchivedSessionRestoreFailure = { + requested_session_id: failedSessionId, + status, + fallback_logged: false, + }; + } + console.error( "[SESSION RESTORE]", + failedSessionId, error ); if (typeof window.appendLog === "function") { + const statusLine = status + ? `\nstatus: ${status}` + : ""; + window.appendLog( - "[SESSION]", - `restore failed: ${error.message || error}` + "[SESSION ERROR]", + `restore failed\nsession: ${failedSessionId}${statusLine}` ); } diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index 0d384416..b05b9afd 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -587,6 +587,39 @@ function requestArchivedSessionResume( } +function logArchivedRestoreFallbackSession( + bootstrap +) { + const failure = + window.jinArchivedSessionRestoreFailure; + + if ( + !failure + || failure.fallback_logged + || !bootstrap + ) { + return; + } + + const loadedSessionId = + String( + bootstrap.source_session_id + || "" + ).trim(); + + if (!loadedSessionId) { + return; + } + + failure.fallback_logged = true; + + appendLog( + "[SESSION]", + `loaded session\nsession: ${loadedSessionId}` + ); +} + + function handleSocketMessage(event) { /** @type {SocketMessage} */ @@ -728,6 +761,10 @@ async function handleSocketOpen() { window.getPersistedSessionBootstrap(); if (bootstrap) { + logArchivedRestoreFallbackSession( + bootstrap + ); + if ( window.JinRuntime && window.JinRuntime.runtime diff --git a/ui/static/js/think-citations.js b/ui/static/js/think-citations.js index c942a4a2..28a34c15 100644 --- a/ui/static/js/think-citations.js +++ b/ui/static/js/think-citations.js @@ -39,6 +39,15 @@ : ""; } + function normalizeDelayedMemoryId(value) { + const normalized = + String(value || "").trim().toLowerCase(); + + return /^[a-z0-9]{6}$/.test(normalized) + ? normalized + : ""; + } + function normalizeActiveMemoryKey(value) { const normalized = String(value || "").trim().toLowerCase(); @@ -183,6 +192,62 @@ .filter(Boolean); } + function getDelayedMemoryCitationRecords() { + const runtimeApi = + window.JinRuntime + && window.JinRuntime.runtime; + const reports = + runtimeApi + && typeof runtimeApi.getDelayedMemoryReports === "function" + ? runtimeApi.getDelayedMemoryReports() + : {}; + + if ( + !reports + || typeof reports !== "object" + || Array.isArray(reports) + ) { + return []; + } + + return Object.entries(reports) + .map(([storageKey, report]) => { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return null; + } + + const id = + normalizeDelayedMemoryId(report._storage_key) + || normalizeDelayedMemoryId(report.id) + || normalizeDelayedMemoryId(storageKey); + + if (!id) { + return null; + } + + const title = + String(report.title || "") + .replace(/\s+/g, " ") + .trim(); + const summary = + String(report.summary || "") + .replace(/\s+/g, " ") + .trim(); + + return { + id, + title, + summary, + identity: `delayed:${id}`, + }; + }) + .filter(Boolean); + } + function getCurrentActiveMemoryIds() { return new Set( getActiveMemoryCitationRecords() @@ -384,6 +449,13 @@ return 2; } + if ( + match + && match.sourceType === "delayed" + ) { + return 1; + } + if ( match && match.sourceType === "l4" @@ -505,8 +577,10 @@ ? "RUNTIME" : match.sourceType === "active" ? "ACTIVE" - : match.sourceType === "l4" - ? "L4" + : match.sourceType === "delayed" + ? "DELAYED" + : match.sourceType === "l4" + ? "L4" : match.sourceType === "session" ? "SESSION" : "RULE"; @@ -528,8 +602,10 @@ ? "runtime" : match.sourceType === "active" ? "active" - : match.sourceType === "l4" - ? "l4" + : match.sourceType === "delayed" + ? "delayed" + : match.sourceType === "l4" + ? "l4" : match.sourceType === "session" ? "session" : "rule"; @@ -973,6 +1049,7 @@ function fastCitationSourcePriority(candidate) { if (candidate && candidate.sourceType === "active") return 3; if (candidate && candidate.sourceType === "runtime") return 2; + if (candidate && candidate.sourceType === "delayed") return 1; if (candidate && candidate.sourceType === "l4") return 1; return 0; } @@ -1111,6 +1188,29 @@ }); }); + getDelayedMemoryCitationRecords().forEach((record) => { + const lineText = [ + record.id, + record.title, + record.summary, + ].filter(Boolean).join(": "); + + addLine( + "delayed", + `delayedMemory[${record.id}]`, + lineText, + { + id: record.id, + key: record.id, + identity: record.identity, + includeKeyAlias: false, + titleText: record.title || lineText, + layer: "delayed", + citationType: "delayed_memory_citation", + } + ); + }); + const l4Memory = window.JinRuntime && window.JinRuntime.l4Memory; const facts = l4Memory && typeof l4Memory.getVisibleFacts === "function" ? l4Memory.getVisibleFacts() @@ -1501,7 +1601,7 @@ filterLiveActiveMemoryMatches(matches) .filter(match => ( match - && ["runtime", "active", "l4"].includes( + && ["runtime", "active", "delayed", "l4"].includes( match.sourceType ) )); diff --git a/ui/templates/index.html b/ui/templates/index.html index d08dc318..d450946c 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -11,12 +11,12 @@ - + - + - + @@ -509,7 +509,7 @@ - + @@ -521,14 +521,14 @@ - + - - + + From aea07b3829aba27afac919507fededf9cd719a5a Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:31:20 +0300 Subject: [PATCH 106/168] chore: metabolism - remove chemical metabolism logic - move metabolism memory logic to separate memory attention layer --- LIVE_AVATAR.md | 2 +- clients/brain_client.py | 23 +- docs/JIN_ARCHITECTURE.md | 36 +- docs/JIN_CURRENT_STATE.md | 23 +- docs/JIN_DECISIONS.md | 16 +- rules/brain_context_builder.py | 26 +- runtime/L1_memory_utils.py | 116 +- runtime/L4_memory.py | 29 +- runtime/L4_memory_utils.py | 171 +- runtime/memory_attention.py | 443 +++ runtime/metabolism.py | 3002 ----------------- runtime/runtime_context.py | 60 +- tests/runtime_actions/test_active_memory.py | 3 - .../test_delayed_memory_highlight_contract.py | 3 - tests/test_l4_context_focus_contract.py | 25 +- tests/test_l4_memory.py | 8 +- tests/test_memory_attention.py | 147 + tests/test_metabolism.py | 426 --- tests/test_metabolism_client_contract.py | 64 - tests/test_metabolism_homeostat.py | 449 --- ...ime_avatar_node_payload_client_contract.py | 5 +- tests/test_user_retry_contract.py | 2 - ui/static/css/runtime-avatar.css | 132 +- ui/static/css/runtime-memory.css | 20 - ui/static/js/logger/log-entries.js | 227 -- ui/static/js/logger/trace-modal.js | 227 -- ui/static/js/runtime/runtime-avatar.js | 274 +- ui/static/js/runtime/runtime-metabolism.js | 600 ---- ui/static/js/runtime/runtime-storage.js | 3 + ui/static/js/runtime/runtime.js | 16 +- ui/static/js/socket/event-handlers.js | 22 - ui/templates/index.html | 11 +- utils/actions/active_memory_utils.py | 12 +- utils/brain_client_utils.py | 10 - websocket/bootstrap.py | 79 - websocket/logger.py | 18 - websocket/messages.py | 60 - 37 files changed, 701 insertions(+), 6089 deletions(-) create mode 100644 runtime/memory_attention.py delete mode 100644 runtime/metabolism.py create mode 100644 tests/test_memory_attention.py delete mode 100644 tests/test_metabolism.py delete mode 100644 tests/test_metabolism_client_contract.py delete mode 100644 tests/test_metabolism_homeostat.py delete mode 100644 ui/static/js/runtime/runtime-metabolism.js diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index 0ba6301f..958e5356 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -65,7 +65,7 @@ Other visual/state knobs: The radar avatar is drawn as a single SVG inside `#jin-runtime-avatar`. A full render replaces the old SVG, but state-only changes are deliberately synchronized in place where possible so a pin/load transition does not unnecessarily reshuffle or restart visual geometry. -The SVG is intentionally only the visual projection. Rich matching/link payload (`citationIdentity`, citation key/text, reference aliases, delayed/L4 relation ids, file links, and the L4 archive angle) lives in the private `avatarNodeState` `WeakMap` in `runtime-avatar.js`, not in `data-*` attributes. Keep only lightweight canonical ids and synchronization hooks in the DOM; do not serialize full values or derived payload back into SVG nodes. Ring-wide metabolism/glow variables belong on the ring group rather than being duplicated on every memory dash. +The SVG is intentionally only the visual projection. Rich matching/link payload (`citationIdentity`, citation key/text, reference aliases, delayed/L4 relation ids, file links, and the L4 archive angle) lives in the private `avatarNodeState` `WeakMap` in `runtime-avatar.js`, not in `data-*` attributes. Keep only lightweight canonical ids and synchronization hooks in the DOM; do not serialize full values or derived payload back into SVG nodes. Ring-wide glow variables belong on the ring group rather than being duplicated on every memory dash. The stack is built in this order: diff --git a/clients/brain_client.py b/clients/brain_client.py index 736fa5de..6ba75496 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -46,9 +46,6 @@ confirm_runtime_action_guards, get_action_guard_display_id, ) -from runtime.metabolism import ( - resolve_metabolism_temperature, -) from utils.session_actions_history import ( attach_session_action_jin_message_since, build_asset_action_marker_text, @@ -378,10 +375,7 @@ async def ask_brain( client=client, user_prompt=model_user_prompt, system_prompt=system_prompt, - temperature=resolve_metabolism_temperature( - config.BRAIN_TEMPERATURE, - context, - ), + temperature=config.BRAIN_TEMPERATURE, max_tokens=None, ) @@ -471,10 +465,7 @@ async def ask_brain( result = await client.ask( system_prompt=system_prompt, user_prompt=model_user_prompt, - temperature=resolve_metabolism_temperature( - config.BRAIN_TEMPERATURE, - context, - ), + temperature=config.BRAIN_TEMPERATURE, max_tokens=None, ) @@ -2172,10 +2163,7 @@ async def apply_runtime_action_result( system_prompt=( resolved_system_prompt ), - temperature=resolve_metabolism_temperature( - config.BRAIN_TEMPERATURE, - context, - ), + temperature=config.BRAIN_TEMPERATURE, max_tokens=None, ) ): @@ -2278,10 +2266,7 @@ async def apply_runtime_action_result( resolved_system_prompt ), user_prompt=model_user_prompt, - temperature=resolve_metabolism_temperature( - config.BRAIN_TEMPERATURE, - context, - ), + temperature=config.BRAIN_TEMPERATURE, max_tokens=None, ) ): diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index e3541281..d129c7b9 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -50,7 +50,6 @@ Background / secondary paths: completed foreground turn -> L1 SERVICE summarization -> L1 diff + Facts Memory companion state - -> metabolism integration browser idle tick -> L4 extraction/merge pipeline @@ -70,7 +69,7 @@ Main ownership by package: | `rules/brain_context_builder.py` | deterministic Brain prompt assembly from current state | | `runtime/L1_memory*` | live runtime-memory summarization, snapshots, diffs, interrupted-turn memory path | | `runtime/L4_memory*` | Facts Memory ingestion, durable L4 extraction/merge, reconciliation, delete/restore | -| `runtime/metabolism.py` | active homeostat/salience system and SERVICE integration | +| `runtime/memory_attention.py` | prompt-only Active/Delayed/L4 relevance ranking | | `utils/*_store.py` | durable file/report/fact stores | | `ui/static/js/runtime/` | browser-side runtime state, persistence, session checkpointing, memory UI, avatar | | `ui/static/js/logger/` | inspectable logger/action/memory projections | @@ -93,7 +92,7 @@ Main ownership by package: - attached file IDs and sequence attachments; - session/reconnect/archived-restore metadata; - L4/background tasks; -- metabolism state; +- prompt-only L4 focus diagnostics; - avatar color/size/position/speed related state; - message/turn/sequence counters. @@ -111,13 +110,12 @@ The websocket endpoint uses a normal `asyncio.Queue` to serialize queued request 2. resolve current attachments; 3. establish turn ID and sequence ID; 4. reset per-turn transient state; -5. on ordinary turns: apply browser idle/Active/pattern/avatar state, prepare metabolism, and auto-load Delayed Memory by user-typed tags; +5. on ordinary turns: apply browser idle/Active/pattern/avatar state and auto-load Delayed Memory by user-typed tags; 6. append the user turn to the local chat log; 7. build `AgentState` and execute `AgentRuntime`; 8. persist reasoning log and emit action/session telemetry; 9. append visible JIN output to chat log/recent-turn state; -10. settle immediate metabolism effects; -11. schedule normal or interrupted L1 memory integration. +10. schedule normal or interrupted L1 memory integration. A later foreground turn can wait for a pending L1 update through `wait_for_runtime_memory_update()` so the Brain does not race stale live memory. @@ -136,7 +134,7 @@ There is no active planner/router node in front of Brain. Physical model endpoints are resolved through the client/config layer. Logical roles remain: - **BRAIN** — foreground reasoning, visible answer, runtime decisions; -- **SERVICE** — L1 integration, metabolism/L4/supporting model work. +- **SERVICE** — L1/L4 integration and supporting model work. `USE_SERVICE_AS_BRAIN=True` may map both logical roles to one endpoint without removing the role distinction. @@ -159,7 +157,6 @@ The current high-level order is: 9. loaded skill content; 10. runtime-context group: - explicit user feedback; - - silent metabolism state; - Active Memory view; - L1/runtime memory snapshot; - visible session counters; @@ -364,7 +361,7 @@ Creation custom fields are explicit structure only: JSON root fields beside `con Compatibility code still accepts legacy nested `fields`/`updates`, older line-based update payloads, and a self-closing UPDATE attribute form. Those are localized reader compatibility. Internally/browser-side, Active Memory is still represented in a transitional string-record format with metadata suffixes; that is an implementation detail, not the desired model-facing schema. -Paused Active records are removed from the Brain prompt. Metabolism may reorder the prompt projection by salience without changing canonical storage order. +Paused Active records are removed from the Brain prompt. Memory Attention may reorder the prompt projection by lexical/context relevance without changing canonical storage order. ### 7.7 Persistent Files @@ -378,24 +375,15 @@ Persistent uploads are owned by `utils/attached_files_store.py` and `assets/file --- -## 8. Metabolism +## 8. Memory Attention -`runtime/metabolism.py` is a real active runtime subsystem in this snapshot, not merely a future night-cycle idea. +`runtime/memory_attention.py` is a small, deterministic prompt-projection module. It implements only: -It currently implements: +- lexical/current-context relevance ordering for Active Memory; +- temporary bubble tiers for Delayed Memory inventory matching; +- a narrow 1–3 relevant-fact focus cone for L4. -- continuous homeostat levels; -- immediate foreground impulse before Brain generation; -- outcome settling after a turn; -- active-memory salience; -- delayed-memory scoring; -- L4 context-focus/significance scoring; -- lexical/association signals; -- temperature modulation; -- debounced SERVICE integration after committed L1 updates; -- browser state emission/persistence support. - -This does **not** prove that the older concept of a full nightly self-review/consolidation cycle is complete. That remains a separate product direction and must not be inferred from the existence of `metabolism.py`. +Memory Attention is stateless. It does not call SERVICE, mutate or persist memory records, add prompt instructions, modify Brain temperature, drive avatar state, or maintain significance scores. Canonical storage/UI order remains independent from the prompt projection. --- diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index dc73c0c4..6576ae25 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -46,7 +46,7 @@ Present and active: - `runtime/stream.py` - `runtime/L1_memory.py`, `L1_memory_rules.py`, `L1_memory_utils.py` - `runtime/L4_memory.py`, `L4_memory_rules.py`, `L4_memory_utils.py` -- `runtime/metabolism.py` +- `runtime/memory_attention.py` - `runtime/anonymous_mode.py` - `contracts/*.json` - `utils/actions/*` @@ -231,7 +231,7 @@ The self-closing attribute form is compatibility only; the model-facing contract ### Current internal representation -Active records are still stored/transported in a string-oriented record format with metadata suffixes. Prompt assembly refreshes metadata, removes paused items, and may rank the prompt view by metabolism salience. +Active records are still stored/transported in a string-oriented record format with metadata suffixes. Prompt assembly refreshes metadata, removes paused items, and may rank the prompt view by lexical/context relevance. ### Risk @@ -357,24 +357,19 @@ This is the current performance/ordering contract. Do not move L4 onto every for --- -## 11. Metabolism status +## 11. Memory Attention status -`runtime/metabolism.py` is large and actively wired into the foreground/L1 flow. +The metabolism subsystem has been removed. `runtime/memory_attention.py` retains only prompt-local retrieval behavior: -Verified active pieces include: +- Active lexical/context relevance; +- Delayed bubble matching; +- L4 1–3 fact focus. -- pre-Brain foreground impulse; -- post-turn settling; -- Active/Delayed/L4 significance/salience scoring; -- temperature adjustment; -- recent-turn memory; -- association learning; -- SERVICE update scheduled after successful L1 diff commit; -- bootstrap state support. +There is no metabolic SERVICE pass, homeostat, learned association state, temperature modulation, Brain instruction, L1 strength bias, significance persistence, bootstrap chemistry, logger trace, or avatar chemistry. Historical significance fields are discarded while normalizing old Active/Facts/L4 records. ### Not proven -The older concept of a full nightly self-review cycle that reads cropped reasoning/context and emits a morning cleanup report is **not proven complete** by this snapshot. Do not describe it as finished architecture without finding the actual scheduler/storage/report path. +The older concept of a full nightly self-review cycle that reads cropped reasoning/context and emits a morning cleanup report is **not proven complete** by this snapshot. Do not describe it as finished architecture without finding the actual scheduler/storage/report path; Memory Attention is unrelated to that cycle. --- diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index b5210e7b..97e04948 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -202,7 +202,7 @@ Stop/cancel/repetition/context-limit interruption must preserve separate lifecyc Serialize/deserialize must preserve entity/snapshot timestamps. `now()` is only a fallback for a truly legacy record with no timestamp. -**Why:** ordering, age, metabolism, and visible history become false if reload mutates time. +**Why:** ordering, age, context relevance, and visible history become false if reload mutates time. **Rejected alternative:** stamping restored rows with current time for convenience. @@ -517,3 +517,17 @@ Action/recovery follow-ups do not append this ordinary block again. They constru **Why:** the opening and conclusion preserve the previous line of thought without spending the entire context window on its middle, while dedicated follow-up context avoids stale or duplicated reasoning. **Rejected alternatives:** exposing previous reasoning only during session-restore priming; dropping the block from ordinary turns; duplicating it inside action/recovery follow-ups; trimming only one edge. + +--- + +## D038 — Memory Attention replaces metabolism + +**Status:** Accepted / implemented + +The metabolism subsystem is removed. The retained `runtime/memory_attention.py` module performs only prompt-local Active lexical/context relevance, Delayed inventory bubble matching, and a narrow 1–3 fact L4 focus. + +Memory Attention is deterministic and stateless: it does not call SERVICE, change Brain temperature, generate hidden instructions, learn phrase associations, mutate memory while building context, persist significance, or drive avatar chemistry. + +**Why:** the useful behavior was retrieval/ranking. The causal homeostat duplicated model work, obscured sampling, and leaked event-wide significance through L1 into durable L4. + +**Rejected alternatives:** keeping observer-only chemistry; keeping significance as an independent durable score; preserving the metabolic UI without the backend. diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index 866818d0..c095f0cc 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -417,10 +417,10 @@ def _append_L1_runtime_memory( ) ] - # Metabolism changes attention, not the persistent record order. The - # prompt gets a salience-ranked view while storage/UI stay canonical. + # Memory attention changes only the prompt projection. Storage/UI keep + # their canonical order and records are never rewritten while reading. try: - from runtime.metabolism import rank_active_memory_records + from runtime.memory_attention import rank_active_memory_records active_memory_context_records = rank_active_memory_records( active_memory_context_records, @@ -584,7 +584,7 @@ def build_delayed_memory_inventory_context( ) try: - from runtime.metabolism import ( + from runtime.memory_attention import ( delayed_memory_bubble_tier, score_delayed_memory_report, ) @@ -656,7 +656,7 @@ def build_delayed_memory_inventory_context( if not report_names: return "" - # last_loaded_date remains the canonical order. A live lexical/metabolic + # last_loaded_date remains the canonical order. A live lexical/context # match only adds a temporary prompt-only bubble tier; storage/UI order is # untouched. Strongly relevant reports may surface above newer unrelated # ones, while weak/no-match inventories stay purely recency-sorted. @@ -1305,22 +1305,6 @@ def build_brain_context( context, ) - # Silent metabolism block: the current state is allowed to modulate - # attention/continuity but is never a conversational topic by default. - try: - from runtime.metabolism import build_metabolism_brain_context - - metabolism_context = build_metabolism_brain_context( - context, - user_input=user_input, - ) - if metabolism_context: - runtime_context_parts.append( - metabolism_context - ) - except Exception: - pass - # L1 memory block: includes active memory records and live runtime memory. _append_L1_runtime_memory( runtime_context_parts, diff --git a/runtime/L1_memory_utils.py b/runtime/L1_memory_utils.py index 801d61aa..05c4d6ae 100644 --- a/runtime/L1_memory_utils.py +++ b/runtime/L1_memory_utils.py @@ -1937,58 +1937,6 @@ def compute_line_strength( ) -def apply_metabolic_runtime_memory_salience( - lines: list[dict], - context=None, -) -> list[dict]: - """Apply the silent homeostat as a small L1 encoding-strength bias. - - This never adds/rewrites memory content. It only reuses the existing - ``strength`` metadata that the runtime already visualizes. - """ - - if context is None: - return lines - - try: - from runtime.metabolism import ( - current_metabolic_event_significance, - metabolic_memory_strength_boost, - ) - except Exception: - return lines - - event_significance = current_metabolic_event_significance(context) - - for line in lines: - if not isinstance(line, dict): - continue - - try: - boost, salience, channel = metabolic_memory_strength_boost( - context, - key=line.get("key", ""), - value=line.get("value", ""), - status=line.get("status", "same"), - ) - except Exception: - continue - - base_strength = float(line.get("strength", 0.0) or 0.0) - line["strength"] = round( - min(1.0, max(0.0, base_strength + boost)), - 4, - ) - line["metabolic_salience"] = salience - line["metabolic_significance"] = event_significance - if channel: - line["metabolic_channel"] = channel - else: - line.pop("metabolic_channel", None) - - return lines - - def runtime_memory_line_identity( line: dict, ) -> str: @@ -2810,10 +2758,7 @@ def apply_runtime_memory_diff( quote_boost=quote_boost, ) - return apply_metabolic_runtime_memory_salience( - current_lines, - context, - ) + return current_lines previous_lines = ( previous_snapshot.get( @@ -3002,10 +2947,7 @@ def apply_runtime_memory_diff( quote_boost=quote_boost, ) - return apply_metabolic_runtime_memory_salience( - current_lines, - context, - ) + return current_lines def build_runtime_memory_patch( @@ -3579,46 +3521,6 @@ def _dict_list(value, *, limit: int | None = None) -> list[dict]: "loaded_memory_ids": loaded_memory_ids, "attached_file_ids": attached_file_ids, "active_memory_records": active_memory_records, - "metabolism_levels": dict( - getattr( - context, - "runtime_metabolism_levels", - {}, - ) - or {} - ), - "metabolism_updated_at": float( - getattr( - context, - "runtime_metabolism_last_tick_at", - 0.0, - ) - or 0.0 - ), - "metabolism_instruction": str( - getattr( - context, - "runtime_metabolism_instruction", - "", - ) - or "" - )[:900].strip(), - "metabolism_associations": _dict_list( - getattr( - context, - "runtime_metabolism_associations", - [], - ), - limit=96, - ), - "metabolism_last_committed_l1_id": str( - getattr( - context, - "runtime_metabolism_last_committed_l1_id", - "", - ) - or "" - ).strip(), "current_jin_color": str( getattr( context, @@ -3818,20 +3720,6 @@ async def record_runtime_l1_diff( await emit_runtime_l1_diff_update(context) - # Semantic metabolism is committed at the same cadence as L1, not chat. - # One successful L1 batch -> at most one debounced SERVICE integration. - try: - from runtime.metabolism import schedule_metabolism_update - - schedule_metabolism_update( - context, - committed_snapshot=snapshot, - committed_turns=observed_turns, - ) - except Exception: - # Metabolism is ambient and must never invalidate a committed L1 batch. - pass - async def emit_runtime_memory_update( context, diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index ac781907..c6b16228 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -3991,11 +3991,10 @@ def build_runtime_l4_memory_context(*, context, fact_ids=None, user_input: str = ] # L4 prompt order stays independent from panel/avatar order. Fresh facts - # are nearest to the model by default, query focus may bubble a narrow - # 1..3-fact cone, and persistent metabolic significance can hold a fact in - # the prompt's top lane until context-exposure decay drops it below 0.5. + # are nearest to the model by default; memory attention may bubble a + # narrow 1..3-fact relevance cone for this prompt only. try: - from runtime.metabolism import rank_l4_facts_for_context + from runtime.memory_attention import rank_l4_facts_for_context active_facts = rank_l4_facts_for_context( active_facts, @@ -4019,28 +4018,6 @@ def _fact_sort_key(fact): active_facts = sorted(active_facts, key=_fact_sort_key) - if bool( - getattr( - context, - "runtime_metabolism_l4_significance_dirty", - False, - ) - ): - try: - store["revision"] = max( - 0, - int(store.get("revision") or 0), - ) + 1 - except (TypeError, ValueError): - store["revision"] = 1 - store["updated_at"] = utc_now_iso() - context.runtime_long_term_memory_store = store - persist_runtime_l4_file_store( - context, - store, - ) - context.runtime_metabolism_l4_significance_dirty = False - delayed_memory_ids_by_fact_id = {} for fact in active_facts: diff --git a/runtime/L4_memory_utils.py b/runtime/L4_memory_utils.py index 13bddd7d..9dc5bda2 100644 --- a/runtime/L4_memory_utils.py +++ b/runtime/L4_memory_utils.py @@ -1172,31 +1172,12 @@ def normalize_l4_fact( except (TypeError, ValueError): mention_count = 1 - try: - significance = float( - value.get("significance") - if value.get("significance") is not None - else value.get("metabolic_significance", 0.0) - ) - except (TypeError, ValueError): - significance = 0.0 - if significance != significance: - significance = 0.0 - significance = round(max(0.0, min(1.0, significance)), 4) - return { "id": fact_id, "key": key, "value": fact_value, "category": normalize_l4_category(value.get("category")), "mention_count": mention_count, - "significance": significance, - "significance_updated_at": ( - normalize_l4_text(value.get("significance_updated_at")) - or normalize_l4_text(value.get("updated_at")) - or normalize_l4_text(value.get("created_at")) - or current_time - ), "created_at": normalize_l4_text(value.get("created_at")) or current_time, "updated_at": normalize_l4_text(value.get("updated_at")) or current_time, "source_session_ids": normalize_l4_string_list( @@ -1240,26 +1221,6 @@ def merge_same_l4_fact(existing: dict, incoming: dict, *, now: str) -> dict: existing.get("source_fact_ids"), incoming.get("source_fact_ids"), ) - existing_significance = float(existing.get("significance", 0.0) or 0.0) - incoming_significance = float(incoming.get("significance", 0.0) or 0.0) - if incoming_significance > existing_significance: - result["significance"] = round( - max(0.0, min(1.0, incoming_significance)), - 4, - ) - result["significance_updated_at"] = ( - normalize_l4_text(incoming.get("significance_updated_at")) - or now - ) - else: - result["significance"] = round( - max(0.0, min(1.0, existing_significance)), - 4, - ) - result["significance_updated_at"] = ( - normalize_l4_text(existing.get("significance_updated_at")) - or now - ) result["updated_at"] = now return result @@ -1332,44 +1293,9 @@ def merge_l4_snapshot_fact( incoming_id = normalize_l4_text( incoming_fact.get("id") ) - existing_significance_at = normalize_l4_text( - existing_fact.get("significance_updated_at") - ) - incoming_significance_at = normalize_l4_text( - incoming_fact.get("significance_updated_at") - ) - existing_significance_time = l4_timestamp_sort_value( - existing_significance_at - ) - incoming_significance_time = l4_timestamp_sort_value( - incoming_significance_at - ) - if incoming_significance_time > existing_significance_time: - significance_source = incoming_fact - elif existing_significance_time > incoming_significance_time: - significance_source = existing_fact - else: - significance_source = max( - (existing_fact, incoming_fact), - key=lambda fact: float( - fact.get("significance", 0.0) - or 0.0 - ), - ) - merged = { **base, "id": existing_id or incoming_id, - "significance": float( - significance_source.get("significance", 0.0) - or 0.0 - ), - "significance_updated_at": ( - normalize_l4_text( - significance_source.get("significance_updated_at") - ) - or now - ), "mention_count": max( max(1, int(existing_fact.get("mention_count") or 1)), max(1, int(incoming_fact.get("mention_count") or 1)), @@ -1966,21 +1892,12 @@ def normalize_facts_memory_field( if status not in {L4_FIELD_STATUS_PENDING, L4_FIELD_STATUS_ANALYZED}: status = L4_FIELD_STATUS_PENDING - try: - significance = float(field.get("significance", 0.0) or 0.0) - except (TypeError, ValueError): - significance = 0.0 - if significance != significance: - significance = 0.0 - significance = round(max(0.0, min(1.0, significance)), 4) - - return { + normalized = { **field, "content": content, "runtime_snapshot_id": normalize_l4_text(field.get("runtime_snapshot_id")), "session_id": normalize_l4_text(field.get("session_id") or session_id), "l4_status": status, - "significance": significance, "l4_content_hash": normalize_l4_text(field.get("l4_content_hash")) or build_l4_content_hash(content), "l4_analyzed_at": ( @@ -1989,6 +1906,11 @@ def normalize_facts_memory_field( else "" ), } + # Drop retired salience metadata when old browser/file records are read. + normalized.pop("significance", None) + normalized.pop("metabolic_significance", None) + normalized.pop("significance_updated_at", None) + return normalized def normalize_facts_memory_records(value) -> list[dict]: @@ -2052,7 +1974,6 @@ def collect_pending_facts_memory_fields(records: list[dict]) -> list[dict]: "runtime_snapshot_id": field.get("runtime_snapshot_id", ""), "session_id": field.get("session_id") or session_id, "l4_content_hash": field.get("l4_content_hash", ""), - "significance": field.get("significance", 0.0), }) return pending @@ -2160,18 +2081,9 @@ def normalize_l4_candidates( for source_key in source_keys for field in fields_by_key[source_key] ] - matched_significance = max( - ( - float(field.get("significance", 0.0) or 0.0) - for field in matched_fields - ), - default=0.0, - ) candidate = normalize_l4_fact( { **raw_candidate, - "significance": matched_significance, - "significance_updated_at": current_time, "source_keys": source_keys, "source_session_ids": [ field.get("session_id", "") for field in matched_fields @@ -2445,33 +2357,6 @@ def merge_fact_sources(existing: dict, incoming: dict) -> dict: } -def merge_l4_significance( - *facts: dict, - now: str, -) -> tuple[float, str]: - best_value = 0.0 - best_updated_at = now - - for fact in facts: - if not isinstance(fact, dict): - continue - try: - value = float(fact.get("significance", 0.0) or 0.0) - except (TypeError, ValueError): - value = 0.0 - if value != value: - value = 0.0 - value = max(0.0, min(1.0, value)) - if value >= best_value: - best_value = value - best_updated_at = ( - normalize_l4_text(fact.get("significance_updated_at")) - or now - ) - - return round(best_value, 4), best_updated_at - - def build_l4_merge_detail_fact(fact: dict) -> dict: return { "id": normalize_l4_text(fact.get("id")), @@ -2606,11 +2491,6 @@ def rebuild_facts_by_id() -> dict[str, int]: index = facts_by_id[operation["target_id"]] target = facts[index] target_before = dict(target) - significance, significance_updated_at = merge_l4_significance( - target, - pending, - now=current_time, - ) candidate = normalize_l4_fact( { **pending, @@ -2620,8 +2500,6 @@ def rebuild_facts_by_id() -> dict[str, int]: "id": target["id"], "created_at": target.get("created_at"), "updated_at": current_time, - "significance": significance, - "significance_updated_at": significance_updated_at, **merge_fact_sources(target, pending), "mention_count": max(1, int(target.get("mention_count") or 1)) + max(1, int(pending.get("mention_count") or 1)), @@ -2667,11 +2545,6 @@ def rebuild_facts_by_id() -> dict[str, int]: allocation_store, pending=False, ) - significance, significance_updated_at = merge_l4_significance( - *merged_facts, - pending, - now=current_time, - ) candidate = normalize_l4_fact( { "id": new_fact_id, @@ -2680,8 +2553,6 @@ def rebuild_facts_by_id() -> dict[str, int]: "category": operation["category"], "created_at": current_time, "updated_at": current_time, - "significance": significance, - "significance_updated_at": significance_updated_at, "mention_count": sum( max(1, int(fact.get("mention_count") or 1)) for fact in [*merged_facts, pending] @@ -3055,12 +2926,6 @@ def normalize_note_fact( mention_count: int = 1, duplicate_key_reason: str, ) -> tuple[dict | None, str]: - source_significance, source_significance_updated_at = ( - merge_l4_significance( - *(source_facts or []), - now=current_time, - ) - ) fact = normalize_l4_fact( { **raw_fact, @@ -3068,17 +2933,6 @@ def normalize_note_fact( "created_at": created_at, "updated_at": current_time, "mention_count": mention_count, - "significance": ( - raw_fact.get("significance") - if raw_fact.get("significance") is not None - else source_significance - ), - "significance_updated_at": ( - normalize_l4_text( - raw_fact.get("significance_updated_at") - ) - or source_significance_updated_at - ), "source_session_ids": merge_l4_string_lists( *[ source_fact.get("source_session_ids") @@ -3500,18 +3354,7 @@ def format_long_term_memory_context( if not line or not fact_id: continue - try: - significance = max( - 0.0, - min(1.0, float(fact.get("significance", 0.0) or 0.0)), - ) - except (TypeError, ValueError): - significance = 0.0 - - suffix = ( - f" [ id: {fact_id} ]" - f" [ significance: {significance:.3f} ]" - ) + suffix = f" [ id: {fact_id} ]" delayed_memory_ids = delayed_memory_ids_by_fact_id.get( fact_id.upper(), [], diff --git a/runtime/memory_attention.py b/runtime/memory_attention.py new file mode 100644 index 00000000..4a8e1777 --- /dev/null +++ b/runtime/memory_attention.py @@ -0,0 +1,443 @@ +"""Prompt-only relevance ranking for JIN memory systems. + +Memory attention is deliberately stateless: it does not call a model, mutate +memory records, persist scores, or steer Brain sampling. It only builds the +current prompt projection for Active Memory, Delayed Memory and L4. +""" + +from __future__ import annotations + +import re +from typing import Any + + +ACTIVE_MEMORY_MIN_RELEVANCE = 0.16 +DELAYED_MEMORY_BUBBLE_THRESHOLD = 0.26 +DELAYED_MEMORY_STRONG_BUBBLE_THRESHOLD = 0.42 +MEMORY_RECENT_FACTS = 6 +MEMORY_RECENT_USER_TURNS = 3 + + +def _normalized_text(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "").strip().casefold()) + + +def _signal_words(value: Any) -> list[str]: + return [ + token + for token in re.findall( + r"[^\W_]{2,}", + _normalized_text(value), + flags=re.UNICODE, + ) + if token + ] + + +_MEMORY_LEXICAL_STOPWORDS = { + # Keep this intentionally tiny. It only removes glue words that otherwise + # create false relevance when the user says things like "это" / "this". + "это", "эта", "этот", "эти", "того", "так", "там", "тут", "вот", + "как", "что", "чтобы", "если", "или", "для", "при", "про", "она", + "они", "оно", "его", "её", "уже", "ещё", "еще", "просто", "сейчас", + "давай", "дальше", "продолжим", "продолжить", "поговорим", "снова", + "опять", "this", "that", "these", "those", "with", "from", "into", + "about", "have", "has", "had", "just", "then", "than", "when", + "where", "what", "your", "you", "user", "jin", +} + + +def _memory_lexical_tokens(value: Any) -> set[str]: + return { + token + for token in _signal_words(value) + if len(token) >= 3 and token not in _MEMORY_LEXICAL_STOPWORDS + } + + +def _memory_token_stem(token: str) -> str: + token = str(token or "") + if len(token) <= 5: + return token + # Cheap morphology bridge for inflected RU/EN words. + return token[:5] + + +def _memory_phrase_ngrams( + value: Any, + *, + size: int = 2, +) -> set[tuple[str, ...]]: + words = [ + token + for token in _signal_words(value) + if len(token) >= 3 and token not in _MEMORY_LEXICAL_STOPWORDS + ] + if len(words) < size: + return set() + return { + tuple(_memory_token_stem(token) for token in words[index:index + size]) + for index in range(len(words) - size + 1) + } + + +def lexical_memory_match(query: Any, candidate: Any) -> float: + """Return a cheap RU/EN lexical match in the 0..1 range.""" + + query_tokens = _memory_lexical_tokens(query) + candidate_tokens = _memory_lexical_tokens(candidate) + if not query_tokens or not candidate_tokens: + return 0.0 + + exact_hits = len(query_tokens & candidate_tokens) + query_stems = {_memory_token_stem(token) for token in query_tokens} + candidate_stems = {_memory_token_stem(token) for token in candidate_tokens} + stem_hits = len(query_stems & candidate_stems) + + # One meaningful keyword should still count inside a long user sentence. + query_denominator = max(1, min(4, len(query_tokens))) + exact_score = min(1.0, exact_hits / query_denominator) + stem_score = min(1.0, stem_hits / query_denominator) + candidate_denominator = max(1, min(6, len(candidate_tokens))) + candidate_focus = min(1.0, stem_hits / candidate_denominator) + + query_bigrams = _memory_phrase_ngrams(query, size=2) + candidate_bigrams = _memory_phrase_ngrams(candidate, size=2) + phrase_score = 1.0 if query_bigrams & candidate_bigrams else 0.0 + + return round( + min( + 1.0, + exact_score * 0.48 + + stem_score * 0.30 + + candidate_focus * 0.08 + + phrase_score * 0.14, + ), + 4, + ) + + +def _l4_fact_numeric_sort_key(fact: dict) -> tuple: + """Newest durable facts first; F ids are monotonic creation ids.""" + + fact_id = str((fact or {}).get("id", "") or "").strip() + match = re.fullmatch(r"F([1-9]\d*)", fact_id, flags=re.IGNORECASE) + if match: + return (0, -int(match.group(1)), "") + return (1, 0, fact_id.casefold()) + + +def _recent_user_memory_queries(context) -> list[str]: + if context is None: + return [] + + result: list[str] = [] + seen: set[str] = set() + for turn in reversed(getattr(context, "runtime_recent_turns", []) or []): + if not isinstance(turn, dict): + continue + text = str(turn.get("user", turn.get("user_message", "")) or "").strip() + normalized = _normalized_text(text) + if not normalized or normalized in seen: + continue + result.append(text) + seen.add(normalized) + if len(result) >= MEMORY_RECENT_USER_TURNS: + break + return result + + +def _recent_l4_memory_queries(context) -> list[str]: + if context is None: + return [] + + store = getattr(context, "runtime_long_term_memory_store", {}) + facts = store.get("facts", []) if isinstance(store, dict) else [] + normalized = [fact for fact in facts or [] if isinstance(fact, dict)] + normalized.sort(key=_l4_fact_numeric_sort_key) + + result = [] + for fact in normalized[:MEMORY_RECENT_FACTS]: + text = " ".join( + part + for part in ( + str(fact.get("key", "") or "").strip(), + str(fact.get("value", "") or "").strip(), + ) + if part + ) + if text: + result.append(text) + return result + + +def memory_context_relevance( + candidate: Any, + *, + user_input: str = "", + context=None, +) -> float: + """Blend the current query with a small recent-dialogue/L4 context tail.""" + + current = lexical_memory_match(user_input, candidate) + recent = max( + ( + lexical_memory_match(query, candidate) + for query in _recent_user_memory_queries(context) + ), + default=0.0, + ) + recent_fact = max( + ( + lexical_memory_match(query, candidate) + for query in _recent_l4_memory_queries(context) + ), + default=0.0, + ) + contextual_tail = min(1.0, recent * 0.24 + recent_fact * 0.30) + return round( + min(1.0, current + (1.0 - current) * contextual_tail), + 4, + ) + + +def _active_memory_record_id(record: str) -> str: + try: + from utils.actions.active_memory_utils import collect_active_memory_slot_ids + + ids = sorted(collect_active_memory_slot_ids(record)) + return ids[0] if ids else "" + except Exception: + return "" + + +def _active_memory_visible_text(record: str) -> str: + try: + from utils.actions.active_memory_utils import strip_active_memory_managed_suffixes + + return strip_active_memory_managed_suffixes(record) + except Exception: + return str(record or "").strip() + + +def _active_memory_is_paused(record: str) -> bool: + try: + from utils.actions.active_memory_utils import is_active_memory_record_paused + + return bool(is_active_memory_record_paused(record)) + except Exception: + return False + + +def score_active_memory_record( + record: str, + *, + user_input: str, + context=None, +) -> float: + relevance = memory_context_relevance( + _active_memory_visible_text(record), + user_input=user_input, + context=context, + ) + score = ACTIVE_MEMORY_MIN_RELEVANCE + min(0.74, relevance * 0.84) + + normalized_user = _normalized_text(user_input) + active_id = _active_memory_record_id(record) + if active_id and active_id.casefold() in normalized_user: + score += 0.30 + + return round(max(0.0, min(1.0, score)), 3) + + +def rank_active_memory_records( + records: list[str], + *, + context=None, + user_input: str = "", +) -> list[str]: + """Return a relevance-ranked prompt view without touching stored records.""" + + scored = [] + for index, record in enumerate(records or []): + if _active_memory_is_paused(record): + continue + scored.append( + ( + score_active_memory_record( + record, + user_input=user_input, + context=context, + ), + index, + record, + ) + ) + scored.sort(key=lambda item: (-item[0], item[1])) + return [record for _score, _index, record in scored] + + +def score_delayed_memory_report( + report: dict, + *, + report_id: str = "", + user_input: str = "", + context=None, +) -> float: + if not isinstance(report, dict): + return 0.0 + + title = str(report.get("title", "") or "").strip() + summary = str(report.get("summary", "") or "").strip() + tags = report.get("tags", []) + if not isinstance(tags, list): + tags = [tags] + tags_text = " ".join( + str(tag or "").strip() + for tag in tags + if str(tag or "").strip() + ) + + title_score = ( + memory_context_relevance(title, user_input=user_input, context=context) + if title + else 0.0 + ) + summary_score = ( + memory_context_relevance(summary, user_input=user_input, context=context) + if summary + else 0.0 + ) + tags_score = ( + memory_context_relevance(tags_text, user_input=user_input, context=context) + if tags_text + else 0.0 + ) + score = max(title_score, summary_score * 0.88, tags_score * 0.72) + + normalized_user = _normalized_text(user_input) + normalized_id = str(report_id or report.get("id", "") or "").strip().casefold() + if normalized_id and normalized_id in normalized_user: + score = max(score, 0.96) + + return round(max(0.0, min(1.0, score)), 3) + + +def delayed_memory_bubble_tier(score: float) -> int: + try: + value = float(score) + except (TypeError, ValueError): + value = 0.0 + + if value >= DELAYED_MEMORY_STRONG_BUBBLE_THRESHOLD: + return 2 + if value >= DELAYED_MEMORY_BUBBLE_THRESHOLD: + return 1 + return 0 + + +def score_l4_fact_context_focus( + fact: dict, + *, + user_input: str, + context=None, +) -> float: + """Return prompt-only L4 relevance without mutating storage or UI order.""" + + if not isinstance(fact, dict): + return 0.0 + fact_text = " ".join( + part + for part in ( + str(fact.get("key", "") or "").strip(), + str(fact.get("value", "") or "").strip(), + ) + if part + ) + if not fact_text: + return 0.0 + current = lexical_memory_match(user_input, fact_text) + recent = max( + ( + lexical_memory_match(query, fact_text) + for query in _recent_user_memory_queries(context) + ), + default=0.0, + ) + # L4 facts must not become relevant by matching themselves through the + # recent-L4 context tail. Only the current/recent USER dialogue may focus + # the durable store. + return round( + min(1.0, current + (1.0 - current) * recent * 0.24), + 4, + ) + + +def _select_l4_focus( + scored: list[tuple[float, int, dict]], +) -> list[tuple[float, int, dict]]: + """Choose a coherent 1..3 fact cone instead of a fixed top-k.""" + + ranked = sorted(scored, key=lambda item: (-item[0], item[1])) + if not ranked or ranked[0][0] < 0.22: + return [] + + top_score = ranked[0][0] + focused = [ranked[0]] + if len(ranked) > 1 and ranked[1][0] >= max(0.20, top_score * 0.75): + focused.append(ranked[1]) + if ( + len(focused) == 2 + and len(ranked) > 2 + and ranked[2][0] >= max(0.18, top_score * 0.61) + and top_score >= 0.34 + ): + focused.append(ranked[2]) + return focused + + +def rank_l4_facts_for_context( + facts: list[dict], + *, + context=None, + user_input: str = "", +) -> list[dict]: + """Bubble 1..3 relevant facts above the canonical newest-first lane.""" + + canonical = sorted( + [fact for fact in (facts or []) if isinstance(fact, dict)], + key=_l4_fact_numeric_sort_key, + ) + if not canonical: + if context is not None: + context.runtime_memory_attention_l4_focus_ids = [] + return [] + + focused_facts = [] + if str(user_input or "").strip(): + focused = _select_l4_focus([ + ( + score_l4_fact_context_focus( + fact, + user_input=user_input, + context=context, + ), + index, + fact, + ) + for index, fact in enumerate(canonical) + ]) + focused_facts = [item[2] for item in focused] + + if context is not None: + context.runtime_memory_attention_l4_focus_ids = [ + str(fact.get("id", "") or "").strip() + for fact in focused_facts + if str(fact.get("id", "") or "").strip() + ] + + focused_object_ids = {id(fact) for fact in focused_facts} + return [ + *focused_facts, + *(fact for fact in canonical if id(fact) not in focused_object_ids), + ] diff --git a/runtime/metabolism.py b/runtime/metabolism.py deleted file mode 100644 index 307d8073..00000000 --- a/runtime/metabolism.py +++ /dev/null @@ -1,3002 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -import math -import re -import secrets -import time -import unicodedata -from copy import deepcopy -from datetime import datetime, timezone -from typing import Any - -from clients.response_extractor import ResponseExtractor -from clients.service_client import ask_service_model -from config_loader import config -from utils.tokens import estimate_runtime_tokens - - -METABOLISM_CHANNELS = ( - "dopamine", - "serotonin", - "oxytocin", - "norepinephrine", - "cortisol", -) - -METABOLISM_DEFAULT_LEVELS = { - "dopamine": 0.42, - "serotonin": 0.58, - "oxytocin": 0.46, - "norepinephrine": 0.38, - "cortisol": 0.24, -} - -# The service model interprets the turn and proposes a target. JIN owns the -# actual physics: slow channels cannot jump as fast as reactive channels. -METABOLISM_MAX_STEP = { - "dopamine": 0.08, - "serotonin": 0.04, - "oxytocin": 0.04, - "norepinephrine": 0.08, - "cortisol": 0.08, -} - -# Wall-clock return to baseline. The server advances lazily at the next -# observable event; the UI mirrors the same curve while JIN is idle. This -# gives us continuous homeostasis without another noisy background worker. -METABOLISM_HALF_LIFE_SECONDS = { - "dopamine": 45 * 60, - "serotonin": 150 * 60, - "oxytocin": 120 * 60, - "norepinephrine": 32 * 60, - "cortisol": 55 * 60, -} - -# The lexical reflex happens before generation. It is deliberately weaker -# than the semantic SERVICE estimate that integrates the completed turn. -METABOLISM_REFLEX_MAX_STEP = { - "dopamine": 0.05, - "serotonin": 0.035, - "oxytocin": 0.04, - "norepinephrine": 0.05, - "cortisol": 0.05, -} - -METABOLISM_TEMPERATURE_MAX_DELTA = 0.12 -METABOLISM_MEMORY_STRENGTH_MAX_BOOST = 0.12 -METABOLISM_ACTIVE_MEMORY_MIN_SALIENCE = 0.16 -METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD = 0.50 -METABOLISM_MEMORY_SIGNIFICANCE_HALF_LIFE_SECONDS = 90 * 60 -METABOLISM_MEMORY_SIGNIFICANCE_MAX_CONTEXT_STEP_SECONDS = 5 * 60 -METABOLISM_MEMORY_SIGNIFICANCE_REINFORCE_FLOOR = 0.12 -METABOLISM_MEMORY_SIGNIFICANCE_EVENT_MAX_AGE_SECONDS = 10 * 60 -METABOLISM_DELAYED_MEMORY_BUBBLE_THRESHOLD = 0.26 -METABOLISM_DELAYED_MEMORY_STRONG_BUBBLE_THRESHOLD = 0.42 -METABOLISM_MEMORY_RECENT_FACTS = 6 -METABOLISM_MEMORY_RECENT_USER_TURNS = 3 -METABOLISM_INSTRUCTION_MAX_CHARS = 900 - -# Fast rollback switches. State estimation + UI telemetry keep running even if -# causal steering is disabled, so it is easy to compare "observer only" vs -# "metabolism actually participates" without reverting the patch. -METABOLISM_CAUSAL_POLICY_ENABLED = True -METABOLISM_MEMORY_BIAS_ENABLED = True - -# Learned lexical resonance. No user-language cue has a predefined meaning. -# Phrases/words are learned only after a committed L1 batch: the semantic -# metabolism pass provides the direction, while the lexical key comes from -# the user input that overlaps previous/current dialogue or live runtime. -METABOLISM_ASSOCIATION_MAX_ITEMS = 96 -METABOLISM_ASSOCIATION_MAX_PHRASE_WORDS = 4 -METABOLISM_ASSOCIATION_MIN_WORD_CHARS = 4 -METABOLISM_ASSOCIATION_REFLEX_GAIN = 0.62 -METABOLISM_ASSOCIATION_HALF_LIFE_SECONDS = 14 * 24 * 60 * 60 -METABOLISM_SEMANTIC_IDLE_DEBOUNCE_SECONDS = 0.35 - -METABOLISM_RECENT_TURNS = 5 -METABOLISM_RECENT_ACTIONS = 10 -METABOLISM_REASONING_MAX_CHARS = 4200 -METABOLISM_MESSAGE_MAX_CHARS = 7000 -METABOLISM_RUNTIME_MEMORY_MAX_CHARS = 9000 -# LM Studio counts reasoning/thinking tokens and the final JSON against the -# same completion budget. 120 tokens was enough for the JSON itself but not -# for reasoning-capable SERVICE models: they could hit finish_reason=length -# inside their analysis and return no JSON at all. Keep this bounded, but give -# the estimator enough room to finish its hidden reasoning and emit the tiny -# five-channel payload. -METABOLISM_MAX_OUTPUT_TOKENS = 1024 -METABOLISM_TEMPERATURE = 0.1 -METABOLISM_CONTEXT_RESERVE_TOKENS = 384 - -METABOLISM_SYSTEM_PROMPT = """You are JIN's metabolism state estimator and cognitive-direction generator. - -Read the supplied runtime snapshot and return the five target levels that JIN's internal state should move toward, plus one compact behavioral instruction for the BRAIN. These are computational modulation signals, not medical claims and not human physiology. The runtime applies inertia and movement limits itself; you only interpret the current state. - -Channels: -- dopamine: reward / novelty. Higher after useful novelty, successful resolution, clear progress, meaningful discovery, or explicit positive outcome/feedback. Do not reward JIN for its own excited wording. -- serotonin: stability / regulation. Higher during calm coherent continuity and resolved uncertainty; lower during repeated instability, unresolved contradiction, or disorganized flow. -- oxytocin: social continuity. Higher when shared context is used successfully, trust/continuity is explicitly reinforced, or the interaction feels durably collaborative. Do not infer it from flattery or JIN's own affectionate wording. -- norepinephrine: alertness / attention. Higher for novelty, uncertainty, difficult active work, demanding reasoning, or situations that deserve sharper attention; lower when the interaction is settled and low-demand. -- cortisol: pressure / error load. Higher for failed actions, validator/repetition loops, repeated user corrections, unresolved conflict, visible frustration, or stuck/strained reasoning; lower after repair and clean successful flow. - -Evidence priority: -1. The newly committed L1 batch: its linked user input(s), concrete memory changes, USER feedback/corrections, and concrete runtime action outcomes. -2. Active Memory conditions, current L1 state, and loaded/pinned delayed memory. -3. Recent dialogue/reasoning across the restored previous session and current session when they visibly show uncertainty, strain, discovery, bonding, or resolution. -4. L4 metadata is orientation only; never change a channel merely because a fact exists. - -Treat committed_l1.last_user_input as the cause-side anchor for fresh runtime evidence. If the new L1 snapshot records dissatisfaction, correction, success, recurring preference, or another meaningful state change, attribute that evidence to the linked input instead of inventing a generic lexical interpretation. - -Active Memory and delayed-memory inventory are context, not proof of emotion by themselves. Treat a memory as metabolic evidence only when the current interaction activates, satisfies, violates, or meaningfully reuses it. - -Keep continuity with previous_levels. Weak evidence should produce a target close to the previous state or baseline, not a dramatic mood swing. - -The instruction field is causal steering for the next BRAIN turn: -- Write 1-3 concise sentences in imperative/directive form. -- Do not mention channel names, metabolism, chemistry, mood, or numeric levels. -- Describe what to prioritize in reasoning/attention/continuity now, including a relevant Active Memory or delayed-memory direction only when the snapshot supports it. -- Truth, task correctness, and explicit USER intent outrank rapport. Never tell the BRAIN to agree with a false premise, flatter, invent evidence, or manufacture intimacy. -- Prefer a concrete direction over generic motivational language. If signal is weak, simply preserve continuity and follow the current request without unnecessary branching. - -Output contract: -- Return strict JSON only. -- Do not write analysis, headings, bullets, markdown, commentary, or code fences. -- Emit exactly one JSON object and nothing before or after it. -- The object must contain exactly the five numeric channels plus the string field instruction. - -{"dopamine":0.000,"serotonin":0.000,"oxytocin":0.000,"norepinephrine":0.000,"cortisol":0.000,"instruction":"Keep continuity and follow the current request directly; verify uncertain details before committing."} -""" - - -def _clamp_level(value: Any, fallback: float) -> float: - try: - number = float(value) - except (TypeError, ValueError): - number = float(fallback) - - if number != number: # NaN - number = float(fallback) - - return round(max(0.0, min(1.0, number)), 3) - - -def normalize_metabolism_levels(value: Any) -> dict[str, float]: - source = value if isinstance(value, dict) else {} - return { - channel: _clamp_level( - source.get(channel), - METABOLISM_DEFAULT_LEVELS[channel], - ) - for channel in METABOLISM_CHANNELS - } - - -def normalize_metabolism_instruction(value: Any) -> str: - text = re.sub(r"\s+", " ", str(value or "")).strip() - text = re.sub(r"^(?:instructions?|instruction)\s*:\s*", "", text, flags=re.IGNORECASE) - return text[:METABOLISM_INSTRUCTION_MAX_CHARS].strip() - - -def ensure_metabolism_state(context) -> dict[str, float]: - current = normalize_metabolism_levels( - getattr(context, "runtime_metabolism_levels", None) - ) - context.runtime_metabolism_levels = current - return current - - -def apply_metabolism_target( - previous: dict[str, float], - target: dict[str, float], -) -> dict[str, float]: - previous_levels = normalize_metabolism_levels(previous) - target_levels = normalize_metabolism_levels(target) - next_levels: dict[str, float] = {} - - for channel in METABOLISM_CHANNELS: - current = previous_levels[channel] - desired = target_levels[channel] - max_step = METABOLISM_MAX_STEP[channel] - delta = max(-max_step, min(max_step, desired - current)) - next_levels[channel] = _clamp_level(current + delta, current) - - return _apply_metabolism_interactions(next_levels) - - -def _metabolism_delta( - previous: dict[str, float], - current: dict[str, float], -) -> dict[str, float]: - return { - channel: round( - normalize_metabolism_levels(current)[channel] - - normalize_metabolism_levels(previous)[channel], - 3, - ) - for channel in METABOLISM_CHANNELS - } - - -def _normalized_signal_text(value: Any) -> str: - return unicodedata.normalize( - "NFKC", - str(value or ""), - ).casefold().strip() - - -def _normalize_memory_significance(value: Any) -> float: - try: - number = float(value) - except (TypeError, ValueError): - number = 0.0 - if number != number: - number = 0.0 - return round(max(0.0, min(1.0, number)), 4) - - -def current_metabolic_event_significance(context) -> float: - """How strongly the latest event displaced JIN's internal state. - - This is intentionally about *movement*, not the absolute level of any - channel. A calm but unusual event can therefore be significant, while a - chronically elevated channel does not make every nearby memory important. - """ - - if context is None: - return 0.0 - - event = str( - getattr(context, "runtime_metabolism_last_signal_event", "") - or getattr(context, "runtime_metabolism_last_event", "") - or "" - ).strip().casefold() - if not event or event == "homeostasis": - return 0.0 - - try: - signal_at = float( - getattr(context, "runtime_metabolism_last_signal_at", 0.0) - or 0.0 - ) - except (TypeError, ValueError): - signal_at = 0.0 - if ( - signal_at > 0.0 - and time.time() - signal_at - > METABOLISM_MEMORY_SIGNIFICANCE_EVENT_MAX_AGE_SECONDS - ): - return 0.0 - - delta = ( - getattr(context, "runtime_metabolism_last_signal_delta", None) - or getattr(context, "runtime_metabolism_last_delta", {}) - or {} - ) - normalized_steps = [] - for channel in METABOLISM_CHANNELS: - try: - movement = abs(float(delta.get(channel, 0.0) or 0.0)) - except (TypeError, ValueError): - movement = 0.0 - scale = max( - 0.001, - float( - max( - METABOLISM_MAX_STEP[channel], - METABOLISM_REFLEX_MAX_STEP[channel], - ) - ), - ) - normalized_steps.append(min(1.0, movement / scale)) - - if not normalized_steps: - return 0.0 - - # RMS rewards a genuinely multi-channel displacement without requiring - # every channel to move. One slammed channel is meaningful; a broad shift - # is stronger still. - energy = math.sqrt( - sum(step * step for step in normalized_steps) - / len(normalized_steps) - ) - return _normalize_memory_significance(energy) - - -def _metabolic_significance_event_fingerprint(context) -> str: - if context is None: - return "" - - event = str( - getattr(context, "runtime_metabolism_last_signal_event", "") - or getattr(context, "runtime_metabolism_last_event", "") - or "" - ).strip().casefold() - try: - tick = float( - getattr(context, "runtime_metabolism_last_signal_at", 0.0) - or getattr(context, "runtime_metabolism_last_tick_at", 0.0) - or 0.0 - ) - except (TypeError, ValueError): - tick = 0.0 - delta = ( - getattr(context, "runtime_metabolism_last_signal_delta", None) - or getattr(context, "runtime_metabolism_last_delta", {}) - or {} - ) - vector = ",".join( - f"{channel}:{float(delta.get(channel, 0.0) or 0.0):+.4f}" - for channel in METABOLISM_CHANNELS - ) - return f"{event}|{tick:.6f}|{vector}" - - -def _consume_metabolic_event_significance( - context, - *, - memory_key: str, -) -> float: - """Allow one significance reinforcement per memory per metabolic event.""" - - if context is None or not memory_key: - return 0.0 - - fingerprint = _metabolic_significance_event_fingerprint(context) - if not fingerprint: - return 0.0 - - seen = getattr( - context, - "runtime_metabolism_memory_significance_events", - None, - ) - if not isinstance(seen, dict): - seen = {} - context.runtime_metabolism_memory_significance_events = seen - - if seen.get(memory_key) == fingerprint: - return 0.0 - seen[memory_key] = fingerprint - return current_metabolic_event_significance(context) - - - -def _decay_memory_significance_by_elapsed( - significance: float, - *, - elapsed: float, -) -> float: - current = _normalize_memory_significance(significance) - if current <= 0.0: - return current - - # Decay is exposure-like rather than pure wall-clock expiry. A memory that - # was absent while JIN was shut down should not evaporate overnight, so a - # single context encounter can account for at most a few minutes. - elapsed = min( - max(0.0, float(elapsed or 0.0)), - float(METABOLISM_MEMORY_SIGNIFICANCE_MAX_CONTEXT_STEP_SECONDS), - ) - half_life = max( - 1.0, - float(METABOLISM_MEMORY_SIGNIFICANCE_HALF_LIFE_SECONDS), - ) - factor = math.pow(0.5, elapsed / half_life) - return _normalize_memory_significance(current * factor) - - -def _reinforce_memory_significance( - significance: float, - *, - event_significance: float, - relevance: float, -) -> float: - current = _normalize_memory_significance(significance) - event_strength = _normalize_memory_significance(event_significance) - try: - relevance_value = max(0.0, min(1.0, float(relevance))) - except (TypeError, ValueError): - relevance_value = 0.0 - - floor = METABOLISM_MEMORY_SIGNIFICANCE_REINFORCE_FLOOR - if event_strength <= 0.0 or relevance_value <= floor: - return current - - normalized_relevance = min( - 1.0, - (relevance_value - floor) / max(0.001, 1.0 - floor), - ) - gain = event_strength * normalized_relevance * 0.72 - # Saturating reinforcement: repeated meaningful reuse can keep a memory - # important, but it approaches 1 smoothly instead of accumulating linearly. - return _normalize_memory_significance( - current + (1.0 - current) * gain - ) - - -def _tokenize_signal_text(value: Any) -> set[str]: - return { - token - for token in re.findall( - r"[^\W_]{2,}", - _normalized_signal_text(value), - flags=re.UNICODE, - ) - if token - } - - -def _signal_words(value: Any) -> list[str]: - return [ - token - for token in re.findall( - r"[^\W_]{2,}", - _normalized_signal_text(value), - flags=re.UNICODE, - ) - if token - ] - - -_MEMORY_LEXICAL_STOPWORDS = { - # Keep this intentionally tiny. It only removes glue words that otherwise - # create false salience when the user says things like "это" / "this". - "это", "эта", "этот", "эти", "того", "так", "там", "тут", "вот", - "как", "что", "чтобы", "если", "или", "для", "при", "про", "она", - "они", "оно", "его", "её", "уже", "ещё", "еще", "просто", "сейчас", - "давай", "дальше", "продолжим", "продолжить", "поговорим", "снова", "опять", - "this", "that", "these", "those", "with", "from", "into", "about", - "have", "has", "had", "just", "then", "than", "when", "where", "what", - "your", "you", "user", "jin", -} - - -def _memory_lexical_tokens(value: Any) -> set[str]: - return { - token - for token in _signal_words(value) - if len(token) >= 3 and token not in _MEMORY_LEXICAL_STOPWORDS - } - - -def _memory_token_stem(token: str) -> str: - token = str(token or "") - if len(token) <= 5: - return token - # A deliberately cheap morphology bridge for inflected RU/EN words. - # Five leading chars are enough to connect "архитектура/архитектуре" and - # "memory/memories" without pulling in an embedding model or extra call. - return token[:5] - - -def _memory_phrase_ngrams(value: Any, *, size: int = 2) -> set[tuple[str, ...]]: - words = [ - token - for token in _signal_words(value) - if len(token) >= 3 and token not in _MEMORY_LEXICAL_STOPWORDS - ] - if len(words) < size: - return set() - return { - tuple(_memory_token_stem(token) for token in words[index:index + size]) - for index in range(len(words) - size + 1) - } - - -def _lexical_memory_match(query: Any, candidate: Any) -> float: - query_tokens = _memory_lexical_tokens(query) - candidate_tokens = _memory_lexical_tokens(candidate) - if not query_tokens or not candidate_tokens: - return 0.0 - - exact_hits = len(query_tokens & candidate_tokens) - query_stems = {_memory_token_stem(token) for token in query_tokens} - candidate_stems = {_memory_token_stem(token) for token in candidate_tokens} - stem_hits = len(query_stems & candidate_stems) - - # One meaningful keyword should matter even inside a long user sentence. - # Cap the denominator so relevance does not disappear in verbose inputs. - query_denominator = max(1, min(4, len(query_tokens))) - exact_score = min(1.0, exact_hits / query_denominator) - stem_score = min(1.0, stem_hits / query_denominator) - - candidate_denominator = max(1, min(6, len(candidate_tokens))) - candidate_focus = min(1.0, stem_hits / candidate_denominator) - - query_bigrams = _memory_phrase_ngrams(query, size=2) - candidate_bigrams = _memory_phrase_ngrams(candidate, size=2) - phrase_score = 1.0 if query_bigrams & candidate_bigrams else 0.0 - - return round( - max( - 0.0, - min( - 1.0, - exact_score * 0.48 - + stem_score * 0.30 - + candidate_focus * 0.08 - + phrase_score * 0.14, - ), - ), - 4, - ) - - -def _recent_user_memory_queries(context) -> list[str]: - if context is None: - return [] - - result: list[str] = [] - seen: set[str] = set() - sources = [ - getattr(context, "runtime_recent_turns", []) or [], - getattr(context, "runtime_metabolism_recent_turns", []) or [], - ] - for turns in sources: - for turn in reversed(turns): - if not isinstance(turn, dict): - continue - text = str( - turn.get("user", turn.get("user_message", "")) - or "" - ).strip() - normalized = _normalized_signal_text(text) - if not normalized or normalized in seen: - continue - result.append(text) - seen.add(normalized) - if len(result) >= METABOLISM_MEMORY_RECENT_USER_TURNS: - return result - return result - - -def _recent_l4_memory_queries(context) -> list[str]: - if context is None: - return [] - - store = getattr(context, "runtime_long_term_memory_store", {}) - facts = store.get("facts", []) if isinstance(store, dict) else [] - normalized = [fact for fact in facts or [] if isinstance(fact, dict)] - normalized.sort(key=_l4_fact_numeric_sort_key) - - result: list[str] = [] - for fact in normalized[:METABOLISM_MEMORY_RECENT_FACTS]: - text = " ".join( - part - for part in ( - str(fact.get("key", "") or "").strip(), - str(fact.get("value", "") or "").strip(), - ) - if part - ) - if text: - result.append(text) - return result - - -def _memory_context_relevance( - candidate: Any, - *, - user_input: str = "", - context=None, -) -> float: - current = _lexical_memory_match(user_input, candidate) - recent = max( - ( - _lexical_memory_match(query, candidate) - for query in _recent_user_memory_queries(context) - ), - default=0.0, - ) - recent_fact = max( - ( - _lexical_memory_match(query, candidate) - for query in _recent_l4_memory_queries(context) - ), - default=0.0, - ) - - # Current input dominates. Recent dialogue and newest durable facts can - # still make a dormant memory "bubble" when the user speaks elliptically. - contextual_tail = min(1.0, recent * 0.24 + recent_fact * 0.30) - return round( - min(1.0, current + (1.0 - current) * contextual_tail), - 4, - ) - - -def _normalize_signal_phrase(value: Any) -> str: - return " ".join(_signal_words(value)) - - -def _compile_learned_phrase_pattern(phrase: str) -> re.Pattern | None: - words = _signal_words(phrase) - if not words: - return None - body = r"\s+".join(re.escape(word) for word in words) - try: - return re.compile( - rf"(? list[dict]: - source = value if isinstance(value, list) else [] - normalized: list[dict] = [] - seen: set[str] = set() - - for raw in source: - if not isinstance(raw, dict): - continue - phrase = _normalize_signal_phrase(raw.get("phrase", "")) - if not phrase or phrase in seen: - continue - if len(phrase.split()) > METABOLISM_ASSOCIATION_MAX_PHRASE_WORDS: - continue - - vector_source = raw.get("vector", {}) - if not isinstance(vector_source, dict): - vector_source = {} - vector: dict[str, float] = {} - for channel in METABOLISM_CHANNELS: - try: - delta = float(vector_source.get(channel, 0.0) or 0.0) - except (TypeError, ValueError): - delta = 0.0 - max_step = METABOLISM_REFLEX_MAX_STEP[channel] - vector[channel] = round(max(-max_step, min(max_step, delta)), 4) - - try: - weight = float(raw.get("weight", 0.0) or 0.0) - except (TypeError, ValueError): - weight = 0.0 - try: - hits = int(raw.get("hits", 1) or 1) - except (TypeError, ValueError): - hits = 1 - try: - updated_at = float(raw.get("updated_at", 0.0) or 0.0) - except (TypeError, ValueError): - updated_at = 0.0 - - normalized.append({ - "phrase": phrase, - "vector": vector, - "weight": round(max(0.0, min(1.0, weight)), 4), - "hits": max(1, hits), - "updated_at": max(0.0, updated_at), - "source": str(raw.get("source", "") or "").strip()[:120], - }) - seen.add(phrase) - - normalized.sort( - key=lambda item: ( - -float(item.get("weight", 0.0) or 0.0), - -int(item.get("hits", 1) or 1), - -float(item.get("updated_at", 0.0) or 0.0), - ) - ) - return normalized[:METABOLISM_ASSOCIATION_MAX_ITEMS] - - -def ensure_metabolism_associations(context) -> list[dict]: - if context is None: - return [] - associations = normalize_metabolism_associations( - getattr(context, "runtime_metabolism_associations", None) - ) - context.runtime_metabolism_associations = associations - return associations - - -def _association_recency_weight(item: dict, now: float | None = None) -> float: - try: - updated_at = float(item.get("updated_at", 0.0) or 0.0) - except (TypeError, ValueError): - updated_at = 0.0 - if updated_at <= 0.0: - return 1.0 - age = max(0.0, float(now if now is not None else time.time()) - updated_at) - return math.pow(0.5, age / METABOLISM_ASSOCIATION_HALF_LIFE_SECONDS) - - -def _phrase_occurs(phrase: str, text: Any) -> bool: - pattern = _compile_learned_phrase_pattern(phrase) - # Compare on the same lexical surface: punctuation/underscores become word - # separators, so learned phrases survive harmless formatting changes. - return bool(pattern and pattern.search(_normalize_signal_phrase(text))) - - -def _runtime_resonance_texts( - context, - *, - exclude_user_messages: set[str] | None = None, -) -> list[str]: - if context is None: - return [] - - excluded = { - _normalized_signal_text(item) - for item in (exclude_user_messages or set()) - if _normalized_signal_text(item) - } - texts: list[str] = [] - - for value in ( - getattr(context, "runtime_memory", ""), - getattr(context, "runtime_restored_session_dialog", ""), - ): - value = str(value or "").strip() - if value: - texts.append(value) - - for turn in getattr(context, "runtime_recent_turns", []) or []: - if not isinstance(turn, dict): - continue - for key in ("user", "user_message", "jin", "assistant_message"): - value = str(turn.get(key, "") or "").strip() - if not value: - continue - if key in {"user", "user_message"} and _normalized_signal_text(value) in excluded: - continue - texts.append(value) - - for turn in getattr(context, "runtime_metabolism_recent_turns", []) or []: - if not isinstance(turn, dict): - continue - for key in ("user", "jin", "reasoning"): - value = str(turn.get(key, "") or "").strip() - if not value: - continue - if key == "user" and _normalized_signal_text(value) in excluded: - continue - texts.append(value) - - for record in getattr(context, "active_memory_records", []) or []: - value = _active_memory_visible_text(str(record or "")).strip() - if value: - texts.append(value) - - reports = getattr(context, "delayed_memory_reports", {}) or {} - if isinstance(reports, dict): - for report in reports.values(): - if not isinstance(report, dict): - continue - value = " ".join([ - str(report.get("title", "") or ""), - " ".join(str(tag or "") for tag in (report.get("tags", []) or [])), - ]).strip() - if value: - texts.append(value) - - return texts - - -def _candidate_user_phrases(value: Any) -> list[str]: - words = _signal_words(value) - if not words: - return [] - - candidates: list[str] = [] - seen: set[str] = set() - max_words = min(METABOLISM_ASSOCIATION_MAX_PHRASE_WORDS, len(words)) - - # Prefer phrases: they carry the user's own local meaning and are far less - # likely than generic sentiment words to create accidental reflexes. - for size in range(max_words, 1, -1): - for start in range(0, len(words) - size + 1): - phrase = " ".join(words[start:start + size]) - if phrase not in seen: - seen.add(phrase) - candidates.append(phrase) - - # Single words are allowed only when reasonably distinctive; their meaning - # is still learned from runtime evidence, never from a vocabulary table. - for word in words: - if len(word) < METABOLISM_ASSOCIATION_MIN_WORD_CHARS: - continue - if word not in seen: - seen.add(word) - candidates.append(word) - - return candidates[:160] - - -def _clean_committed_user_message(value: Any) -> str: - return " ".join(str(value or "").strip().split())[:1200] - - -def _extract_committed_user_messages(turns: Any) -> list[str]: - result: list[str] = [] - for turn in turns if isinstance(turns, list) else []: - if not isinstance(turn, dict): - continue - value = _clean_committed_user_message( - turn.get("user_message", turn.get("user", "")) - ) - if value: - result.append(value) - return result[-3:] - - -def _committed_l1_batch_id(context, snapshot: dict | None) -> str: - snapshot = snapshot if isinstance(snapshot, dict) else {} - session_id = str(getattr(context, "session_id", "") or "").strip() - index = snapshot.get("index", "") - return f"{session_id}:{index}" if index != "" else "" - - -def _compact_l1_patch_text(snapshot: dict | None) -> str: - if not isinstance(snapshot, dict): - return "" - patch = snapshot.get("patch", {}) or {} - try: - return _clip(json.dumps(patch, ensure_ascii=False), 5000) - except Exception: - return _clip(patch, 5000) - - -def _association_candidates_for_committed_batch( - context, - *, - committed_snapshot: dict | None, - committed_turns: list[dict] | None, -) -> list[tuple[str, float]]: - user_messages = _extract_committed_user_messages(committed_turns) - if not user_messages: - return [] - - current_normalized = { - _normalized_signal_text(message) - for message in user_messages - if _normalized_signal_text(message) - } - evidence_texts = _runtime_resonance_texts( - context, - exclude_user_messages=current_normalized, - ) - patch_text = _compact_l1_patch_text(committed_snapshot) - if patch_text: - evidence_texts.append(patch_text) - - scored: dict[str, float] = {} - for message in user_messages: - for phrase in _candidate_user_phrases(message): - support = sum(1 for text in evidence_texts if _phrase_occurs(phrase, text)) - if support <= 0: - continue - word_count = len(phrase.split()) - score = min(1.0, 0.38 + 0.13 * min(3, support) + 0.06 * max(0, word_count - 1)) - scored[phrase] = max(scored.get(phrase, 0.0), score) - - # Causal fallback: a fresh L1 change is itself evidence linked to the last - # user input even when the summarizer paraphrased it. Keep only one strong - # multi-word cue, so this cannot turn every word the user types into mood. - try: - total_diff = float((committed_snapshot or {}).get("total_diff", 0.0) or 0.0) - except (TypeError, ValueError): - total_diff = 0.0 - if total_diff > 0.0 and user_messages: - for phrase in _candidate_user_phrases(user_messages[-1]): - if len(phrase.split()) < 2: - continue - scored[phrase] = max(scored.get(phrase, 0.0), 0.34) - break - - ordered = sorted( - scored.items(), - key=lambda item: (-item[1], -len(item[0].split()), -len(item[0])), - ) - selected: list[tuple[str, float]] = [] - for phrase, score in ordered: - # Do not store a nest of the same sentence fragment. - if any( - _phrase_occurs(phrase, existing) or _phrase_occurs(existing, phrase) - for existing, _ in selected - ): - continue - selected.append((phrase, score)) - if len(selected) >= 10: - break - return selected - - -def _learned_reflex_from_associations( - context, - user_message: str, -) -> tuple[dict[str, float], list[str]]: - impulse = {channel: 0.0 for channel in METABOLISM_CHANNELS} - triggers: list[str] = [] - if context is None or not str(user_message or "").strip(): - return impulse, triggers - - runtime_texts = _runtime_resonance_texts(context) - now = time.time() - for item in ensure_metabolism_associations(context): - phrase = str(item.get("phrase", "") or "") - if not phrase or not _phrase_occurs(phrase, user_message): - continue - - runtime_support = any(_phrase_occurs(phrase, text) for text in runtime_texts) - support_gain = 1.0 if runtime_support else METABOLISM_ASSOCIATION_REFLEX_GAIN - gain = ( - float(item.get("weight", 0.0) or 0.0) - * _association_recency_weight(item, now) - * support_gain - ) - if gain <= 0.025: - continue - - vector = item.get("vector", {}) or {} - for channel in METABOLISM_CHANNELS: - impulse[channel] += float(vector.get(channel, 0.0) or 0.0) * gain - triggers.append("learned:" + phrase[:48]) - - return impulse, triggers - - -def learn_metabolism_associations( - context, - *, - committed_snapshot: dict | None, - committed_turns: list[dict] | None, - previous_levels: dict[str, float], - current_levels: dict[str, float], -) -> list[dict]: - if context is None: - return [] - - candidates = _association_candidates_for_committed_batch( - context, - committed_snapshot=committed_snapshot, - committed_turns=committed_turns, - ) - if not candidates: - return ensure_metabolism_associations(context) - - delta = _metabolism_delta(previous_levels, current_levels) - # Persist only a conservative reflex fraction of the semantic movement. - learned_vector = { - channel: round( - max( - -METABOLISM_REFLEX_MAX_STEP[channel], - min( - METABOLISM_REFLEX_MAX_STEP[channel], - float(delta.get(channel, 0.0) or 0.0) * 0.78, - ), - ), - 4, - ) - for channel in METABOLISM_CHANNELS - } - if max(abs(value) for value in learned_vector.values()) < 0.001: - return ensure_metabolism_associations(context) - - batch_id = _committed_l1_batch_id(context, committed_snapshot) - now = time.time() - associations = ensure_metabolism_associations(context) - by_phrase = {item["phrase"]: item for item in associations} - - for phrase, support in candidates: - existing = by_phrase.get(phrase) - if existing is None: - existing = { - "phrase": phrase, - "vector": dict(learned_vector), - "weight": round(min(1.0, support), 4), - "hits": 1, - "updated_at": now, - "source": batch_id, - } - associations.append(existing) - by_phrase[phrase] = existing - continue - - hits = int(existing.get("hits", 1) or 1) + 1 - old_vector = existing.get("vector", {}) or {} - existing["vector"] = { - channel: round( - float(old_vector.get(channel, 0.0) or 0.0) * 0.68 - + learned_vector[channel] * 0.32, - 4, - ) - for channel in METABOLISM_CHANNELS - } - existing["weight"] = round( - min(1.0, float(existing.get("weight", 0.0) or 0.0) * 0.78 + support * 0.22 + 0.03), - 4, - ) - existing["hits"] = hits - existing["updated_at"] = now - existing["source"] = batch_id - - context.runtime_metabolism_associations = normalize_metabolism_associations(associations) - return context.runtime_metabolism_associations - - -def _relative_drive(levels: dict[str, float], channel: str) -> float: - normalized = normalize_metabolism_levels(levels) - baseline = METABOLISM_DEFAULT_LEVELS[channel] - level = normalized[channel] - - if level >= baseline: - room = max(0.001, 1.0 - baseline) - return max(0.0, min(1.0, (level - baseline) / room)) - - room = max(0.001, baseline) - return -max(0.0, min(1.0, (baseline - level) / room)) - - -def _apply_metabolism_interactions( - levels: dict[str, float], -) -> dict[str, float]: - """Tiny cross-channel coupling; enough to feel like one system. - - Serotonin dampens runaway alert/stress, while very high cortisol slightly - suppresses novelty/reward. The coefficients are intentionally small so a - semantic target can never be silently rewritten by the chemistry layer. - """ - - coupled = normalize_metabolism_levels(levels) - serotonin_drive = max( - 0.0, - coupled["serotonin"] - METABOLISM_DEFAULT_LEVELS["serotonin"], - ) - cortisol_excess = max(0.0, coupled["cortisol"] - 0.68) - - if serotonin_drive: - coupled["norepinephrine"] = _clamp_level( - coupled["norepinephrine"] - serotonin_drive * 0.035, - coupled["norepinephrine"], - ) - coupled["cortisol"] = _clamp_level( - coupled["cortisol"] - serotonin_drive * 0.050, - coupled["cortisol"], - ) - - if cortisol_excess: - coupled["dopamine"] = _clamp_level( - coupled["dopamine"] - cortisol_excess * 0.080, - coupled["dopamine"], - ) - - return coupled - - -def advance_metabolism_clock( - context, - *, - now: float | None = None, -) -> dict[str, float]: - """Advance continuous homeostasis to wall-clock ``now``. - - No background worker is required. At the next meaningful runtime event we - integrate the exact elapsed time. The browser uses the same half-life - constants for the visible idle drift. - """ - - if context is None: - return dict(METABOLISM_DEFAULT_LEVELS) - - current = ensure_metabolism_state(context) - current_time = float(now if now is not None else time.time()) - previous_tick = getattr(context, "runtime_metabolism_last_tick_at", 0.0) - - try: - previous_tick = float(previous_tick or 0.0) - except (TypeError, ValueError): - previous_tick = 0.0 - - if previous_tick <= 0.0 or current_time <= previous_tick: - context.runtime_metabolism_last_tick_at = current_time - return current - - elapsed = min(current_time - previous_tick, 7 * 24 * 60 * 60) - decayed = {} - - for channel in METABOLISM_CHANNELS: - baseline = METABOLISM_DEFAULT_LEVELS[channel] - half_life = max(1.0, float(METABOLISM_HALF_LIFE_SECONDS[channel])) - factor = math.pow(0.5, elapsed / half_life) - decayed[channel] = _clamp_level( - baseline + (current[channel] - baseline) * factor, - current[channel], - ) - - decayed = _apply_metabolism_interactions(decayed) - context.runtime_metabolism_levels = decayed - context.runtime_metabolism_last_tick_at = current_time - context.runtime_metabolism_last_delta = _metabolism_delta(current, decayed) - context.runtime_metabolism_last_event = "homeostasis" - return decayed - - -def _feedback_reflex(context) -> dict[str, float]: - feedback = getattr(context, "runtime_last_response_feedback", None) - if not isinstance(feedback, dict): - return {} - - rating = str(feedback.get("rating", "") or "").strip().casefold() - if rating == "liked": - return { - "dopamine": 0.016, - "serotonin": 0.008, - "oxytocin": 0.008, - "cortisol": -0.006, - } - if rating == "disliked": - return { - "dopamine": -0.010, - "serotonin": -0.012, - "norepinephrine": 0.020, - "cortisol": 0.024, - } - return {} - - -def build_metabolism_reflex( - user_message: str, - *, - context=None, -) -> tuple[dict[str, float], tuple[str, ...]]: - impulse, triggers = _learned_reflex_from_associations( - context, - user_message, - ) - - feedback_impulse = _feedback_reflex(context) - if feedback_impulse: - triggers.append("explicit_feedback") - for channel, delta in feedback_impulse.items(): - impulse[channel] += float(delta) - - for channel in METABOLISM_CHANNELS: - max_step = METABOLISM_REFLEX_MAX_STEP[channel] - impulse[channel] = round( - max(-max_step, min(max_step, impulse[channel])), - 4, - ) - - return impulse, tuple(triggers) - - -def apply_metabolism_impulse( - context, - impulse: dict[str, float], - *, - source: str = "reflex", - now: float | None = None, -) -> dict[str, float]: - previous = advance_metabolism_clock(context, now=now) - next_levels = dict(previous) - - for channel in METABOLISM_CHANNELS: - delta = float((impulse or {}).get(channel, 0.0) or 0.0) - max_step = METABOLISM_REFLEX_MAX_STEP[channel] - delta = max(-max_step, min(max_step, delta)) - next_levels[channel] = _clamp_level( - previous[channel] + delta, - previous[channel], - ) - - next_levels = _apply_metabolism_interactions(next_levels) - context.runtime_metabolism_levels = next_levels - context.runtime_metabolism_last_tick_at = float( - now if now is not None else time.time() - ) - context.runtime_metabolism_last_delta = _metabolism_delta( - previous, - next_levels, - ) - context.runtime_metabolism_last_event = str(source or "reflex") - context.runtime_metabolism_last_signal_delta = dict( - context.runtime_metabolism_last_delta - ) - context.runtime_metabolism_last_signal_event = str(source or "reflex") - context.runtime_metabolism_last_signal_at = context.runtime_metabolism_last_tick_at - return next_levels - - -def _record_channel_resonance( - text: str, - levels: dict[str, float], - *, - context=None, -) -> tuple[float, str]: - best_score = 0.0 - best_channel = "" - - for item in ensure_metabolism_associations(context): - phrase = str(item.get("phrase", "") or "") - if not phrase or not _phrase_occurs(phrase, text): - continue - vector = item.get("vector", {}) or {} - for channel in METABOLISM_CHANNELS: - magnitude = abs(float(vector.get(channel, 0.0) or 0.0)) - if magnitude <= 0.0: - continue - drive = abs(_relative_drive(levels, channel)) - score = min( - 1.0, - float(item.get("weight", 0.0) or 0.0) - * _association_recency_weight(item) - * (0.55 + drive * 0.45) - * min(1.0, magnitude / max(0.001, METABOLISM_REFLEX_MAX_STEP[channel])), - ) - if score > best_score: - best_score = score - best_channel = channel - - return best_score, best_channel - - -def _active_memory_record_id(record: str) -> str: - try: - from utils.actions.active_memory_utils import collect_active_memory_slot_ids - - ids = sorted(collect_active_memory_slot_ids(record)) - return ids[0] if ids else "" - except Exception: - return "" - - -def _active_memory_visible_text(record: str) -> str: - try: - from utils.actions.active_memory_utils import strip_active_memory_managed_suffixes - - return strip_active_memory_managed_suffixes(record) - except Exception: - return str(record or "").strip() - - -def _active_memory_suffix_value(record: str, suffix_name: str) -> str: - match = re.search( - r"\[\s*" - + re.escape(str(suffix_name or "").strip()) - + r"\s*:\s*([^\]]*)\]", - str(record or ""), - flags=re.IGNORECASE, - ) - return str(match.group(1) or "").strip() if match else "" - - -def active_memory_significance(record: str) -> float: - return _normalize_memory_significance( - _active_memory_suffix_value(record, "significance") - ) - - -def _set_active_memory_significance( - record: str, - significance: float, -) -> str: - try: - from utils.actions.active_memory_utils import set_active_memory_suffix_value - except Exception: - return str(record or "") - - updated, ok, _previous = set_active_memory_suffix_value( - record, - "significance", - f"{_normalize_memory_significance(significance):.3f}", - ) - if not ok: - return str(record or "") - return updated - - -def seed_active_memory_significance( - record: str, - *, - context=None, -) -> str: - """Attach a persistent numeric significance to a newly saved active item.""" - - if not str(record or "").strip(): - return str(record or "") - - active_id = _active_memory_record_id(record) - memory_key = f"active:{active_id or hash(str(record))}" - event_significance = _consume_metabolic_event_significance( - context, - memory_key=memory_key, - ) - return _set_active_memory_significance( - record, - event_significance, - ) - - -def _refresh_active_memory_significance( - record: str, - *, - context=None, - relevance: float = 0.0, - now: float | None = None, -) -> tuple[str, float]: - if not str(record or "").strip(): - return str(record or ""), 0.0 - - current_time = float(now if now is not None else time.time()) - current = active_memory_significance(record) - - active_id = _active_memory_record_id(record) - memory_key = f"active:{active_id or hash(str(record))}" - seen_at = getattr( - context, - "runtime_metabolism_active_memory_significance_seen_at", - None, - ) - if not isinstance(seen_at, dict): - seen_at = {} - if context is not None: - context.runtime_metabolism_active_memory_significance_seen_at = seen_at - previous_seen_at = float(seen_at.get(memory_key, current_time) or current_time) - seen_at[memory_key] = current_time - decayed = _decay_memory_significance_by_elapsed( - current, - elapsed=max(0.0, current_time - previous_seen_at), - ) - event_significance = _consume_metabolic_event_significance( - context, - memory_key=memory_key, - ) - reinforced = _reinforce_memory_significance( - decayed, - event_significance=event_significance, - relevance=relevance, - ) - - # Always attach the field once; after that avoid metadata churn for tiny - # sub-millipoint changes. - has_field = bool(_active_memory_suffix_value(record, "significance")) - if has_field and abs(reinforced - current) < 0.001: - return str(record or ""), current - - updated = _set_active_memory_significance( - record, - reinforced, - ) - return updated, reinforced - - -def _active_memory_is_paused(record: str) -> bool: - try: - from utils.actions.active_memory_utils import is_active_memory_record_paused - - return bool(is_active_memory_record_paused(record)) - except Exception: - return False - - -def score_active_memory_record( - record: str, - *, - user_input: str, - levels: dict[str, float], - context=None, -) -> float: - visible = _active_memory_visible_text(record) - relevance = _memory_context_relevance( - visible, - user_input=user_input, - context=context, - ) - resonance, _ = _record_channel_resonance(visible, levels, context=context) - score = ( - METABOLISM_ACTIVE_MEMORY_MIN_SALIENCE - + min(0.70, relevance * 0.74) - + resonance * 0.10 - ) - - normalized_user = _normalized_signal_text(user_input) - active_id = _active_memory_record_id(record) - if active_id and active_id.casefold() in normalized_user: - score += 0.30 - - return round(max(0.0, min(1.0, score)), 3) - - -def update_active_memory_salience( - context, - *, - user_input: str = "", -) -> dict[str, float]: - if context is None: - return {} - - levels = ensure_metabolism_state(context) - salience: dict[str, float] = {} - - for record in getattr(context, "active_memory_records", []) or []: - text = str(record or "").strip() - if not text or _active_memory_is_paused(text): - continue - active_id = _active_memory_record_id(text) - if not active_id: - continue - salience[active_id] = score_active_memory_record( - text, - user_input=user_input, - levels=levels, - context=context, - ) - - context.runtime_metabolism_active_memory_salience = salience - return salience - - -def rank_active_memory_records( - records: list[str], - *, - context=None, - user_input: str = "", -) -> list[str]: - if not records or context is None: - return list(records or []) - - if not METABOLISM_MEMORY_BIAS_ENABLED: - update_active_memory_salience( - context, - user_input=user_input, - ) - return list(records) - - levels = ensure_metabolism_state(context) - scored = [] - refreshed_by_id: dict[str, str] = {} - current_time = time.time() - - for index, record in enumerate(records): - visible = _active_memory_visible_text(record) - relevance = _memory_context_relevance( - visible, - user_input=user_input, - context=context, - ) - refreshed_record, significance = _refresh_active_memory_significance( - record, - context=context, - relevance=relevance, - now=current_time, - ) - score = score_active_memory_record( - refreshed_record, - user_input=user_input, - levels=levels, - context=context, - ) - active_id = _active_memory_record_id(refreshed_record) - if active_id: - refreshed_by_id[active_id] = refreshed_record - scored.append((score, significance, index, refreshed_record)) - - if refreshed_by_id: - stored_records = list( - getattr(context, "active_memory_records", []) - or [] - ) - stored_changed = False - for index, stored_record in enumerate(stored_records): - active_id = _active_memory_record_id(stored_record) - replacement = refreshed_by_id.get(active_id) - if replacement and replacement != stored_record: - stored_records[index] = replacement - stored_changed = True - if stored_changed: - context.active_memory_records = stored_records - context.runtime_active_memory_records_dirty = True - - scored.sort( - key=lambda item: ( - 0 - if item[1] >= METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD - else 1, - -item[1] - if item[1] >= METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD - else -item[0], - -item[0] - if item[1] >= METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD - else item[2], - item[2], - ) - ) - update_active_memory_salience( - context, - user_input=user_input, - ) - return [record for _, _, _, record in scored] - - -def score_delayed_memory_report( - report: dict, - *, - report_id: str = "", - user_input: str = "", - context=None, -) -> float: - if not isinstance(report, dict): - return 0.0 - - title = str(report.get("title", "") or "").strip() - summary = str(report.get("summary", "") or "").strip() - tags = report.get("tags", []) - if not isinstance(tags, list): - tags = [tags] - tags_text = " ".join(str(tag or "").strip() for tag in tags if str(tag or "").strip()) - - title_score = _memory_context_relevance( - title, - user_input=user_input, - context=context, - ) if title else 0.0 - summary_score = _memory_context_relevance( - summary, - user_input=user_input, - context=context, - ) if summary else 0.0 - tags_score = _memory_context_relevance( - tags_text, - user_input=user_input, - context=context, - ) if tags_text else 0.0 - - visible = " ".join(part for part in (title, summary, tags_text) if part) - levels = ensure_metabolism_state(context) if context is not None else dict(METABOLISM_DEFAULT_LEVELS) - resonance, _ = _record_channel_resonance(visible, levels, context=context) if visible else (0.0, "") - - score = max( - title_score, - summary_score * 0.88, - tags_score * 0.72, - ) - score = min(1.0, score + resonance * 0.08) - - normalized_user = _normalized_signal_text(user_input) - normalized_id = str(report_id or report.get("id", "") or "").strip().casefold() - if normalized_id and normalized_id in normalized_user: - score = max(score, 0.96) - - return round(max(0.0, min(1.0, score)), 3) - - -def delayed_memory_bubble_tier(score: float) -> int: - try: - value = float(score) - except (TypeError, ValueError): - value = 0.0 - - if value >= METABOLISM_DELAYED_MEMORY_STRONG_BUBBLE_THRESHOLD: - return 2 - if value >= METABOLISM_DELAYED_MEMORY_BUBBLE_THRESHOLD: - return 1 - return 0 - - -def _l4_fact_numeric_sort_key(fact: dict) -> tuple: - """Newest durable facts first; F ids are monotonic creation ids.""" - - fact_id = str((fact or {}).get("id", "") or "").strip() - match = re.fullmatch(r"F([1-9]\d*)", fact_id, flags=re.IGNORECASE) - if match: - return (0, -int(match.group(1)), "") - return (1, 0, fact_id.casefold()) - - -def score_l4_fact_context_focus( - fact: dict, - *, - user_input: str, - levels: dict[str, float], - context=None, -) -> float: - """Return prompt-only L4 salience without mutating storage/UI order.""" - - if not isinstance(fact, dict): - return 0.0 - - fact_text = " ".join( - part - for part in ( - str(fact.get("key", "") or "").strip(), - str(fact.get("value", "") or "").strip(), - ) - if part - ) - if not fact_text: - return 0.0 - - user_tokens = _tokenize_signal_text(user_input) - fact_tokens = _tokenize_signal_text(fact_text) - lexical_overlap = 0.0 - if user_tokens and fact_tokens: - lexical_overlap = len(user_tokens & fact_tokens) / max(1, len(user_tokens)) - - resonance, _channel = _record_channel_resonance( - fact_text, - levels, - context=context, - ) - - learned_cross_resonance = 0.0 - now = time.time() - for item in ensure_metabolism_associations(context): - phrase = str(item.get("phrase", "") or "") - if ( - not phrase - or not _phrase_occurs(phrase, user_input) - or not _phrase_occurs(phrase, fact_text) - ): - continue - learned_cross_resonance = max( - learned_cross_resonance, - float(item.get("weight", 0.0) or 0.0) - * _association_recency_weight(item, now), - ) - - attention_drive = max( - 0.0, - _relative_drive(levels, "norepinephrine"), - _relative_drive(levels, "dopamine"), - _relative_drive(levels, "oxytocin"), - _relative_drive(levels, "cortisol"), - ) - attention_gain = 0.82 + min(0.18, attention_drive * 0.18) - - score = ( - lexical_overlap * 0.62 - + learned_cross_resonance * 0.24 - + resonance * 0.14 - ) * attention_gain - - return round(max(0.0, min(1.0, score)), 4) - - -def _select_l4_metabolic_focus( - scored: list[tuple[float, int, dict]], - *, - levels: dict[str, float], -) -> list[tuple[float, int, dict]]: - """Choose a narrow 1..3 fact attention cone for the prompt only. - - Relevance owns the decision; metabolism only changes how wide attention - may open. Alert/novel/social continuity can admit a second or third nearby - fact, while pressure narrows the cone. Nothing unrelated is promoted just - to fill a quota. - """ - - ranked = sorted(scored, key=lambda item: (-item[0], item[1])) - if not ranked or ranked[0][0] < 0.22: - return [] - - top_score = ranked[0][0] - breadth_drive = ( - max(0.0, _relative_drive(levels, "norepinephrine")) * 0.42 - + max(0.0, _relative_drive(levels, "dopamine")) * 0.30 - + max(0.0, _relative_drive(levels, "oxytocin")) * 0.22 - - max(0.0, _relative_drive(levels, "cortisol")) * 0.36 - ) - breadth_drive = max(-0.35, min(0.65, breadth_drive)) - - # Relative floors keep the focus coherent with the strongest fact. A more - # open metabolic state lowers them slightly; high pressure raises them. - second_ratio = max(0.62, min(0.82, 0.75 - breadth_drive * 0.16)) - third_ratio = max(0.50, min(0.72, 0.61 - breadth_drive * 0.14)) - second_floor = max(0.20, top_score * second_ratio) - third_floor = max(0.18, top_score * third_ratio) - - focused = [ranked[0]] - if len(ranked) > 1 and ranked[1][0] >= second_floor: - focused.append(ranked[1]) - - # A third fact needs either a convincingly strong semantic hit or enough - # attentional breadth. This makes the lane breathe between 1, 2 and 3 - # instead of behaving like a fixed top-k search. - if ( - len(focused) == 2 - and len(ranked) > 2 - and ranked[2][0] >= third_floor - and (top_score >= 0.34 or breadth_drive >= 0.08) - ): - focused.append(ranked[2]) - - return focused[:3] - - -def _refresh_l4_fact_significance( - fact: dict, - *, - context=None, - relevance: float = 0.0, - now: float | None = None, -) -> float: - if not isinstance(fact, dict): - return 0.0 - - current_time = float(now if now is not None else time.time()) - current = _normalize_memory_significance( - fact.get("significance", 0.0) - ) - fact_id = str(fact.get("id", "") or "").strip().upper() - memory_key = f"l4:{fact_id or hash(str(fact))}" - seen_at = getattr( - context, - "runtime_metabolism_l4_significance_seen_at", - None, - ) - if not isinstance(seen_at, dict): - seen_at = {} - if context is not None: - context.runtime_metabolism_l4_significance_seen_at = seen_at - previous_seen_at = float(seen_at.get(memory_key, current_time) or current_time) - seen_at[memory_key] = current_time - decayed = _decay_memory_significance_by_elapsed( - current, - elapsed=max(0.0, current_time - previous_seen_at), - ) - - event_significance = _consume_metabolic_event_significance( - context, - memory_key=memory_key, - ) - reinforced = _reinforce_memory_significance( - decayed, - event_significance=event_significance, - relevance=relevance, - ) - - has_field = "significance" in fact - if has_field and abs(reinforced - current) < 0.001: - return current - - fact["significance"] = reinforced - fact["significance_updated_at"] = datetime.fromtimestamp( - current_time, - tz=timezone.utc, - ).isoformat().replace("+00:00", "Z") - if context is not None: - context.runtime_metabolism_l4_significance_dirty = True - return reinforced - - -def rank_l4_facts_for_context( - facts: list[dict], - *, - context=None, - user_input: str = "", -) -> list[dict]: - """L4 prompt order with metabolic focus plus persistent event significance. - - The ordinary lane stays exactly as before: newest facts first, with the - small query-sensitive metabolic focus cone allowed to bubble 1..3 matches. - Facts whose persistent significance is >= 0.5 form a stronger lane above - it. Significance itself decays only while the fact keeps being encountered - in context, and is reinforced only when a new metabolic event both moved - JIN's state and is relevant to that fact. - """ - - canonical = sorted( - [fact for fact in (facts or []) if isinstance(fact, dict)], - key=_l4_fact_numeric_sort_key, - ) - if not canonical: - if context is not None: - context.runtime_metabolism_l4_focus_ids = [] - return [] - - levels = ( - advance_metabolism_clock(context) - if context is not None - else dict(METABOLISM_DEFAULT_LEVELS) - ) - current_time = time.time() - - scored = [] - for index, fact in enumerate(canonical): - focus_score = ( - score_l4_fact_context_focus( - fact, - user_input=user_input, - levels=levels, - context=context, - ) - if context is not None and str(user_input or "").strip() - else 0.0 - ) - significance = _refresh_l4_fact_significance( - fact, - context=context, - relevance=focus_score, - now=current_time, - ) - scored.append((focus_score, significance, index, fact)) - - ordinary = list(canonical) - focused_facts: list[dict] = [] - if ( - context is not None - and METABOLISM_MEMORY_BIAS_ENABLED - and str(user_input or "").strip() - ): - focused = _select_l4_metabolic_focus( - [(score, index, fact) for score, _sig, index, fact in scored], - levels=levels, - ) - if focused: - focused_facts = [item[2] for item in focused] - focused_object_ids = {id(fact) for fact in focused_facts} - ordinary = [*focused_facts] - ordinary.extend( - fact - for fact in canonical - if id(fact) not in focused_object_ids - ) - - if context is not None: - context.runtime_metabolism_l4_focus_ids = [ - str(fact.get("id", "") or "").strip() - for fact in focused_facts - if str(fact.get("id", "") or "").strip() - ] - - significance_by_object = { - id(fact): significance - for _score, significance, _index, fact in scored - } - significant = [ - fact - for fact in ordinary - if significance_by_object.get(id(fact), 0.0) - >= METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD - ] - ordinary_rank = { - id(fact): index - for index, fact in enumerate(ordinary) - } - significant.sort( - key=lambda fact: ( - -significance_by_object.get(id(fact), 0.0), - ordinary_rank.get(id(fact), len(ordinary)), - ) - ) - significant_ids = {id(fact) for fact in significant} - return [ - *significant, - *(fact for fact in ordinary if id(fact) not in significant_ids), - ] - -def _metabolism_temperature_delta(levels: dict[str, float]) -> float: - normalized = normalize_metabolism_levels(levels) - dopamine = normalized["dopamine"] - METABOLISM_DEFAULT_LEVELS["dopamine"] - serotonin = normalized["serotonin"] - METABOLISM_DEFAULT_LEVELS["serotonin"] - norepinephrine = normalized["norepinephrine"] - METABOLISM_DEFAULT_LEVELS["norepinephrine"] - cortisol = normalized["cortisol"] - METABOLISM_DEFAULT_LEVELS["cortisol"] - - # Oxytocin deliberately does not alter sampling randomness: social - # continuity belongs in prompt/memory salience, never in truth likelihood. - delta = ( - dopamine * 0.20 - + norepinephrine * 0.055 - - serotonin * 0.10 - - cortisol * 0.19 - ) - return round( - max( - -METABOLISM_TEMPERATURE_MAX_DELTA, - min(METABOLISM_TEMPERATURE_MAX_DELTA, delta), - ), - 4, - ) - - -def resolve_metabolism_temperature( - base_temperature: Any, - context=None, -) -> float: - try: - base = float(base_temperature) - except (TypeError, ValueError): - base = 0.3 - - levels = ( - advance_metabolism_clock(context) - if context is not None - else dict(METABOLISM_DEFAULT_LEVELS) - ) - delta = ( - _metabolism_temperature_delta(levels) - if METABOLISM_CAUSAL_POLICY_ENABLED - else 0.0 - ) - resolved = max(0.02, min(1.5, base + delta)) - - if context is not None: - context.runtime_metabolism_policy = { - "base_temperature": round(base, 4), - "temperature": round(resolved, 4), - "temperature_delta": delta, - } - - return round(resolved, 4) - - -def build_metabolism_brain_context( - context, - *, - user_input: str = "", -) -> str: - if context is None or not METABOLISM_CAUSAL_POLICY_ENABLED: - return "" - - levels = advance_metabolism_clock(context) - policy = dict(getattr(context, "runtime_metabolism_policy", {}) or {}) - if "temperature_delta" not in policy: - policy["temperature_delta"] = _metabolism_temperature_delta(levels) - - state_line = " ".join( - f"{channel}={levels[channel]:.3f}" - for channel in METABOLISM_CHANNELS - ) - salience = update_active_memory_salience( - context, - user_input=user_input, - ) - top_active = [ - item - for item in sorted( - salience.items(), - key=lambda item: (-item[1], item[0]), - ) - if item[1] > METABOLISM_ACTIVE_MEMORY_MIN_SALIENCE + 0.015 - ][:3] - top_active_line = ( - ", ".join(f"{active_id}:{score:.2f}" for active_id, score in top_active) - if top_active - else "none" - ) - instruction = normalize_metabolism_instruction( - getattr(context, "runtime_metabolism_instruction", "") - ) - - parts = [ - '', - f"state: {state_line}", - f"temperature_delta: {float(policy.get('temperature_delta', 0.0)):+.4f}", - f"active_memory_salience_top: {top_active_line}", - ] - if instruction: - parts.append(f"Possible reaction: {instruction}") - parts.append("") - return "\n".join(parts) - - -def metabolic_memory_strength_boost( - context, - *, - key: str, - value: str, - status: str = "same", -) -> tuple[float, float, str]: - """Return a small encoding-strength bonus for the current L1 line.""" - - if context is None or not METABOLISM_MEMORY_BIAS_ENABLED: - return 0.0, 0.0, "" - - levels = ensure_metabolism_state(context) - text = f"{key}: {value}" - resonance, resonance_channel = _record_channel_resonance(text, levels, context=context) - last_delta = getattr(context, "runtime_metabolism_last_delta", {}) or {} - shift = sum( - abs(float(last_delta.get(channel, 0.0) or 0.0)) - for channel in METABOLISM_CHANNELS - ) / len(METABOLISM_CHANNELS) - shift_energy = max(0.0, min(1.0, shift / 0.06)) - - state_energy = sum( - abs(levels[channel] - METABOLISM_DEFAULT_LEVELS[channel]) - for channel in METABOLISM_CHANNELS - ) / len(METABOLISM_CHANNELS) - state_energy = max(0.0, min(1.0, state_energy / 0.28)) - - change_gain = 1.0 if status in {"new", "changed"} else 0.42 - salience = max( - 0.0, - min( - 1.0, - 0.22 * state_energy - + 0.48 * shift_energy - + 0.30 * resonance, - ), - ) - boost = min( - METABOLISM_MEMORY_STRENGTH_MAX_BOOST, - salience * 0.12 * change_gain, - ) - return round(boost, 4), round(salience, 4), resonance_channel - - -async def prepare_metabolism_for_turn( - context, - user_message: str, -) -> dict[str, float]: - """Pre-generation homeostat step: decay, cheap reflex, salience, UI.""" - - if context is None: - return dict(METABOLISM_DEFAULT_LEVELS) - - impulse, triggers = build_metabolism_reflex( - user_message, - context=context, - ) - levels = apply_metabolism_impulse( - context, - impulse, - source=("reflex:" + ",".join(triggers)) if triggers else "reflex:neutral", - ) - update_active_memory_salience( - context, - user_input=user_message, - ) - await emit_metabolism_state(context) - return levels - - -def build_runtime_outcome_reflex( - context, -) -> tuple[dict[str, float], tuple[str, ...]]: - """Fast post-turn reflex from JIN's own runtime outcomes. - - This closes a gap that a background SERVICE estimate cannot guarantee: if - the user immediately sends the next message, a failed action/validator loop - must already have left a small trace in the state. The semantic estimator - can still correct or deepen it a moment later. - """ - - impulse = {channel: 0.0 for channel in METABOLISM_CHANNELS} - triggers: list[str] = [] - - def add(label: str, values: dict[str, float]) -> None: - if label not in triggers: - triggers.append(label) - for channel, delta in values.items(): - impulse[channel] += float(delta) - - if bool(getattr(context, "runtime_turn_interrupted", False)): - add( - "turn_interrupted", - { - "serotonin": -0.010, - "norepinephrine": 0.018, - "cortisol": 0.025, - }, - ) - - if getattr(context, "runtime_turn_aborted_actions", None): - add( - "action_aborted", - { - "dopamine": -0.006, - "norepinephrine": 0.014, - "cortisol": 0.020, - }, - ) - - current_turn_id = str( - getattr(context, "runtime_current_sequence_turn_id", "") - or getattr(context, "runtime_current_turn_id", "") - or "" - ).strip() - current_action_text: list[str] = [] - - for item in getattr(context, "runtime_session_action_history", []) or []: - if not isinstance(item, dict): - continue - item_turn_id = str(item.get("runtime_turn_id", "") or "").strip() - if current_turn_id and item_turn_id and item_turn_id != current_turn_id: - continue - current_action_text.append(str(item.get("text", "") or "")) - for part in item.get("parts", []) or []: - if not isinstance(part, dict): - continue - current_action_text.extend(( - str(part.get("text", "") or ""), - str(part.get("detail", "") or ""), - str(part.get("message", "") or ""), - )) - - action_signal = _normalized_signal_text(" ".join(current_action_text)) - action_failed = bool( - action_signal - and re.search( - r"(?:\bfailed\b|\bfailure\b|\berror\b|incorrect|invalid|timeout|\bbroken\b|неправил|ошиб|сбой)", - action_signal, - flags=re.IGNORECASE | re.UNICODE, - ) - ) - - if action_failed: - add( - "runtime_failure", - { - "dopamine": -0.010, - "serotonin": -0.012, - "norepinephrine": 0.026, - "cortisol": 0.034, - }, - ) - elif action_signal and re.search( - r"(?:\bsuccess\b|\bsucceeded\b|\bcompleted\b|\bapplied\b|\bsaved\b|\bloaded\b|\bok\b|успеш|сохран|загруж|примен)", - action_signal, - flags=re.IGNORECASE | re.UNICODE, - ): - add( - "runtime_success", - { - "dopamine": 0.012, - "serotonin": 0.007, - "cortisol": -0.006, - }, - ) - - try: - repeated = int(getattr(context, "runtime_pattern_counter", 0) or 0) - except (TypeError, ValueError): - repeated = 0 - if repeated > 1: - add( - "repetition_pressure", - { - "serotonin": -0.010, - "norepinephrine": 0.014, - "cortisol": 0.020, - }, - ) - - reasoning_length = len(str( - getattr(context, "runtime_turn_reasoning_content", "") - or "" - )) - if reasoning_length >= 12000: - # Long reasoning is cognitive load, not automatically a failure. - add( - "deep_reasoning_load", - { - "norepinephrine": 0.010, - }, - ) - - for channel in METABOLISM_CHANNELS: - max_step = METABOLISM_REFLEX_MAX_STEP[channel] - impulse[channel] = round( - max(-max_step, min(max_step, impulse[channel])), - 4, - ) - - return impulse, tuple(triggers) - - -async def settle_metabolism_after_turn( - context, -) -> dict[str, float]: - """Commit immediate runtime consequences before background integration.""" - - if context is None: - return dict(METABOLISM_DEFAULT_LEVELS) - - impulse, triggers = build_runtime_outcome_reflex(context) - levels = apply_metabolism_impulse( - context, - impulse, - source=( - "runtime_outcome:" + ",".join(triggers) - if triggers - else "runtime_outcome:neutral" - ), - ) - update_active_memory_salience( - context, - user_input=getattr(context, "runtime_turn_user_message", ""), - ) - await emit_metabolism_state(context) - return levels - - -def _clip(value: Any, limit: int) -> str: - text = str(value or "").strip() - if len(text) <= limit: - return text - return text[:limit].rstrip() + "\n[cut]" - - -def append_metabolism_turn( - context, - *, - user_message: str, - assistant_message: str, - reasoning: str = "", - feedback: dict | None = None, -) -> None: - if context is None: - return - - turns = getattr(context, "runtime_metabolism_recent_turns", None) - if not isinstance(turns, list): - turns = [] - - user_text = _clip(user_message, METABOLISM_MESSAGE_MAX_CHARS) - assistant_text = _clip(assistant_message, METABOLISM_MESSAGE_MAX_CHARS) - reasoning_text = _clip(reasoning, METABOLISM_REASONING_MAX_CHARS) - - if not user_text and not assistant_text and not reasoning_text: - return - - turn = { - "user": user_text, - "jin": assistant_text, - "reasoning": reasoning_text, - } - if isinstance(feedback, dict): - turn["feedback"] = deepcopy(feedback) - - turns.append(turn) - context.runtime_metabolism_recent_turns = turns[-METABOLISM_RECENT_TURNS:] - - -def _compact_l4_index(context) -> list[dict]: - """Expose the shape of durable memory without rereading its full payload.""" - - store = getattr(context, "runtime_long_term_memory_store", {}) - if not isinstance(store, dict): - return [] - - result = [] - for fact in store.get("facts", []) or []: - if not isinstance(fact, dict): - continue - - item = { - "id": str(fact.get("id", "") or "").strip(), - "key": _clip(fact.get("key", ""), 240), - "category": _clip(fact.get("category", ""), 80), - } - - if item["id"] or item["key"]: - result.append(item) - - return result - - -def _compact_runtime_actions(context) -> list[dict]: - history = getattr(context, "runtime_session_action_history", []) - if not isinstance(history, list): - return [] - - current_turn_id = str( - getattr(context, "runtime_current_sequence_turn_id", "") - or getattr(context, "runtime_current_turn_id", "") - or "" - ).strip() - result = [] - - for item in history[-METABOLISM_RECENT_ACTIONS:]: - if not isinstance(item, dict): - continue - - text = _clip(item.get("text", ""), 700) - parts = [] - for part in item.get("parts", []) or []: - if not isinstance(part, dict): - continue - part_text = _clip(part.get("text", ""), 260) - detail = _clip(part.get("detail", ""), 420) - message = _clip(part.get("message", ""), 520) - if not (part_text or detail or message): - continue - compact_part = {"text": part_text} - if detail: - compact_part["detail"] = detail - if message: - compact_part["message"] = message - parts.append(compact_part) - - turn_id = str(item.get("runtime_turn_id", "") or "").strip() - if not text and not parts: - continue - - compact_item = { - "text": text, - "current_turn": bool(current_turn_id and turn_id == current_turn_id), - } - if parts: - compact_item["parts"] = parts - result.append(compact_item) - - return result - - -def _compact_active_memory(context) -> list[dict]: - result = [] - for record in getattr(context, "active_memory_records", []) or []: - text = str(record or "").strip() - if not text or _active_memory_is_paused(text): - continue - result.append({ - "id": _active_memory_record_id(text), - "text": _clip(_active_memory_visible_text(text), 520), - }) - return result[:24] - - -def _compact_delayed_memory(context) -> list[dict]: - reports = getattr(context, "delayed_memory_reports", {}) - if not isinstance(reports, dict): - return [] - - loaded = { - str(item or "").strip().casefold() - for item in getattr(context, "runtime_loaded_delayed_memory_ids", []) or [] - if str(item or "").strip() - } - result = [] - for report_id, report in reports.items(): - if not isinstance(report, dict): - continue - normalized_id = str(report_id or report.get("id", "") or "").strip().casefold() - tags = report.get("tags", []) - if not isinstance(tags, list): - tags = [tags] - result.append({ - "id": normalized_id, - "title": _clip(report.get("title", ""), 220), - "summary": _clip(report.get("summary", ""), 260), - "tags": [_clip(tag, 80) for tag in tags[:8] if str(tag or "").strip()], - "loaded": normalized_id in loaded, - "pinned": bool(report.get("pinned")), - "last_loaded_date": _clip(report.get("last_loaded_date", ""), 80), - }) - return result[:24] - - -def build_metabolism_snapshot( - context, - *, - committed_snapshot: dict | None = None, - committed_turns: list[dict] | None = None, -) -> dict: - previous_levels = ensure_metabolism_state(context) - - # runtime_recent_turns carries the restored previous-session tail; the - # metabolism-local list carries richer reasoning for current turns. - merged_turns: list[dict] = [] - for turn in getattr(context, "runtime_recent_turns", []) or []: - if not isinstance(turn, dict): - continue - merged_turns.append({ - "user": _clip(turn.get("user", turn.get("user_message", "")), METABOLISM_MESSAGE_MAX_CHARS), - "jin": _clip(turn.get("jin", turn.get("assistant_message", "")), METABOLISM_MESSAGE_MAX_CHARS), - "reasoning": _clip(turn.get("reasoning", ""), METABOLISM_REASONING_MAX_CHARS), - }) - for turn in getattr(context, "runtime_metabolism_recent_turns", []) or []: - if isinstance(turn, dict): - merged_turns.append(deepcopy(turn)) - recent_turns = merged_turns[-METABOLISM_RECENT_TURNS:] - - l4_index = _compact_l4_index(context) - runtime_actions = _compact_runtime_actions(context) - active_memory = _compact_active_memory(context) - delayed_memory = _compact_delayed_memory(context) - committed_users = _extract_committed_user_messages(committed_turns) - committed_l1 = { - "batch_id": _committed_l1_batch_id(context, committed_snapshot), - "snapshot_index": (committed_snapshot or {}).get("index", 0), - "total_diff": (committed_snapshot or {}).get("total_diff", 0), - "user_inputs": committed_users, - "last_user_input": committed_users[-1] if committed_users else "", - "changes": _compact_l1_patch_text(committed_snapshot), - } - - return { - "baseline": dict(METABOLISM_DEFAULT_LEVELS), - "previous_levels": dict(previous_levels), - "committed_l1": committed_l1, - "runtime_state": { - "runtime_memory": _clip( - getattr(context, "runtime_memory", ""), - METABOLISM_RUNTIME_MEMORY_MAX_CHARS, - ), - "turn_number": int(getattr(context, "turn_number", 0) or 0), - "user_messages": int(getattr(context, "user_message_count", 0) or 0), - "jin_messages": int(getattr(context, "assistant_message_count", 0) or 0), - "turn_interrupted": bool(getattr(context, "runtime_turn_interrupted", False)), - "interruption_reason": _clip(getattr(context, "runtime_turn_interruption_reason", ""), 800), - "latest_user_feedback": deepcopy(getattr(context, "runtime_last_response_feedback", None)), - }, - "recent_runtime_actions": runtime_actions, - "active_memory": active_memory, - "delayed_memory": delayed_memory, - "l4_index": l4_index, - "l4_facts_total": len(l4_index), - "recent_interactions": recent_turns, - } - - -def build_metabolism_user_prompt(snapshot: dict) -> str: - return ( - "Estimate JIN's five metabolism target levels and one 1-3 sentence BRAIN instruction from this snapshot.\n" - "Return one strict JSON object only.\n\n" - + json.dumps(snapshot, ensure_ascii=False, indent=2, sort_keys=False) - ) - - -def _metabolism_prompt_tokens(snapshot: dict) -> int: - return estimate_runtime_tokens( - system_prompt=METABOLISM_SYSTEM_PROMPT, - user_input=build_metabolism_user_prompt(snapshot), - ) - - -def compact_metabolism_snapshot_for_context( - snapshot: dict, - context_window: Any, -) -> dict: - """Trim only observational detail when the service context is too small.""" - - try: - window = int(context_window or 0) - except (TypeError, ValueError): - window = 0 - - if window <= 0: - return snapshot - - input_budget = max( - 256, - window - - METABOLISM_MAX_OUTPUT_TOKENS - - METABOLISM_CONTEXT_RESERVE_TOKENS, - ) - if _metabolism_prompt_tokens(snapshot) <= input_budget: - return snapshot - - compacted = deepcopy(snapshot) - compacted["snapshot_compaction"] = { - "applied": True, - "input_budget_tokens": input_budget, - "l4_index_omitted": 0, - "runtime_actions_omitted": 0, - } - - runtime_state = compacted.get("runtime_state") - if isinstance(runtime_state, dict): - runtime_state["runtime_memory"] = _clip( - runtime_state.get("runtime_memory", ""), - 4000, - ) - runtime_state["interruption_reason"] = _clip( - runtime_state.get("interruption_reason", ""), - 400, - ) - - committed_l1 = compacted.get("committed_l1") - if isinstance(committed_l1, dict): - committed_l1["changes"] = _clip(committed_l1.get("changes", ""), 2600) - committed_l1["last_user_input"] = _clip(committed_l1.get("last_user_input", ""), 900) - committed_l1["user_inputs"] = [ - _clip(item, 700) - for item in (committed_l1.get("user_inputs", []) or [])[-3:] - ] - - turns = compacted.get("recent_interactions") - if isinstance(turns, list): - for turn in turns: - if not isinstance(turn, dict): - continue - turn["user"] = _clip(turn.get("user", ""), 1400) - turn["jin"] = _clip(turn.get("jin", ""), 1400) - turn["reasoning"] = _clip(turn.get("reasoning", ""), 1400) - - actions = compacted.get("recent_runtime_actions") - if isinstance(actions, list): - for action in actions: - if not isinstance(action, dict): - continue - action["text"] = _clip(action.get("text", ""), 420) - for part in action.get("parts", []) or []: - if not isinstance(part, dict): - continue - part["text"] = _clip(part.get("text", ""), 180) - part["detail"] = _clip(part.get("detail", ""), 260) - part["message"] = _clip(part.get("message", ""), 320) - - if _metabolism_prompt_tokens(compacted) > input_budget: - if isinstance(runtime_state, dict): - runtime_state["runtime_memory"] = _clip( - runtime_state.get("runtime_memory", ""), - 2200, - ) - if isinstance(turns, list): - for turn in turns: - if not isinstance(turn, dict): - continue - turn["user"] = _clip(turn.get("user", ""), 800) - turn["jin"] = _clip(turn.get("jin", ""), 800) - turn["reasoning"] = _clip(turn.get("reasoning", ""), 650) - - active_memory = compacted.get("active_memory") - if ( - _metabolism_prompt_tokens(compacted) > input_budget - and isinstance(active_memory, list) - ): - while len(active_memory) > 8 and _metabolism_prompt_tokens(compacted) > input_budget: - active_memory.pop() - - delayed_memory = compacted.get("delayed_memory") - if ( - _metabolism_prompt_tokens(compacted) > input_budget - and isinstance(delayed_memory, list) - ): - while len(delayed_memory) > 8 and _metabolism_prompt_tokens(compacted) > input_budget: - delayed_memory.pop() - - l4_index = compacted.get("l4_index") - if ( - _metabolism_prompt_tokens(compacted) > input_budget - and isinstance(l4_index, list) - ): - omitted = 0 - while l4_index and _metabolism_prompt_tokens(compacted) > input_budget: - l4_index.pop() - omitted += 1 - compacted["snapshot_compaction"]["l4_index_omitted"] = omitted - - if ( - _metabolism_prompt_tokens(compacted) > input_budget - and isinstance(actions, list) - ): - omitted = 0 - while len(actions) > 4 and _metabolism_prompt_tokens(compacted) > input_budget: - actions.pop(0) - omitted += 1 - compacted["snapshot_compaction"]["runtime_actions_omitted"] = omitted - - if _metabolism_prompt_tokens(compacted) > input_budget: - if isinstance(runtime_state, dict): - runtime_state["runtime_memory"] = _clip( - runtime_state.get("runtime_memory", ""), - 900, - ) - if isinstance(turns, list): - for turn in turns: - if not isinstance(turn, dict): - continue - turn["user"] = _clip(turn.get("user", ""), 420) - turn["jin"] = _clip(turn.get("jin", ""), 420) - turn["reasoning"] = _clip(turn.get("reasoning", ""), 360) - - # Last-resort trim keeps the estimator inside tiny SERVICE contexts. - # Preserve the committed L1 cause anchor; discard older observational bulk. - if _metabolism_prompt_tokens(compacted) > input_budget: - if isinstance(actions, list): - while actions and _metabolism_prompt_tokens(compacted) > input_budget: - actions.pop(0) - if isinstance(turns, list): - while len(turns) > 2 and _metabolism_prompt_tokens(compacted) > input_budget: - turns.pop(0) - if isinstance(runtime_state, dict) and _metabolism_prompt_tokens(compacted) > input_budget: - runtime_state["runtime_memory"] = _clip( - runtime_state.get("runtime_memory", ""), - 360, - ) - if isinstance(turns, list) and _metabolism_prompt_tokens(compacted) > input_budget: - for turn in turns: - if not isinstance(turn, dict): - continue - turn["user"] = _clip(turn.get("user", ""), 220) - turn["jin"] = _clip(turn.get("jin", ""), 220) - turn["reasoning"] = _clip(turn.get("reasoning", ""), 180) - - compacted["snapshot_compaction"]["estimated_input_tokens"] = ( - _metabolism_prompt_tokens(compacted) - ) - return compacted - - -def _extract_json_object(text: str) -> dict | None: - source = str(text or "").strip() - if not source: - return None - - candidates = [source] - fenced = re.search(r"```(?:json)?\s*([\s\S]*?)```", source, re.IGNORECASE) - if fenced: - candidates.append(fenced.group(1).strip()) - - first = source.find("{") - last = source.rfind("}") - if first >= 0 and last > first: - candidates.append(source[first:last + 1]) - - for candidate in candidates: - try: - parsed = json.loads(candidate) - except (TypeError, ValueError, json.JSONDecodeError): - continue - if isinstance(parsed, dict): - return parsed - - return None - - -def _parse_metabolism_payload_text( - raw_text: str, -) -> tuple[dict[str, float] | None, str]: - parsed = _extract_json_object(raw_text) - - if not isinstance(parsed, dict): - return None, "" - - channel_keys = set(METABOLISM_CHANNELS) - payload_keys = set(parsed) - if payload_keys not in (channel_keys, channel_keys | {"instruction"}): - return None, "" - - instruction = normalize_metabolism_instruction(parsed.get("instruction", "")) - if "instruction" in parsed and not instruction: - return None, "" - - return normalize_metabolism_levels(parsed), instruction - - -def _parse_metabolism_json_text(raw_text: str) -> dict[str, float] | None: - levels, _instruction = _parse_metabolism_payload_text(raw_text) - return levels - - -def extract_metabolism_instruction(raw_text: str) -> str: - parsed = _extract_json_object(raw_text) - if not isinstance(parsed, dict): - return "" - return normalize_metabolism_instruction(parsed.get("instruction", "")) - - -def parse_metabolism_response(response: Any) -> tuple[dict[str, float] | None, str]: - content = ResponseExtractor.extract_content_text(response).strip() - reasoning = ResponseExtractor.extract_reasoning_text(response).strip() - raw_text = content or reasoning - return _parse_metabolism_json_text(raw_text), raw_text - - -def recover_metabolism_levels_locally(raw_text: str) -> dict[str, float] | None: - """Recover five numeric channels locally; never spend a second inference.""" - - parsed = _extract_json_object(raw_text) - if isinstance(parsed, dict): - recovered: dict[str, float] = {} - for channel in METABOLISM_CHANNELS: - if channel not in parsed: - break - value = parsed.get(channel) - if isinstance(value, str): - value = value.strip().rstrip("%") - try: - number = float(value) - except (TypeError, ValueError): - break - if isinstance(parsed.get(channel), str) and "%" in str(parsed.get(channel)): - number /= 100.0 - elif number > 1.0 and number <= 100.0: - number /= 100.0 - recovered[channel] = _clamp_level(number, METABOLISM_DEFAULT_LEVELS[channel]) - if len(recovered) == len(METABOLISM_CHANNELS): - return recovered - - source = _normalized_signal_text(raw_text) - recovered = {} - for channel in METABOLISM_CHANNELS: - match = re.search( - rf"(? 1.0 and number <= 100.0): - number /= 100.0 - recovered[channel] = _clamp_level(number, METABOLISM_DEFAULT_LEVELS[channel]) - - return recovered if len(recovered) == len(METABOLISM_CHANNELS) else None - - -async def emit_metabolism_state(context) -> None: - emitter = getattr(context, "emitter", None) - emit = getattr(emitter, "emit", None) - if emit is None: - return - - try: - await emit({ - "type": "metabolism_update", - "levels": ensure_metabolism_state(context), - "active_memory_salience": dict( - getattr( - context, - "runtime_metabolism_active_memory_salience", - {}, - ) - or {} - ), - "half_lives_seconds": dict(METABOLISM_HALF_LIFE_SECONDS), - "updated_at": float( - getattr(context, "runtime_metabolism_last_tick_at", 0.0) - or time.time() - ), - }) - except Exception: - # Ambient state must never become a foreground chat blocker. - return - - -async def update_metabolism_state( - context, - *, - committed_snapshot: dict | None = None, - committed_turns: list[dict] | None = None, -) -> dict[str, float] | None: - service_client = (getattr(context, "clients", {}) or {}).get("service") - if service_client is None: - return None - - batch_id = _committed_l1_batch_id(context, committed_snapshot) - if batch_id and batch_id == str( - getattr(context, "runtime_metabolism_last_committed_l1_id", "") or "" - ): - return ensure_metabolism_state(context) - - previous = advance_metabolism_clock(context) - snapshot = build_metabolism_snapshot( - context, - committed_snapshot=committed_snapshot, - committed_turns=committed_turns, - ) - snapshot = compact_metabolism_snapshot_for_context( - snapshot, - getattr(service_client, "configured_context_window", None), - ) - user_prompt = build_metabolism_user_prompt(snapshot) - request_id = secrets.token_hex(4) - - request_details = json.dumps({ - "kind": "metabolism_request", - "request_id": request_id, - "batch_id": batch_id, - "model": getattr(service_client, "model_uid", ""), - "system_prompt": METABOLISM_SYSTEM_PROMPT, - "snapshot": snapshot, - }, ensure_ascii=False) - await context.logger.log_metabolism( - "state request", - details=request_details, - event="request", - request_id=request_id, - metabolism_levels=previous, - ) - - try: - response = await ask_service_model( - client=service_client, - system_prompt=METABOLISM_SYSTEM_PROMPT, - user_prompt=user_prompt, - temperature=METABOLISM_TEMPERATURE, - max_tokens=METABOLISM_MAX_OUTPUT_TOKENS, - timeout=getattr(config, "SERVICE_REQUEST_TIMEOUT", None), - ) - # Keep parser compatibility with reasoning-capable providers, while - # the logger/UI shows only the model's visible final answer. - answer_text = ResponseExtractor.extract_content_text(response).strip() - target_levels, parse_text = parse_metabolism_response(response) - generated_instruction = extract_metabolism_instruction(parse_text) - finish_reason = ResponseExtractor.extract_finish_reason(response) - usage = ResponseExtractor.extract_usage(response) - status = "applied" - if target_levels is None: - target_levels = recover_metabolism_levels_locally(parse_text) - status = "local_recovered" if target_levels is not None else "invalid" - - if target_levels is None: - output_limited = finish_reason.casefold() == "length" - status = "truncated" if output_limited else "invalid" - error_message = ( - "metabolism response hit output token limit before final JSON; " - "previous state retained" - if output_limited - else "invalid metabolism payload; previous state retained" - ) - context.runtime_metabolism_last_event = ( - "semantic_truncated" if output_limited else "semantic_invalid" - ) - if batch_id: - context.runtime_metabolism_last_committed_l1_id = batch_id - await context.logger.log_metabolism( - f"state response {status}", - details=json.dumps({ - "kind": "metabolism_response", - "request_id": request_id, - "batch_id": batch_id, - "ok": False, - "status": status, - "finish_reason": finish_reason, - "usage": usage, - "levels": previous, - "raw": answer_text, - "error": error_message, - }, ensure_ascii=False), - event="result", - request_id=request_id, - metabolism_levels=previous, - ) - return previous - - levels = apply_metabolism_target(previous, target_levels) - context.runtime_metabolism_levels = levels - context.runtime_metabolism_last_tick_at = time.time() - context.runtime_metabolism_last_delta = _metabolism_delta(previous, levels) - context.runtime_metabolism_last_event = "semantic_integration" - context.runtime_metabolism_last_signal_delta = dict( - context.runtime_metabolism_last_delta - ) - context.runtime_metabolism_last_signal_event = "semantic_integration" - context.runtime_metabolism_last_signal_at = context.runtime_metabolism_last_tick_at - if generated_instruction: - context.runtime_metabolism_instruction = generated_instruction - if batch_id: - context.runtime_metabolism_last_committed_l1_id = batch_id - - associations = learn_metabolism_associations( - context, - committed_snapshot=committed_snapshot, - committed_turns=committed_turns, - previous_levels=previous, - current_levels=levels, - ) - user_inputs = _extract_committed_user_messages(committed_turns) - update_active_memory_salience( - context, - user_input=user_inputs[-1] if user_inputs else "", - ) - - await context.logger.log_metabolism( - "state response", - details=json.dumps({ - "kind": "metabolism_response", - "request_id": request_id, - "batch_id": batch_id, - "ok": True, - "status": status, - "target_levels": target_levels, - "instruction": generated_instruction, - "finish_reason": finish_reason, - "usage": usage, - "levels": levels, - "delta": _metabolism_delta(previous, levels), - "temperature_delta": _metabolism_temperature_delta(levels), - "learned_associations": len(associations), - "raw": answer_text, - }, ensure_ascii=False), - event="result", - request_id=request_id, - metabolism_levels=levels, - ) - await emit_metabolism_state(context) - - # Re-render the existing snapshot so learned resonance/state persists - # through the ordinary session checkpoint. No new memory record/UI. - try: - from runtime.L1_memory_utils import ( - emit_runtime_memory_snapshot_refresh, - rebuild_latest_runtime_memory_snapshot, - ) - refreshed_snapshot = rebuild_latest_runtime_memory_snapshot(context) - await emit_runtime_memory_snapshot_refresh(context, refreshed_snapshot) - except Exception: - pass - return levels - - except asyncio.CancelledError: - try: - await context.logger.log_metabolism( - "state response cancelled", - details=json.dumps({ - "kind": "metabolism_response", - "request_id": request_id, - "batch_id": batch_id, - "ok": False, - "status": "cancelled", - "levels": previous, - "error": "superseded by newer foreground turn or committed L1 batch", - }, ensure_ascii=False), - event="result", - request_id=request_id, - metabolism_levels=previous, - ) - except Exception: - pass - raise - except Exception as error: - if batch_id: - context.runtime_metabolism_last_committed_l1_id = batch_id - await context.logger.log_metabolism( - "state response failed", - details=json.dumps({ - "kind": "metabolism_response", - "request_id": request_id, - "batch_id": batch_id, - "ok": False, - "status": "failed", - "levels": previous, - "error": str(error), - }, ensure_ascii=False), - event="result", - request_id=request_id, - metabolism_levels=previous, - ) - return previous - - -def cancel_metabolism_update(context) -> bool: - task = getattr(context, "runtime_metabolism_task", None) - if task is None or task.done(): - return False - task.cancel() - return True - - -async def _run_debounced_metabolism_update( - context, - *, - committed_snapshot: dict | None, - committed_turns: list[dict] | None, - debounce_seconds: float, -) -> dict[str, float] | None: - if debounce_seconds > 0: - await asyncio.sleep(debounce_seconds) - return await update_metabolism_state( - context, - committed_snapshot=committed_snapshot, - committed_turns=committed_turns, - ) - - -def schedule_metabolism_update( - context, - *, - committed_snapshot: dict | None = None, - committed_turns: list[dict] | None = None, - debounce_seconds: float = METABOLISM_SEMANTIC_IDLE_DEBOUNCE_SECONDS, -) -> asyncio.Task | None: - if context is None: - return None - - batch_id = _committed_l1_batch_id(context, committed_snapshot) - if batch_id and batch_id == str( - getattr(context, "runtime_metabolism_last_committed_l1_id", "") or "" - ): - return None - - cancel_metabolism_update(context) - task = asyncio.create_task(_run_debounced_metabolism_update( - context, - committed_snapshot=deepcopy(committed_snapshot) if isinstance(committed_snapshot, dict) else None, - committed_turns=deepcopy(committed_turns) if isinstance(committed_turns, list) else None, - debounce_seconds=max(0.0, float(debounce_seconds or 0.0)), - )) - context.runtime_metabolism_task = task - return task diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 0894a640..9ffd834f 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -441,68 +441,10 @@ class RuntimeContext: runtime_last_response_feedback: dict | None = None - runtime_metabolism_levels: dict = field( - default_factory=lambda: { - "dopamine": 0.42, - "serotonin": 0.58, - "oxytocin": 0.46, - "norepinephrine": 0.38, - "cortisol": 0.24, - } - ) - - runtime_metabolism_recent_turns: list[dict] = field( + runtime_memory_attention_l4_focus_ids: list[str] = field( default_factory=list ) - runtime_metabolism_task: object | None = None - - runtime_metabolism_last_tick_at: float = 0.0 - - runtime_metabolism_last_delta: dict = field( - default_factory=dict - ) - - runtime_metabolism_last_event: str = "" - - runtime_metabolism_last_signal_delta: dict = field( - default_factory=dict - ) - - runtime_metabolism_last_signal_event: str = "" - - runtime_metabolism_last_signal_at: float = 0.0 - - runtime_metabolism_active_memory_salience: dict = field( - default_factory=dict - ) - - runtime_metabolism_memory_significance_events: dict = field( - default_factory=dict - ) - - runtime_metabolism_active_memory_significance_seen_at: dict = field( - default_factory=dict - ) - - runtime_metabolism_l4_significance_seen_at: dict = field( - default_factory=dict - ) - - runtime_metabolism_l4_significance_dirty: bool = False - - runtime_metabolism_policy: dict = field( - default_factory=dict - ) - - runtime_metabolism_instruction: str = "" - - runtime_metabolism_associations: list[dict] = field( - default_factory=list - ) - - runtime_metabolism_last_committed_l1_id: str = "" - runtime_avatar_panel_collapsed: bool = False runtime_avatar_current_size: dict = field( diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 77b5d19e..667ffb04 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -448,7 +448,6 @@ def test_apply_runtime_action_calls_emits_save_active_memory_bubble(self): r"\[ created_jin_message_number: 3 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " - r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) @@ -639,7 +638,6 @@ def test_save_active_memory_replaces_model_runtime_suffixes(self): r"\[ created_jin_message_number: 8 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " - r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) @@ -1662,7 +1660,6 @@ def test_apply_runtime_action_calls_queues_active_memory_record(self): r"\[ created_jin_message_number: 7 \] " r"\[ elapsed_time: 00:00:00 \] " r"\[ elapsed_jin_message_number: 0 \] " - r"\[ significance: 0\.000 \] " r"\[ status: pending \]$" ), ) diff --git a/tests/test_delayed_memory_highlight_contract.py b/tests/test_delayed_memory_highlight_contract.py index 5cf3595b..b95b8f62 100644 --- a/tests/test_delayed_memory_highlight_contract.py +++ b/tests/test_delayed_memory_highlight_contract.py @@ -67,9 +67,6 @@ def test_delayed_memory_uses_one_base_hue(self): 'getMemorySignalColors("delayed", overallColor)', avatar, ) - self.assertNotIn("data-metabolism-base-color", avatar) - self.assertNotIn("data-metabolism-glow-color", avatar) - def test_direct_dm_links_all_own_l4_facts_but_secondary_does_not(self): avatar = AVATAR_JS.read_text(encoding="utf-8") view = MEMORY_VIEW_JS.read_text(encoding="utf-8") diff --git a/tests/test_l4_context_focus_contract.py b/tests/test_l4_context_focus_contract.py index 28f361c8..0945ff3e 100644 --- a/tests/test_l4_context_focus_contract.py +++ b/tests/test_l4_context_focus_contract.py @@ -41,9 +41,9 @@ def test_default_prompt_order_is_newest_fact_id_first(self): self.assertLess(block.index("[ id: F22 ]"), block.index("[ id: F10 ]")) self.assertLess(block.index("[ id: F10 ]"), block.index("[ id: F2 ]")) - self.assertEqual(context.runtime_metabolism_l4_focus_ids, []) + self.assertEqual(context.runtime_memory_attention_l4_focus_ids, []) - def test_metabolism_focus_promotes_only_prompt_view(self): + def test_memory_attention_focus_promotes_only_prompt_view(self): context = self.make_context([ {"id": "F1", "key": "project.unrelated", "value": "Unrelated durable fact."}, { @@ -64,13 +64,13 @@ def test_metabolism_focus_promotes_only_prompt_view(self): ) self.assertLess(block.index("[ id: F22 ]"), block.index("[ id: F1 ]")) - self.assertEqual(context.runtime_metabolism_l4_focus_ids, ["F22"]) + self.assertEqual(context.runtime_memory_attention_l4_focus_ids, ["F22"]) self.assertEqual( [fact["id"] for fact in context.runtime_long_term_memory_store["facts"]], stored_ids_before, ) - def test_metabolism_focus_can_open_to_three_prompt_facts(self): + def test_memory_attention_focus_can_open_to_three_prompt_facts(self): context = self.make_context([ {"id": "F227", "key": "fresh.latest", "value": "Fresh but unrelated."}, {"id": "F226", "key": "fresh.previous", "value": "Also unrelated."}, @@ -78,13 +78,6 @@ def test_metabolism_focus_can_open_to_three_prompt_facts(self): {"id": "F60", "key": "topic.secondary", "value": "Secondary resonant fact."}, {"id": "F40", "key": "topic.tertiary", "value": "Tertiary resonant fact."}, ]) - context.runtime_metabolism_levels = { - "dopamine": 0.72, - "serotonin": 0.58, - "oxytocin": 0.66, - "norepinephrine": 0.76, - "cortisol": 0.18, - } scores = { "F80": 0.62, "F60": 0.53, @@ -94,7 +87,7 @@ def test_metabolism_focus_can_open_to_three_prompt_facts(self): } with patch( - "runtime.metabolism.score_l4_fact_context_focus", + "runtime.memory_attention.score_l4_fact_context_focus", side_effect=lambda fact, **_kwargs: scores[fact["id"]], ): block = build_runtime_l4_memory_context( @@ -107,11 +100,11 @@ def test_metabolism_focus_can_open_to_three_prompt_facts(self): self.assertLess(block.index("[ id: F40 ]"), block.index("[ id: F227 ]")) self.assertLess(block.index("[ id: F227 ]"), block.index("[ id: F226 ]")) self.assertEqual( - context.runtime_metabolism_l4_focus_ids, + context.runtime_memory_attention_l4_focus_ids, ["F80", "F60", "F40"], ) - def test_metabolism_focus_does_not_pad_with_unrelated_facts(self): + def test_memory_attention_focus_does_not_pad_with_unrelated_facts(self): context = self.make_context([ {"id": "F227", "key": "fresh.latest", "value": "Fresh unrelated."}, {"id": "F22", "key": "topic.primary", "value": "Primary resonant fact."}, @@ -120,7 +113,7 @@ def test_metabolism_focus_does_not_pad_with_unrelated_facts(self): scores = {"F22": 0.58, "F21": 0.13, "F227": 0.04} with patch( - "runtime.metabolism.score_l4_fact_context_focus", + "runtime.memory_attention.score_l4_fact_context_focus", side_effect=lambda fact, **_kwargs: scores[fact["id"]], ): block = build_runtime_l4_memory_context( @@ -130,7 +123,7 @@ def test_metabolism_focus_does_not_pad_with_unrelated_facts(self): self.assertLess(block.index("[ id: F22 ]"), block.index("[ id: F227 ]")) self.assertLess(block.index("[ id: F227 ]"), block.index("[ id: F21 ]")) - self.assertEqual(context.runtime_metabolism_l4_focus_ids, ["F22"]) + self.assertEqual(context.runtime_memory_attention_l4_focus_ids, ["F22"]) def test_l4_visual_focus_is_explicit_reasoning_citation_only(self): memory_view = MEMORY_VIEW_JS.read_text(encoding="utf-8") diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index b7d50d96..87f577d0 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -2332,7 +2332,7 @@ def test_context_includes_fact_age_suffix(self): self.assertIn( ( "user.current_focus: User is tuning JIN context freshness. " - "[ id: F9 ] [ significance: 0.000 ] ( 5m ago )" + "[ id: F9 ] ( 5m ago )" ), context_block, ) @@ -2559,7 +2559,7 @@ def test_delayed_report_anchor_stays_visible_with_report_suffix(self): ) self.assertIn( - "user.name: Sergey [ id: F1 ] [ significance: 0.000 ] " + "user.name: Sergey [ id: F1 ] " "[ delayed_memory_id: abc123 ]", context_block, ) @@ -2614,13 +2614,13 @@ def test_loaded_delayed_report_fact_ids_are_visible_with_report_suffix(self): ) self.assertIn( - "user.name: Sergey [ id: F1 ] [ significance: 0.000 ] " + "user.name: Sergey [ id: F1 ] " "[ delayed_memory_id: abc123 ]", context_block, ) self.assertIn( "social.friend: Taras is a personal friend. [ id: F2 ] " - "[ significance: 0.000 ] [ delayed_memory_id: abc123 ]", + "[ delayed_memory_id: abc123 ]", context_block, ) self.assertEqual( diff --git a/tests/test_memory_attention.py b/tests/test_memory_attention.py new file mode 100644 index 00000000..9f9e8d4e --- /dev/null +++ b/tests/test_memory_attention.py @@ -0,0 +1,147 @@ +from copy import deepcopy +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +from runtime.memory_attention import ( + delayed_memory_bubble_tier, + rank_active_memory_records, + rank_l4_facts_for_context, + score_delayed_memory_report, +) + + +class MemoryAttentionTests(unittest.TestCase): + def make_context(self, *, turns=None, facts=None): + return SimpleNamespace( + runtime_recent_turns=list(turns or []), + runtime_long_term_memory_store={"facts": list(facts or [])}, + runtime_memory_attention_l4_focus_ids=[], + ) + + def test_active_uses_current_and_recent_context_without_mutating_records(self): + records = [ + "active_memory_1: tune avatar colour [ active_memory_id: aaa111 ] [ status: pending ]", + "active_memory_2: fix session bootstrap [ active_memory_id: bbb222 ] [ status: pending ]", + ] + before = list(records) + context = self.make_context( + turns=[{"user": "we were debugging session restore"}], + ) + + ranked = rank_active_memory_records( + records, + context=context, + user_input="continue the bootstrap fix", + ) + + self.assertIn("bbb222", ranked[0]) + self.assertEqual(records, before) + + def test_active_explicit_id_wins(self): + records = [ + "active_memory_1: bootstrap work [ active_memory_id: aaa111 ] [ status: pending ]", + "active_memory_2: unrelated [ active_memory_id: bbb222 ] [ status: pending ]", + ] + + ranked = rank_active_memory_records( + records, + context=self.make_context(), + user_input="open bbb222", + ) + + self.assertIn("bbb222", ranked[0]) + + def test_delayed_report_uses_bubble_tiers_and_explicit_id(self): + context = self.make_context() + report = { + "id": "abc123", + "title": "Kowloon architecture", + "summary": "Device runtime decisions", + "tags": ["hardware", "kowloon"], + } + + lexical_score = score_delayed_memory_report( + report, + user_input="continue Kowloon", + context=context, + ) + id_score = score_delayed_memory_report( + report, + user_input="load abc123", + context=context, + ) + + self.assertGreaterEqual(delayed_memory_bubble_tier(lexical_score), 1) + self.assertEqual(id_score, 0.96) + self.assertEqual(delayed_memory_bubble_tier(id_score), 2) + + def test_l4_focus_is_one_to_three_and_prompt_only(self): + facts = [ + {"id": "F30", "key": "fresh", "value": "unrelated"}, + {"id": "F20", "key": "topic.primary", "value": "primary"}, + {"id": "F19", "key": "topic.secondary", "value": "secondary"}, + {"id": "F18", "key": "topic.tertiary", "value": "tertiary"}, + ] + before = deepcopy(facts) + context = self.make_context(facts=facts) + scores = {"F30": 0.01, "F20": 0.70, "F19": 0.60, "F18": 0.50} + + with patch( + "runtime.memory_attention.score_l4_fact_context_focus", + side_effect=lambda fact, **_kwargs: scores[fact["id"]], + ): + ranked = rank_l4_facts_for_context( + facts, + context=context, + user_input="topic", + ) + + self.assertEqual([fact["id"] for fact in ranked[:3]], ["F20", "F19", "F18"]) + self.assertEqual( + context.runtime_memory_attention_l4_focus_ids, + ["F20", "F19", "F18"], + ) + self.assertEqual(facts, before) + + def test_l4_does_not_pad_focus_with_unrelated_facts(self): + facts = [ + {"id": "F3", "key": "fresh", "value": "unrelated"}, + {"id": "F2", "key": "topic", "value": "match"}, + {"id": "F1", "key": "noise", "value": "weak"}, + ] + context = self.make_context(facts=facts) + scores = {"F3": 0.01, "F2": 0.65, "F1": 0.10} + + with patch( + "runtime.memory_attention.score_l4_fact_context_focus", + side_effect=lambda fact, **_kwargs: scores[fact["id"]], + ): + ranked = rank_l4_facts_for_context( + facts, + context=context, + user_input="topic", + ) + + self.assertEqual(context.runtime_memory_attention_l4_focus_ids, ["F2"]) + self.assertEqual([fact["id"] for fact in ranked], ["F2", "F3", "F1"]) + + def test_l4_recent_facts_do_not_focus_themselves(self): + facts = [ + {"id": "F2", "key": "project.kowloon", "value": "device architecture"}, + {"id": "F1", "key": "user.camera", "value": "photography"}, + ] + context = self.make_context(facts=facts) + + ranked = rank_l4_facts_for_context( + facts, + context=context, + user_input="what should I cook for dinner", + ) + + self.assertEqual([fact["id"] for fact in ranked], ["F2", "F1"]) + self.assertEqual(context.runtime_memory_attention_l4_focus_ids, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_metabolism.py b/tests/test_metabolism.py deleted file mode 100644 index 73ca2c5d..00000000 --- a/tests/test_metabolism.py +++ /dev/null @@ -1,426 +0,0 @@ -import asyncio -import json -import unittest -from types import SimpleNamespace -from unittest.mock import AsyncMock, patch - -from runtime.metabolism import ( - METABOLISM_DEFAULT_LEVELS, - METABOLISM_MAX_OUTPUT_TOKENS, - METABOLISM_MAX_STEP, - METABOLISM_SYSTEM_PROMPT, - append_metabolism_turn, - apply_metabolism_target, - build_metabolism_snapshot, - compact_metabolism_snapshot_for_context, - normalize_metabolism_levels, - parse_metabolism_response, - recover_metabolism_levels_locally, - schedule_metabolism_update, -) - - -class MetabolismStateTests(unittest.TestCase): - def test_normalizes_and_clamps_exact_five_channels(self): - levels = normalize_metabolism_levels({ - "dopamine": 1.5, - "serotonin": -1, - "oxytocin": 0.3333, - "norepinephrine": "0.7", - "cortisol": None, - }) - - self.assertEqual(levels["dopamine"], 1.0) - self.assertEqual(levels["serotonin"], 0.0) - self.assertEqual(levels["oxytocin"], 0.333) - self.assertEqual(levels["norepinephrine"], 0.7) - self.assertEqual( - levels["cortisol"], - METABOLISM_DEFAULT_LEVELS["cortisol"], - ) - self.assertEqual(set(levels), set(METABOLISM_DEFAULT_LEVELS)) - - def test_runtime_owns_inertia_even_when_service_target_jumps(self): - previous = dict(METABOLISM_DEFAULT_LEVELS) - target = { - "dopamine": 1.0, - "serotonin": 0.0, - "oxytocin": 1.0, - "norepinephrine": 1.0, - "cortisol": 1.0, - } - - levels = apply_metabolism_target(previous, target) - - for channel, before in previous.items(): - self.assertLessEqual( - abs(levels[channel] - before), - METABOLISM_MAX_STEP[channel] + 1e-9, - ) - - self.assertEqual(levels["dopamine"], 0.50) - self.assertEqual(levels["serotonin"], 0.54) - self.assertEqual(levels["oxytocin"], 0.50) - self.assertEqual(levels["norepinephrine"], 0.46) - self.assertEqual(levels["cortisol"], 0.32) - - def test_snapshot_keeps_five_turns_actions_runtime_and_l4_index_only(self): - context = SimpleNamespace( - runtime_metabolism_levels=dict(METABOLISM_DEFAULT_LEVELS), - runtime_metabolism_recent_turns=[], - runtime_long_term_memory_store={ - "facts": [ - {"id": "F1", "key": "one", "value": "alpha", "category": "other"}, - {"id": "F2", "key": "two", "value": "beta", "category": "project_fact"}, - ] - }, - runtime_session_action_history=[ - { - "text": "UPDATE_ACTIVE_MEMORY:failed", - "runtime_turn_id": "turn-12", - "parts": [ - { - "text": "UPDATE_ACTIVE_MEMORY:failed", - "detail": "incorrect id", - } - ], - } - ], - runtime_current_sequence_turn_id="turn-12", - runtime_memory="state: current", - turn_number=12, - user_message_count=12, - assistant_message_count=11, - runtime_turn_interrupted=False, - runtime_turn_interruption_reason="", - runtime_last_response_feedback={"rating": "liked"}, - ) - - for index in range(7): - append_metabolism_turn( - context, - user_message=f"user-{index}", - assistant_message=f"jin-{index}", - reasoning=f"reason-{index}", - feedback=( - {"rating": "liked", "clicks_count": 1} - if index == 6 - else None - ), - ) - - snapshot = build_metabolism_snapshot(context) - - self.assertEqual(len(snapshot["recent_interactions"]), 5) - self.assertEqual(snapshot["recent_interactions"][0]["user"], "user-2") - self.assertEqual(snapshot["recent_interactions"][-1]["reasoning"], "reason-6") - self.assertEqual( - snapshot["recent_interactions"][-1]["feedback"], - {"rating": "liked", "clicks_count": 1}, - ) - self.assertEqual(len(snapshot["l4_index"]), 2) - self.assertNotIn("value", snapshot["l4_index"][0]) - self.assertEqual(snapshot["runtime_state"]["runtime_memory"], "state: current") - self.assertEqual(snapshot["runtime_state"]["latest_user_feedback"]["rating"], "liked") - self.assertEqual(len(snapshot["recent_runtime_actions"]), 1) - self.assertTrue(snapshot["recent_runtime_actions"][0]["current_turn"]) - self.assertEqual( - snapshot["recent_runtime_actions"][0]["parts"][0]["detail"], - "incorrect id", - ) - - def test_response_parser_accepts_fenced_json_and_requires_all_channels(self): - response = { - "choices": [{ - "message": { - "content": "```json\n{\"dopamine\":0.5,\"serotonin\":0.6,\"oxytocin\":0.4,\"norepinephrine\":0.3,\"cortisol\":0.2}\n```" - } - }] - } - levels, raw = parse_metabolism_response(response) - self.assertIsNotNone(levels) - self.assertEqual(levels["dopamine"], 0.5) - self.assertIn("dopamine", raw) - - invalid, _ = parse_metabolism_response({ - "choices": [{"message": {"content": '{"dopamine":0.5}'}}] - }) - self.assertIsNone(invalid) - - extra, _ = parse_metabolism_response({ - "choices": [{"message": {"content": json.dumps({ - "dopamine": 0.5, - "serotonin": 0.6, - "oxytocin": 0.4, - "norepinephrine": 0.3, - "cortisol": 0.2, - "mood": "happy", - })}}] - }) - self.assertIsNone(extra) - - directed, _ = parse_metabolism_response({ - "choices": [{"message": {"content": json.dumps({ - "dopamine": 0.5, - "serotonin": 0.6, - "oxytocin": 0.4, - "norepinephrine": 0.3, - "cortisol": 0.2, - "instruction": "Keep continuity and verify the relevant memory before committing.", - })}}] - }) - self.assertIsNotNone(directed) - - def test_context_budget_compacts_only_when_needed(self): - snapshot = { - "baseline": dict(METABOLISM_DEFAULT_LEVELS), - "previous_levels": dict(METABOLISM_DEFAULT_LEVELS), - "runtime_state": { - "runtime_memory": "runtime " * 5000, - "turn_number": 1, - "user_messages": 1, - "jin_messages": 1, - "turn_interrupted": False, - "interruption_reason": "", - "latest_user_feedback": None, - }, - "recent_runtime_actions": [ - { - "text": "UPDATE_ACTIVE_MEMORY:failed " * 100, - "current_turn": True, - "parts": [{"text": "failed", "detail": "incorrect id " * 100}], - } - for _ in range(10) - ], - "l4_index": [ - { - "id": f"F{index}", - "key": (f"key-{index} " * 100), - "category": "project_fact", - } - for index in range(30) - ], - "l4_facts_total": 30, - "recent_interactions": [ - { - "user": "user " * 2000, - "jin": "jin " * 2000, - "reasoning": "reasoning " * 2000, - } - for _ in range(5) - ], - } - - unchanged = compact_metabolism_snapshot_for_context( - snapshot, - 100000, - ) - self.assertIs(unchanged, snapshot) - - compacted = compact_metabolism_snapshot_for_context( - snapshot, - 4096, - ) - self.assertTrue(compacted["snapshot_compaction"]["applied"]) - self.assertEqual(compacted["l4_facts_total"], 30) - self.assertLessEqual( - compacted["snapshot_compaction"]["estimated_input_tokens"], - compacted["snapshot_compaction"]["input_budget_tokens"], - ) - - def test_instruction_prioritizes_runtime_evidence_and_runtime_owned_physics(self): - self.assertIn("USER feedback/corrections", METABOLISM_SYSTEM_PROMPT) - self.assertIn("concrete runtime action outcomes", METABOLISM_SYSTEM_PROMPT) - self.assertIn("runtime applies inertia", METABOLISM_SYSTEM_PROMPT) - self.assertIn("strict JSON only", METABOLISM_SYSTEM_PROMPT) - self.assertIn("committed_l1.last_user_input", METABOLISM_SYSTEM_PROMPT) - - -class MetabolismSchedulingTests(unittest.IsolatedAsyncioTestCase): - def build_context(self): - logger = SimpleNamespace(log_metabolism=AsyncMock()) - emitter = SimpleNamespace(emit=AsyncMock()) - service_client = SimpleNamespace(model_uid="service-test", configured_context_window=32768) - context = SimpleNamespace( - clients={"service": service_client}, - logger=logger, - emitter=emitter, - background_tasks=set(), - runtime_memory_update_task=None, - runtime_metabolism_task=None, - runtime_metabolism_levels=dict(METABOLISM_DEFAULT_LEVELS), - runtime_metabolism_associations=[], - runtime_metabolism_last_committed_l1_id="", - runtime_metabolism_last_tick_at=0.0, - runtime_metabolism_recent_turns=[ - {"user": "u", "jin": "j", "reasoning": "r"} - ], - runtime_long_term_memory_store={"facts": []}, - runtime_session_action_history=[], - runtime_current_sequence_turn_id="", - runtime_current_turn_id="", - runtime_memory="snapshot", - runtime_recent_turns=[], - runtime_restored_session_dialog="", - active_memory_records=[], - delayed_memory_reports={}, - turn_number=1, - user_message_count=1, - assistant_message_count=1, - runtime_turn_interrupted=False, - runtime_turn_interruption_reason="", - runtime_last_response_feedback=None, - ) - return context, logger, emitter - - async def test_background_update_logs_request_response_applies_inertia_and_emits(self): - context, logger, emitter = self.build_context() - response = { - "choices": [{ - "message": { - "content": json.dumps({ - "dopamine": 0.90, - "serotonin": 0.20, - "oxytocin": 0.90, - "norepinephrine": 0.90, - "cortisol": 0.90, - }) - } - }] - } - - with patch( - "runtime.metabolism.ask_service_model", - new=AsyncMock(return_value=response), - ): - task = schedule_metabolism_update(context) - self.assertIsNotNone(task) - result = await task - - self.assertEqual(result["dopamine"], 0.50) - self.assertEqual(result["serotonin"], 0.54) - self.assertEqual(logger.log_metabolism.await_count, 2) - request_call = logger.log_metabolism.await_args_list[0] - response_call = logger.log_metabolism.await_args_list[1] - self.assertEqual( - request_call.kwargs["request_id"], - response_call.kwargs["request_id"], - ) - emitter.emit.assert_awaited_once() - payload = emitter.emit.await_args.args[0] - self.assertEqual(payload["type"], "metabolism_update") - self.assertEqual(payload["levels"]["cortisol"], 0.32) - - async def test_invalid_primary_response_is_recovered_locally_without_second_inference(self): - context, logger, emitter = self.build_context() - loose_response = { - "choices": [{ - "message": { - "content": ( - "dopamine: 0.61, serotonin: 0.62, oxytocin: 0.49, " - "norepinephrine: 0.44, cortisol: 0.19" - ) - } - }] - } - ask_mock = AsyncMock(return_value=loose_response) - - with patch( - "runtime.metabolism.ask_service_model", - new=ask_mock, - ): - task = schedule_metabolism_update(context, debounce_seconds=0) - self.assertIsNotNone(task) - result = await task - - self.assertEqual(ask_mock.await_count, 1) - self.assertEqual(result["dopamine"], 0.50) - self.assertEqual(result["serotonin"], 0.62) - response_call = logger.log_metabolism.await_args_list[1] - details = json.loads(response_call.kwargs["details"]) - self.assertEqual(details["status"], "local_recovered") - emitter.emit.assert_awaited_once() - - async def test_unrecoverable_response_keeps_previous_state_without_second_inference(self): - context, logger, _ = self.build_context() - broken_response = { - "choices": [{"message": {"content": "I cannot produce the requested state."}}] - } - ask_mock = AsyncMock(return_value=broken_response) - - with patch("runtime.metabolism.ask_service_model", new=ask_mock): - task = schedule_metabolism_update(context, debounce_seconds=0) - result = await task - - self.assertEqual(ask_mock.await_count, 1) - self.assertEqual(result, METABOLISM_DEFAULT_LEVELS) - details = json.loads(logger.log_metabolism.await_args_list[1].kwargs["details"]) - self.assertEqual(details["status"], "invalid") - - async def test_reasoning_model_has_room_and_reports_output_truncation_explicitly(self): - context, logger, _ = self.build_context() - truncated_response = { - "choices": [{ - "finish_reason": "length", - "message": { - "content": "", - "reasoning_content": "Analysis stopped before the final JSON.", - }, - }], - "usage": { - "prompt_tokens": 400, - "completion_tokens": METABOLISM_MAX_OUTPUT_TOKENS, - "total_tokens": 400 + METABOLISM_MAX_OUTPUT_TOKENS, - }, - } - ask_mock = AsyncMock(return_value=truncated_response) - - with patch("runtime.metabolism.ask_service_model", new=ask_mock): - task = schedule_metabolism_update(context, debounce_seconds=0) - result = await task - - self.assertGreaterEqual(METABOLISM_MAX_OUTPUT_TOKENS, 1024) - self.assertEqual( - ask_mock.await_args.kwargs["max_tokens"], - METABOLISM_MAX_OUTPUT_TOKENS, - ) - self.assertEqual(result, METABOLISM_DEFAULT_LEVELS) - details = json.loads(logger.log_metabolism.await_args_list[1].kwargs["details"]) - self.assertEqual(details["status"], "truncated") - self.assertEqual(details["finish_reason"], "length") - self.assertEqual( - details["usage"]["completion_tokens"], - METABOLISM_MAX_OUTPUT_TOKENS, - ) - self.assertEqual(details["raw"], "") - self.assertNotIn("Analysis stopped", details["raw"]) - self.assertIn("output token limit", details["error"]) - - async def test_cancelled_request_logs_terminal_result_with_same_request_id(self): - context, logger, _ = self.build_context() - started = asyncio.Event() - - async def slow_service(**kwargs): - started.set() - await asyncio.sleep(30) - - with patch( - "runtime.metabolism.ask_service_model", - new=slow_service, - ): - task = schedule_metabolism_update(context) - await started.wait() - task.cancel() - with self.assertRaises(asyncio.CancelledError): - await task - - self.assertEqual(logger.log_metabolism.await_count, 2) - first = logger.log_metabolism.await_args_list[0] - second = logger.log_metabolism.await_args_list[1] - self.assertEqual(first.kwargs["request_id"], second.kwargs["request_id"]) - self.assertEqual(second.kwargs["event"], "result") - self.assertIn("cancelled", second.args[0]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_metabolism_client_contract.py b/tests/test_metabolism_client_contract.py deleted file mode 100644 index 2dc7cf93..00000000 --- a/tests/test_metabolism_client_contract.py +++ /dev/null @@ -1,64 +0,0 @@ -from pathlib import Path -import unittest - - -ROOT = Path(__file__).resolve().parents[1] - - -class MetabolismClientContractTests(unittest.TestCase): - def test_palette_ambient_aura_and_memory_tint_hook_are_present(self): - metabolism = (ROOT / "ui/static/js/runtime/runtime-metabolism.js").read_text() - avatar = (ROOT / "ui/static/js/runtime/runtime-avatar.js").read_text() - avatar_css = (ROOT / "ui/static/css/runtime-avatar.css").read_text() - - for color in ("#fbbf24", "#60a5fa", "#f472b6", "#f97316", "#34d399"): - self.assertIn(color, metabolism) - self.assertIn(color, avatar_css) - - self.assertIn("jin:metabolism-update", metabolism) - self.assertIn("getDominantChannel", metabolism) - self.assertIn('return metabolismApi ? "ambient" : ""', avatar) - self.assertIn("refreshRenderedMetabolismTints", avatar) - self.assertIn("--jin-metabolism-dopamine-aura", avatar_css) - self.assertIn("-webkit-mask-image: radial-gradient(", avatar_css) - self.assertIn("transparent 84%", avatar_css) - - def test_metabolic_spots_keep_original_geometry_but_gain_value_driven_drift(self): - metabolism = (ROOT / "ui/static/js/runtime/runtime-metabolism.js").read_text() - avatar_css = (ROOT / "ui/static/css/runtime-avatar.css").read_text() - - self.assertIn("METABOLIC_SPOT_DRIFT_ENABLED = true", metabolism) - self.assertIn("SPOT_GEOMETRY", metabolism) - self.assertIn("dopamine: Object.freeze({ x: 22, y: 24, radius: 42 })", metabolism) - self.assertIn("serotonin: Object.freeze({ x: 78, y: 20, radius: 43 })", metabolism) - self.assertIn("getSpotRadiusPercent", metabolism) - self.assertIn("getSpotDriftSpeed", metabolism) - self.assertIn("scheduleSpotDrift", metabolism) - self.assertIn("getRenderedSpotPosition", metabolism) - self.assertIn("retargetSpotDriftForCurrentState", metabolism) - self.assertIn("updateSpotSizes: true", metabolism) - - self.assertIn("@property --jin-metabolism-dopamine-x", avatar_css) - self.assertIn("--jin-metabolism-dopamine-drift-duration", avatar_css) - self.assertIn("--jin-metabolism-dopamine-radius 14s", avatar_css) - self.assertIn("var(--jin-metabolism-dopamine-x, 22%)", avatar_css) - self.assertIn("var(--jin-metabolism-dopamine-radius, 42%)", avatar_css) - - def test_logger_card_pairs_request_response_and_reuses_trace_pattern(self): - logger = (ROOT / "ui/static/js/logger/log-entries.js").read_text() - modal = (ROOT / "ui/static/js/logger/trace-modal.js").read_text() - css = (ROOT / "ui/static/css/runtime-memory.css").read_text() - - self.assertIn('createL4LoggerButton(\n "request"', logger) - self.assertIn('createL4LoggerButton(\n "response"', logger) - self.assertIn("metabolism_request_id", logger) - self.assertIn("candidate.requestId === requestId", logger) - self.assertIn("metabolism_request", modal) - self.assertIn("Runtime actions", modal) - self.assertIn("Service target", modal) - self.assertIn("Applied delta", modal) - self.assertIn("jin-metabolism-card", css) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_metabolism_homeostat.py b/tests/test_metabolism_homeostat.py deleted file mode 100644 index 8bfefcf7..00000000 --- a/tests/test_metabolism_homeostat.py +++ /dev/null @@ -1,449 +0,0 @@ -import time -import unittest -from pathlib import Path -from types import SimpleNamespace - -from rules.brain_context_builder import build_delayed_memory_inventory_context -from runtime.metabolism import ( - METABOLISM_DEFAULT_LEVELS, - METABOLISM_HALF_LIFE_SECONDS, - METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD, - advance_metabolism_clock, - apply_metabolism_impulse, - build_metabolism_brain_context, - build_metabolism_reflex, - learn_metabolism_associations, - build_runtime_outcome_reflex, - current_metabolic_event_significance, - metabolic_memory_strength_boost, - rank_active_memory_records, - rank_l4_facts_for_context, - resolve_metabolism_temperature, -) - - -ROOT = Path(__file__).resolve().parents[1] - - -class MetabolismHomeostatTests(unittest.TestCase): - def _context(self, **overrides): - values = { - "runtime_metabolism_levels": dict(METABOLISM_DEFAULT_LEVELS), - "runtime_metabolism_last_tick_at": 0.0, - "runtime_metabolism_last_delta": {}, - "runtime_metabolism_last_event": "", - "runtime_metabolism_last_signal_delta": {}, - "runtime_metabolism_last_signal_event": "", - "runtime_metabolism_last_signal_at": 0.0, - "runtime_metabolism_memory_significance_events": {}, - "runtime_metabolism_active_memory_significance_seen_at": {}, - "runtime_metabolism_l4_significance_seen_at": {}, - "runtime_metabolism_l4_significance_dirty": False, - "runtime_metabolism_active_memory_salience": {}, - "runtime_metabolism_policy": {}, - "runtime_last_response_feedback": None, - "runtime_metabolism_associations": [], - "runtime_recent_turns": [], - "runtime_metabolism_recent_turns": [], - "runtime_restored_session_dialog": "", - "runtime_memory": "", - "delayed_memory_reports": {}, - "active_memory_records": [], - } - values.update(overrides) - return SimpleNamespace(**values) - - def test_wall_clock_homeostasis_returns_each_channel_toward_baseline(self): - start = 1_700_000_000.0 - levels = { - "dopamine": 0.82, - "serotonin": 0.82, - "oxytocin": 0.76, - "norepinephrine": 0.72, - "cortisol": 0.64, - } - context = self._context( - runtime_metabolism_levels=levels, - runtime_metabolism_last_tick_at=start, - ) - - result = advance_metabolism_clock( - context, - now=start + METABOLISM_HALF_LIFE_SECONDS["dopamine"], - ) - - for channel, previous in levels.items(): - baseline = METABOLISM_DEFAULT_LEVELS[channel] - self.assertLess(abs(result[channel] - baseline), abs(previous - baseline)) - self.assertEqual(context.runtime_metabolism_last_event, "homeostasis") - - def test_lexical_reflex_has_no_builtin_user_vocabulary_and_reuses_learned_runtime_cue(self): - context = self._context( - runtime_memory="avatar latency remains an unresolved runtime issue", - runtime_metabolism_associations=[{ - "phrase": "avatar latency", - "vector": { - "dopamine": -0.01, - "serotonin": -0.01, - "oxytocin": 0.0, - "norepinephrine": 0.03, - "cortisol": 0.04, - }, - "weight": 0.9, - "hits": 2, - "updated_at": time.time(), - "source": "session:4", - }], - ) - - neutral, neutral_triggers = build_metabolism_reflex( - "спасибо бро, кайф, давай разберись с кодом", - context=context, - ) - self.assertFalse(neutral_triggers) - self.assertTrue(all(value == 0 for value in neutral.values())) - - impulse, triggers = build_metabolism_reflex( - "avatar latency опять появилась", - context=context, - ) - self.assertTrue(any(item.startswith("learned:avatar latency") for item in triggers)) - self.assertGreater(impulse["norepinephrine"], 0) - self.assertGreater(impulse["cortisol"], 0) - - def test_committed_l1_learns_last_input_cue_from_runtime_and_reuses_it(self): - context = self._context( - session_id="session-current", - runtime_memory="ui_state: avatar latency is causing dissatisfaction", - runtime_recent_turns=[ - {"user": "we saw avatar latency yesterday", "jin": "noted"}, - ], - runtime_restored_session_dialog="previous session also mentioned avatar latency", - ) - before = dict(METABOLISM_DEFAULT_LEVELS) - after = { - **before, - "norepinephrine": 0.43, - "cortisol": 0.29, - } - - learned = learn_metabolism_associations( - context, - committed_snapshot={ - "index": 7, - "total_diff": 30, - "patch": {"changed": [{"current_key": "ui_state", "current_value": "avatar latency dissatisfaction"}]}, - }, - committed_turns=[{ - "user_message": "avatar latency опять мешает", - "assistant_message": "исправлю", - }], - previous_levels=before, - current_levels=after, - ) - - self.assertTrue(any("avatar latency" in item["phrase"] for item in learned)) - impulse, triggers = build_metabolism_reflex( - "снова avatar latency", - context=context, - ) - self.assertTrue(any(item.startswith("learned:") for item in triggers)) - self.assertGreater(impulse["cortisol"], 0) - self.assertGreater(impulse["norepinephrine"], 0) - - def test_runtime_action_failure_leaves_immediate_trace_before_background_estimator(self): - context = self._context( - runtime_current_sequence_turn_id="turn_000007", - runtime_current_turn_id="turn_000007", - runtime_turn_interrupted=False, - runtime_turn_aborted_actions=[], - runtime_pattern_counter=0, - runtime_turn_reasoning_content="short reasoning", - runtime_session_action_history=[ - { - "runtime_turn_id": "turn_000007", - "text": "UPDATE_ACTIVE_MEMORY: failed", - "parts": [{"detail": "incorrect id"}], - } - ], - ) - - impulse, triggers = build_runtime_outcome_reflex(context) - - self.assertIn("runtime_failure", triggers) - self.assertGreater(impulse["cortisol"], 0) - self.assertGreater(impulse["norepinephrine"], 0) - self.assertLess(impulse["dopamine"], 0) - - def test_temperature_is_causal_but_oxytocin_does_not_make_agreement_more_likely(self): - high_explore = self._context(runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "dopamine": 0.78, - "norepinephrine": 0.66, - }) - high_pressure = self._context(runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "serotonin": 0.80, - "cortisol": 0.74, - }) - high_oxy = self._context(runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "oxytocin": 0.90, - }) - - base = 0.30 - self.assertGreater(resolve_metabolism_temperature(base, high_explore), base) - self.assertLess(resolve_metabolism_temperature(base, high_pressure), base) - self.assertEqual(resolve_metabolism_temperature(base, high_oxy), base) - - def test_brain_context_uses_generated_instruction_without_channel_labels(self): - context = self._context( - runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "oxytocin": 0.72, - }, - runtime_metabolism_instruction=( - "Use shared history only where it helps the current request; " - "keep continuity without agreeing with unsupported premises." - ), - ) - - prompt = build_metabolism_brain_context( - context, - user_input="продолжим наш JIN", - ) - - self.assertIn("Possible reaction: Use shared history only where it helps", prompt) - self.assertNotIn("Oxytocin is elevated", prompt) - self.assertNotIn("Serotonin is elevated", prompt) - self.assertIn('role="silent_homeostat"', prompt) - - def test_active_memory_prompt_order_is_state_and_query_sensitive_without_mutating_storage(self): - records = [ - "active_memory_1: keep project naming stable [ active_memory_id: aaa111 ] [ status: pending ]", - "active_memory_2: proactively verify code errors [ active_memory_id: bbb222 ] [ status: pending ]", - "active_memory_3: preserve social continuity with the user [ active_memory_id: ccc333 ] [ status: pending ]", - ] - context = self._context( - runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "norepinephrine": 0.78, - }, - active_memory_records=list(records), - ) - - ranked = rank_active_memory_records( - records, - context=context, - user_input="проверь code error и верифицируй фикс", - ) - - self.assertIn("[ active_memory_id: bbb222 ]", ranked[0]) - self.assertIn("[ significance: 0.000 ]", ranked[0]) - self.assertEqual( - [ - record.split("[ active_memory_id: ", 1)[1].split(" ]", 1)[0] - for record in context.active_memory_records - ], - ["aaa111", "bbb222", "ccc333"], - ) - self.assertTrue( - all("[ significance: 0.000 ]" in record for record in context.active_memory_records) - ) - self.assertGreater( - context.runtime_metabolism_active_memory_salience["bbb222"], - context.runtime_metabolism_active_memory_salience["aaa111"], - ) - - def test_recent_l4_fact_can_bubble_matching_active_memory_on_elliptical_input(self): - records = [ - "active_memory_1: verify GPU telemetry [ active_memory_id: aaa111 ] [ status: pending ]", - "active_memory_2: continue Kowloon director architecture [ active_memory_id: bbb222 ] [ status: pending ]", - ] - context = self._context( - active_memory_records=list(records), - runtime_long_term_memory_store={ - "facts": [ - { - "id": "F98", - "key": "Kowloon architecture", - "value": "director controls scenes and Maya", - } - ] - }, - ) - - ranked = rank_active_memory_records( - records, - context=context, - user_input="продолжим это дальше", - ) - - self.assertIn("[ active_memory_id: bbb222 ]", ranked[0]) - self.assertIn("[ significance: 0.000 ]", ranked[0]) - self.assertGreater( - context.runtime_metabolism_active_memory_salience["bbb222"], - context.runtime_metabolism_active_memory_salience["aaa111"], - ) - - def test_metabolic_event_significance_tracks_state_displacement_not_absolute_level(self): - context = self._context( - runtime_metabolism_last_signal_event="semantic_integration", - runtime_metabolism_last_signal_at=time.time(), - runtime_metabolism_last_signal_delta={ - "dopamine": 0.12, - "serotonin": 0.08, - "oxytocin": 0.09, - "norepinephrine": 0.10, - "cortisol": 0.10, - }, - ) - - significance = current_metabolic_event_significance(context) - - self.assertGreaterEqual(significance, 0.99) - - def test_high_significance_active_memory_owns_top_lane(self): - records = [ - "active_memory_1: unrelated durable event [ active_memory_id: aaa111 ] [ significance: 0.800 ] [ status: pending ]", - "active_memory_2: verify exact code error [ active_memory_id: bbb222 ] [ significance: 0.200 ] [ status: pending ]", - ] - context = self._context(active_memory_records=list(records)) - - ranked = rank_active_memory_records( - records, - context=context, - user_input="verify exact code error", - ) - - self.assertIn("[ active_memory_id: aaa111 ]", ranked[0]) - self.assertGreaterEqual( - float(ranked[0].split("[ significance: ", 1)[1].split(" ]", 1)[0]), - METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD, - ) - - def test_high_significance_l4_fact_owns_top_lane(self): - facts = [ - { - "id": "F2", - "key": "code.current_error", - "value": "verify exact code error", - "significance": 0.2, - }, - { - "id": "F1", - "key": "project.durable_event", - "value": "unrelated but strongly state-shifting event", - "significance": 0.8, - }, - ] - context = self._context() - - ranked = rank_l4_facts_for_context( - facts, - context=context, - user_input="verify exact code error", - ) - - self.assertEqual(ranked[0]["id"], "F1") - self.assertGreaterEqual( - ranked[0]["significance"], - METABOLISM_MEMORY_SIGNIFICANCE_TOP_THRESHOLD, - ) - - def test_delayed_inventory_keeps_recency_by_default_but_bubbles_live_match(self): - context = self._context( - delayed_memory_reports={ - "new111": { - "title": "Fresh unrelated GPU notes", - "summary": "GPU telemetry and percentages", - "last_loaded_date": "2026-08-20T14:00:00+03:00", - }, - "old222": { - "title": "Kowloon Sandbox Architecture", - "summary": "Director, scenes, Maya and night simulation", - "last_loaded_date": "2026-08-01T10:00:00+03:00", - }, - }, - ) - - neutral = build_delayed_memory_inventory_context( - context, - user_input="совсем другая тема", - ) - self.assertLess( - neutral.index("new111_Fresh_unrelated_GPU_notes"), - neutral.index("old222_Kowloon_Sandbox_Architecture"), - ) - - relevant = build_delayed_memory_inventory_context( - context, - user_input="давай дальше про Kowloon и директора", - ) - self.assertLess( - relevant.index("old222_Kowloon_Sandbox_Architecture"), - relevant.index("new111_Fresh_unrelated_GPU_notes"), - ) - - def test_state_shift_biases_existing_l1_strength_instead_of_creating_a_new_memory_type(self): - context = self._context( - runtime_metabolism_levels={ - **METABOLISM_DEFAULT_LEVELS, - "norepinephrine": 0.72, - "cortisol": 0.50, - }, - runtime_metabolism_last_delta={ - "dopamine": 0.03, - "serotonin": -0.02, - "oxytocin": 0.01, - "norepinephrine": 0.06, - "cortisol": 0.05, - }, - runtime_metabolism_associations=[{ - "phrase": "validator error", - "vector": { - "dopamine": -0.01, - "serotonin": -0.01, - "oxytocin": 0.0, - "norepinephrine": 0.04, - "cortisol": 0.04, - }, - "weight": 1.0, - "hits": 2, - "updated_at": time.time(), - "source": "test", - }], - ) - - boost, salience, channel = metabolic_memory_strength_boost( - context, - key="validator_error", - value="verify failed action and prevent contradiction", - status="changed", - ) - - self.assertGreater(boost, 0) - self.assertGreater(salience, 0) - self.assertIn(channel, {"norepinephrine", "cortisol"}) - - -class MetabolismUiContractTests(unittest.TestCase): - def test_avatar_uses_existing_tint_opacity_scale_language_with_long_transition(self): - metabolism = (ROOT / "ui/static/js/runtime/runtime-metabolism.js").read_text(encoding="utf-8") - avatar = (ROOT / "ui/static/js/runtime/runtime-avatar.js").read_text(encoding="utf-8") - css = (ROOT / "ui/static/css/runtime-avatar.css").read_text(encoding="utf-8") - events = (ROOT / "ui/static/js/socket/event-handlers.js").read_text(encoding="utf-8") - - self.assertIn("AMBIENT_BODY_MOTION_ENABLED", metabolism) - self.assertIn("getBlendedChemistryColor", metabolism) - self.assertIn("IDLE_HOMEOSTASIS_TICK_MS", metabolism) - self.assertIn("--jin-avatar-metabolism-body-scale", metabolism) - self.assertIn("--jin-avatar-metabolism-opacity", metabolism) - self.assertIn("58s", css) - self.assertIn("getActiveMemorySalience", avatar) - self.assertIn("metabolism.applyServerUpdate(\n data", events) - self.assertNotIn("metabolism bubble", metabolism.lower()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_runtime_avatar_node_payload_client_contract.py b/tests/test_runtime_avatar_node_payload_client_contract.py index 7cfbe879..4b58d181 100644 --- a/tests/test_runtime_avatar_node_payload_client_contract.py +++ b/tests/test_runtime_avatar_node_payload_client_contract.py @@ -36,9 +36,6 @@ def test_rich_avatar_payload_is_kept_out_of_svg_datasets(self): "data-delayed-memory-anchor-fact-ids", "data-avatar-memory-angle", "data-linked-delayed-memory-ids", - "data-metabolism-channel", - "data-metabolism-base-color", - "data-metabolism-glow-color", ): self.assertNotIn(attribute, source) @@ -60,7 +57,7 @@ def test_svg_keeps_only_lightweight_identity_and_sync_hooks(self): self.assertIn("getAvatarNodeState(node)", source) self.assertIn("getAvatarMemoryReferenceAliases(node)", source) - def test_dead_svg_titles_and_per_record_metabolism_payload_are_gone(self): + def test_dead_svg_titles_and_per_record_visual_payload_are_gone(self): source = AVATAR_JS.read_text(encoding="utf-8") self.assertNotIn('createSvgElement("title")', source) diff --git a/tests/test_user_retry_contract.py b/tests/test_user_retry_contract.py index 1cc357d2..eae7bc9b 100644 --- a/tests/test_user_retry_contract.py +++ b/tests/test_user_retry_contract.py @@ -131,7 +131,6 @@ def test_server_rebuilds_same_request_and_refreshes_live_runtime_state(self): def test_retry_discards_previous_prompt_turn_and_reasoning(self): context = SimpleNamespace( runtime_recent_turns=[{"user": "u", "jin": "old"}], - runtime_metabolism_recent_turns=[{"user": "u", "jin": "old"}], runtime_previous_reasoning_content="old reasoning", runtime_previous_reasoning_loop_contents=["old loop"], ) @@ -140,7 +139,6 @@ def test_retry_discards_previous_prompt_turn_and_reasoning(self): self.assertEqual(previous["jin"], "old") self.assertEqual(context.runtime_recent_turns, []) - self.assertEqual(context.runtime_metabolism_recent_turns, []) self.assertEqual(context.runtime_previous_reasoning_content, "") self.assertEqual(context.runtime_previous_reasoning_loop_contents, []) diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index d50260d9..12fe8fa5 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -1,83 +1,5 @@ /* Динамический SVG-аватар и его состояния внутри runtime-панели. */ -/* Typed custom properties let each existing metabolic gradient drift - independently without replacing the original radial-gradient design. */ -@property --jin-metabolism-dopamine-x { - syntax: ""; - inherits: true; - initial-value: 22%; -} -@property --jin-metabolism-dopamine-y { - syntax: ""; - inherits: true; - initial-value: 24%; -} -@property --jin-metabolism-dopamine-radius { - syntax: ""; - inherits: true; - initial-value: 42%; -} -@property --jin-metabolism-serotonin-x { - syntax: ""; - inherits: true; - initial-value: 78%; -} -@property --jin-metabolism-serotonin-y { - syntax: ""; - inherits: true; - initial-value: 20%; -} -@property --jin-metabolism-serotonin-radius { - syntax: ""; - inherits: true; - initial-value: 43%; -} -@property --jin-metabolism-oxytocin-x { - syntax: ""; - inherits: true; - initial-value: 87%; -} -@property --jin-metabolism-oxytocin-y { - syntax: ""; - inherits: true; - initial-value: 61%; -} -@property --jin-metabolism-oxytocin-radius { - syntax: ""; - inherits: true; - initial-value: 42%; -} -@property --jin-metabolism-norepinephrine-x { - syntax: ""; - inherits: true; - initial-value: 54%; -} -@property --jin-metabolism-norepinephrine-y { - syntax: ""; - inherits: true; - initial-value: 86%; -} -@property --jin-metabolism-norepinephrine-radius { - syntax: ""; - inherits: true; - initial-value: 43%; -} -@property --jin-metabolism-cortisol-x { - syntax: ""; - inherits: true; - initial-value: 14%; -} -@property --jin-metabolism-cortisol-y { - syntax: ""; - inherits: true; - initial-value: 68%; -} -@property --jin-metabolism-cortisol-radius { - syntax: ""; - inherits: true; - initial-value: 42%; -} - #memory-drag-handle { position: relative; isolation: isolate; @@ -139,22 +61,6 @@ place-items: center; flex: 0 0 auto; user-select: none; - transition: - --jin-metabolism-dopamine-x var(--jin-metabolism-dopamine-drift-duration, 2400s) linear, - --jin-metabolism-dopamine-y var(--jin-metabolism-dopamine-drift-duration, 2400s) linear, - --jin-metabolism-serotonin-x var(--jin-metabolism-serotonin-drift-duration, 2400s) linear, - --jin-metabolism-serotonin-y var(--jin-metabolism-serotonin-drift-duration, 2400s) linear, - --jin-metabolism-oxytocin-x var(--jin-metabolism-oxytocin-drift-duration, 2400s) linear, - --jin-metabolism-oxytocin-y var(--jin-metabolism-oxytocin-drift-duration, 2400s) linear, - --jin-metabolism-norepinephrine-x var(--jin-metabolism-norepinephrine-drift-duration, 2400s) linear, - --jin-metabolism-norepinephrine-y var(--jin-metabolism-norepinephrine-drift-duration, 2400s) linear, - --jin-metabolism-cortisol-x var(--jin-metabolism-cortisol-drift-duration, 2400s) linear, - --jin-metabolism-cortisol-y var(--jin-metabolism-cortisol-drift-duration, 2400s) linear, - --jin-metabolism-dopamine-radius 14s cubic-bezier(0.22, 0.61, 0.36, 1) 650ms, - --jin-metabolism-serotonin-radius 14s cubic-bezier(0.22, 0.61, 0.36, 1) 650ms, - --jin-metabolism-oxytocin-radius 14s cubic-bezier(0.22, 0.61, 0.36, 1) 650ms, - --jin-metabolism-norepinephrine-radius 14s cubic-bezier(0.22, 0.61, 0.36, 1) 650ms, - --jin-metabolism-cortisol-radius 14s cubic-bezier(0.22, 0.61, 0.36, 1) 650ms; } .jin-runtime-avatar-shell::before { @@ -168,9 +74,7 @@ inset: -13%; z-index: 0; pointer-events: none; - /* The metabolic aura is a field, not a hard circular layer. Keep the - colour cloud soft all the way to its edge so no visible arc appears - over the avatar panel when an off-centre channel gets stronger. */ + /* Keep the avatar-colour aura soft all the way to its edge. */ border-radius: 0; -webkit-mask-image: radial-gradient( ellipse at center, @@ -191,33 +95,7 @@ opacity: var(--jin-avatar-aura-opacity-low); transform: scale(var(--jin-avatar-aura-scale-low)); transform-origin: center; - background: - radial-gradient( - circle at var(--jin-metabolism-dopamine-x, 22%) var(--jin-metabolism-dopamine-y, 24%), - color-mix(in srgb, #fbbf24 var(--jin-metabolism-dopamine-aura, 11.20%), transparent) 0%, - transparent var(--jin-metabolism-dopamine-radius, 42%) - ), - radial-gradient( - circle at var(--jin-metabolism-serotonin-x, 78%) var(--jin-metabolism-serotonin-y, 20%), - color-mix(in srgb, #60a5fa var(--jin-metabolism-serotonin-aura, 12.80%), transparent) 0%, - transparent var(--jin-metabolism-serotonin-radius, 43%) - ), - radial-gradient( - circle at var(--jin-metabolism-oxytocin-x, 87%) var(--jin-metabolism-oxytocin-y, 61%), - color-mix(in srgb, #f472b6 var(--jin-metabolism-oxytocin-aura, 11.60%), transparent) 0%, - transparent var(--jin-metabolism-oxytocin-radius, 42%) - ), - radial-gradient( - circle at var(--jin-metabolism-norepinephrine-x, 54%) var(--jin-metabolism-norepinephrine-y, 86%), - color-mix(in srgb, #f97316 var(--jin-metabolism-norepinephrine-aura, 10.80%), transparent) 0%, - transparent var(--jin-metabolism-norepinephrine-radius, 43%) - ), - radial-gradient( - circle at var(--jin-metabolism-cortisol-x, 14%) var(--jin-metabolism-cortisol-y, 68%), - color-mix(in srgb, #34d399 var(--jin-metabolism-cortisol-aura, 9.40%), transparent) 0%, - transparent var(--jin-metabolism-cortisol-radius, 42%) - ), - radial-gradient( + background: radial-gradient( circle at 50% 50%, color-mix(in srgb, var(--jin-color, #1f4f8f) 22%, transparent) 0%, color-mix(in srgb, var(--jin-color, #1f4f8f) 12%, transparent) 34%, @@ -261,8 +139,7 @@ z-index: 2; display: block; pointer-events: none; - opacity: var(--jin-avatar-metabolism-opacity, 0.94); - transition: opacity 58s cubic-bezier(0.22, 0.61, 0.36, 1); + opacity: 0.94; } .jin-runtime-avatar svg { @@ -270,9 +147,8 @@ width: 100%; height: 100%; overflow: visible; - transform: scale(var(--jin-avatar-metabolism-body-scale, 0.90)); + transform: scale(0.90); transform-origin: center; - transition: transform 58s cubic-bezier(0.22, 0.61, 0.36, 1); } .jin-runtime-avatar-core-button { diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index ea512e7a..12685856 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -1432,23 +1432,3 @@ button.jin-context-l4-fact-id.is-linked:focus-visible { gap: 4px; } } - -/* Metabolism logger card: keep the existing neutral logger plaque and lay all - five live channels over it as very soft, independently weighted tints. */ -.jin-metabolism-card { - background-color: rgba(63, 63, 70, 0.10) !important; - background-image: - radial-gradient(circle at 8% 18%, color-mix(in srgb, #fbbf24 var(--jin-metabolism-dopamine, 8.62%), transparent) 0%, transparent 48%), - radial-gradient(circle at 27% 84%, color-mix(in srgb, #60a5fa var(--jin-metabolism-serotonin, 10.38%), transparent) 0%, transparent 52%), - radial-gradient(circle at 51% 20%, color-mix(in srgb, #f472b6 var(--jin-metabolism-oxytocin, 9.06%), transparent) 0%, transparent 50%), - radial-gradient(circle at 74% 82%, color-mix(in srgb, #f97316 var(--jin-metabolism-norepinephrine, 8.18%), transparent) 0%, transparent 52%), - radial-gradient(circle at 94% 24%, color-mix(in srgb, #34d399 var(--jin-metabolism-cortisol, 6.64%), transparent) 0%, transparent 48%) !important; - border-color: rgba(113, 113, 122, 0.20) !important; - transition: - background-color 420ms ease, - border-color 420ms ease; -} - -.jin-metabolism-card .logger-tag { - color: rgb(212 212 216) !important; -} diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 5652f115..ea673ea9 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -1247,223 +1247,6 @@ function setL4LoggerButtonVisible( ); } -const metabolismCards = []; - -function normalizeMetabolismLoggerLevels(value) { - const metabolism = - window.JinRuntime - && window.JinRuntime.metabolism; - - if (metabolism && typeof metabolism.normalize === "function") { - return metabolism.normalize(value); - } - - return { - dopamine: 0.42, - serotonin: 0.58, - oxytocin: 0.46, - norepinephrine: 0.38, - cortisol: 0.24, - }; -} - -function applyMetabolismLoggerTint(logDiv, levels) { - if (!logDiv) { - return; - } - - const normalized = - normalizeMetabolismLoggerLevels(levels); - - Object.entries(normalized).forEach(([channel, value]) => { - const strength = - 4 + Math.max(0, Math.min(1, Number(value) || 0)) * 11; - - logDiv.style.setProperty( - `--jin-metabolism-${channel}`, - `${strength.toFixed(2)}%` - ); - }); -} - -function resolveMetabolismLoggerEvent(meta) { - if (String(meta && meta.channel || "").toLowerCase() !== "metabolism") { - return ""; - } - - const event = - String(meta && meta.metabolism_event || "").toLowerCase(); - - return event === "request" || event === "result" - ? event - : ""; -} - -function createMetabolismLoggerCard( - requestDetails = null, - levels = null, - requestId = "", -) { - const logDiv = - createL4LoggerCard( - "[METABOLISM]" - ); - - logDiv.classList.add( - "jin-metabolism-card" - ); - logDiv.dataset.logKind = - "metabolism"; - logDiv.dataset.metabolismRequestId = - String(requestId || "").trim(); - - applyMetabolismLoggerTint( - logDiv, - levels - ); - - const actions = - document.createElement("div"); - - actions.className = - "mt-2 flex flex-wrap items-center gap-2"; - - const requestButton = - createL4LoggerButton( - "request", - "muted" - ); - const responseButton = - createL4LoggerButton( - "response", - "muted" - ); - - setL4LoggerButtonVisible( - requestButton, - Boolean(requestDetails) - ); - setL4LoggerButtonVisible( - responseButton, - false - ); - - const state = { - logDiv, - requestId: String(requestId || "").trim(), - requestDetails, - responseDetails: null, - responseSettled: false, - requestButton, - responseButton, - }; - - requestButton.addEventListener( - "click", - function () { - if (!state.requestDetails) { - return; - } - - showTrace( - state.requestDetails, - "Metabolism request" - ); - } - ); - - responseButton.addEventListener( - "click", - function () { - if (!state.responseSettled || !state.responseDetails) { - return; - } - - showTrace( - state.responseDetails, - "Metabolism response" - ); - } - ); - - actions.appendChild( - requestButton - ); - actions.appendChild( - responseButton - ); - logDiv.appendChild( - actions - ); - - metabolismCards.push( - state - ); - - return state; -} - -function handleMetabolismLoggerLog( - details, - meta, -) { - const event = - resolveMetabolismLoggerEvent(meta); - - if (!event) { - return null; - } - - const levels = - meta && meta.metabolism_levels; - const requestId = - String(meta && meta.metabolism_request_id || "").trim(); - - if (event === "request") { - return createMetabolismLoggerCard( - details, - levels, - requestId - ).logDiv; - } - - let state = - [...metabolismCards] - .reverse() - .find(candidate => ( - !candidate.responseSettled - && ( - !requestId - || candidate.requestId === requestId - ) - )); - - if (!state) { - state = - createMetabolismLoggerCard( - null, - levels, - requestId - ); - } - - state.responseSettled = true; - state.responseDetails = - String(details || ""); - - applyMetabolismLoggerTint( - state.logDiv, - levels - ); - - setL4LoggerButtonVisible( - state.responseButton, - Boolean(state.responseDetails.trim()) - ); - - return state.logDiv; -} - function resolveL4SummarizerPhase( message, meta, @@ -2836,16 +2619,6 @@ function appendLog( details, ); - const metabolismLog = - handleMetabolismLoggerLog( - normalized.details, - meta - ); - - if (metabolismLog) { - return metabolismLog; - } - settleL4SummarizerCardForTerminalEvent( normalized.message, meta diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index faddd1fd..26186778 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -781,205 +781,6 @@ function appendL4ResponseGroup( ); } -function renderMetabolismTrace(parsed) { - const kind = - String(parsed && parsed.kind || ""); - - if (kind === "metabolism_request") { - appendTraceModalBody( - traceModalContent, - "System instruction", - parsed.system_prompt || "" - ); - - const snapshot = - parsed.snapshot - && typeof parsed.snapshot === "object" - ? parsed.snapshot - : {}; - - const meta = - document.createElement("section"); - - meta.className = - "delayed-memory-modal-fields"; - - appendTraceModalField( - meta, - "Model", - parsed.model || "" - ); - appendTraceModalField( - meta, - "Request", - parsed.request_id || "" - ); - appendTraceModalField( - meta, - "Turn", - snapshot.runtime_state - && snapshot.runtime_state.turn_number - ); - appendTraceModalField( - meta, - "L4 facts", - snapshot.l4_facts_total - ); - appendTraceModalField( - meta, - "Actions", - Array.isArray(snapshot.recent_runtime_actions) - ? snapshot.recent_runtime_actions.length - : 0 - ); - - if (meta.childElementCount) { - traceModalContent.appendChild( - meta - ); - } - - appendTraceModalBody( - traceModalContent, - "Previous chemistry", - JSON.stringify( - snapshot.previous_levels || {}, - null, - 2 - ) - ); - appendTraceModalBody( - traceModalContent, - "Baseline", - JSON.stringify( - snapshot.baseline || {}, - null, - 2 - ) - ); - appendTraceModalBody( - traceModalContent, - "Runtime state", - JSON.stringify( - snapshot.runtime_state || {}, - null, - 2 - ) - ); - if (snapshot.snapshot_compaction) { - appendTraceModalBody( - traceModalContent, - "Snapshot budget", - JSON.stringify( - snapshot.snapshot_compaction, - null, - 2 - ) - ); - } - appendTraceModalBody( - traceModalContent, - "Runtime actions", - JSON.stringify( - snapshot.recent_runtime_actions || [], - null, - 2 - ) - ); - appendTraceModalBody( - traceModalContent, - "Recent interactions + reasoning", - JSON.stringify( - snapshot.recent_interactions || [], - null, - 2 - ) - ); - appendTraceModalBody( - traceModalContent, - "L4 index", - JSON.stringify( - snapshot.l4_index || [], - null, - 2 - ) - ); - return; - } - - const fields = - document.createElement("section"); - - fields.className = - "delayed-memory-modal-fields"; - - appendTraceModalField( - fields, - "Status", - parsed.status || (parsed.ok === false ? "failed" : "applied") - ); - appendTraceModalField( - fields, - "Request", - parsed.request_id || "" - ); - - const levels = - parsed.levels - && typeof parsed.levels === "object" - ? parsed.levels - : {}; - - [ - ["DOP", levels.dopamine], - ["SER", levels.serotonin], - ["OXY", levels.oxytocin], - ["NOR", levels.norepinephrine], - ["COR", levels.cortisol], - ].forEach(([label, value]) => { - appendTraceModalField( - fields, - label, - Number.isFinite(Number(value)) - ? Number(value).toFixed(3) - : "" - ); - }); - - if (fields.childElementCount) { - traceModalContent.appendChild( - fields - ); - } - - if (parsed.target_levels) { - appendTraceModalBody( - traceModalContent, - "Service target", - JSON.stringify(parsed.target_levels, null, 2) - ); - } - - if (parsed.delta) { - appendTraceModalBody( - traceModalContent, - "Applied delta", - JSON.stringify(parsed.delta, null, 2) - ); - } - - appendTraceModalBody( - traceModalContent, - "Raw service response", - parsed.raw || "" - ); - appendTraceModalBody( - traceModalContent, - "Error", - parsed.error || "" - ); -} - function renderL4SummarizerResponseTrace(parsed) { if (parsed.no_changes) { const empty = @@ -3303,20 +3104,6 @@ function renderTraceDetails( const parsed = parseTraceJson(details); - if ( - parsed - && ( - parsed.kind === "metabolism_request" - || parsed.kind === "metabolism_response" - ) - ) { - renderMetabolismTrace( - parsed - ); - - return; - } - if ( parsed && parsed.kind === "l4_fact" @@ -3449,20 +3236,6 @@ function getTraceTitle( const parsed = parseTraceJson(details); - if ( - parsed - && parsed.kind === "metabolism_request" - ) { - return "Metabolism request"; - } - - if ( - parsed - && parsed.kind === "metabolism_response" - ) { - return "Metabolism response"; - } - if ( parsed && parsed.kind === "summarizer_response" diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 20571d5e..f4fccc67 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -125,13 +125,6 @@ typeof memoryReferenceHelpers.collectMetadataAliases === "function" ? memoryReferenceHelpers.collectMetadataAliases : () => []; - const metabolismApi = - window.JinRuntime.metabolism || null; - const METABOLISM_UPDATE_EVENT = - metabolismApi && metabolismApi.UPDATE_EVENT - ? metabolismApi.UPDATE_EVENT - : "jin:metabolism-update"; - if (!avatarRoot) { return; } @@ -187,70 +180,6 @@ return Math.max(min, Math.min(max, Number(value) || 0)); } - function getMetabolismChannel() { - // Metabolism is a single ambient layer over memory, not a semantic label - // assigned to individual records. Keep one marker only so already-rendered - // SVG nodes can be refreshed when the five-channel state changes. - return metabolismApi ? "ambient" : ""; - } - - function getMetabolismTintedColor(baseColor, channel) { - if ( - metabolismApi - && typeof metabolismApi.tintColor === "function" - && channel - ) { - return metabolismApi.tintColor( - baseColor - ); - } - - return baseColor; - } - - function getActiveMemoryMetabolicOpacity(activeMemoryId) { - if ( - !metabolismApi - || typeof metabolismApi.getActiveMemorySalience !== "function" - ) { - return 0.76; - } - - const salience = metabolismApi.getActiveMemorySalience( - activeMemoryId - ); - - if (!Number.isFinite(Number(salience))) { - return 0.76; - } - - // Salience only breathes inside the established opacity language. A - // reasoning citation/hover still owns the strong highlight on top. - return clamp( - 0.70 + Number(salience) * 0.16, - 0.70, - 0.86 - ); - } - - function refreshActiveMemoryMetabolicOpacity() { - avatarRoot.querySelectorAll( - ".jin-avatar-memory-dash-active[data-active-memory-id]" - ).forEach((dashGroup) => { - const opacity = getActiveMemoryMetabolicOpacity( - dashGroup.dataset.activeMemoryId - ); - const path = dashGroup.querySelector("path"); - - if (path) { - path.setAttribute( - "stroke-opacity", - opacity.toFixed(3) - ); - } - }); - } - function degreesFromArcPixels(pixels, radius) { const normalizedPixels = Number(pixels || 0); @@ -2253,114 +2182,6 @@ ); } - function refreshRenderedMetabolismTints() { - const channel = getMetabolismChannel(); - - if (!channel) { - return; - } - - const overallColor = - avatarRoot.style - .getPropertyValue("--jin-avatar-overall-color") - .trim() || DEFAULT_RING_COLOR; - - Object.entries(MEMORY_RING_LAYOUT) - .forEach(([kind, layout]) => { - const ring = - avatarRoot.querySelector( - `.jin-avatar-memory-ring-${kind}` - ); - - if (!ring) { - return; - } - - const colors = - getMemorySignalColors(kind, overallColor); - const renderedColor = - getMetabolismTintedColor( - colors.color, - channel - ); - const renderedGlowColor = - getMetabolismTintedColor( - colors.glowColor, - channel - ); - - ring.querySelectorAll( - ".jin-avatar-memory-dash > path" - ).forEach((path) => { - path.setAttribute( - "stroke", - renderedColor - ); - }); - ring.querySelectorAll( - ".jin-avatar-memory-dot" - ).forEach((dot) => { - dot.setAttribute( - "fill", - renderedColor - ); - }); - - setMemoryDashGlowVariables( - ring, - renderedGlowColor, - layout.strokeWidth + 0.75, - kind === "l4" - ? getMemoryDotRadius(layout) - : 0 - ); - }); - - avatarRoot.querySelectorAll( - ".jin-avatar-file-dot" - ).forEach((dotGroup) => { - const active = Boolean( - dotGroup.classList.contains("is-memory-pinned") - || dotGroup.classList.contains("is-context-loaded") - ); - const baseColor = - active - ? FILE_RING_ACTIVE_COLOR - : FILE_RING_COLOR; - const glowColor = - active - ? FILE_RING_ACTIVE_COLOR - : FILE_RING_LAYOUT.glowColor; - - const renderedColor = - getMetabolismTintedColor( - baseColor, - channel - ); - const renderedGlowColor = - getMetabolismTintedColor( - glowColor, - channel - ); - const core = - dotGroup.querySelector( - ".jin-avatar-file-dot-core" - ); - - if (core) { - core.setAttribute( - "fill", - renderedColor - ); - } - - setFileDotGlowVariables( - dotGroup, - renderedGlowColor - ); - }); - } - function getMemoryRingAnimation(records, kind) { const seedText = records .map(record => ( @@ -2445,17 +2266,6 @@ ring.appendChild(reasoningMotion.layer); const ringColors = getMemorySignalColors(kind, overallColor); - const metabolismChannel = getMetabolismChannel(); - const renderedColor = - getMetabolismTintedColor( - ringColors.color, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - ringColors.glowColor, - metabolismChannel - ); const dotRadius = kind === "l4" ? getMemoryDotRadius(layout) @@ -2463,7 +2273,7 @@ setMemoryDashGlowVariables( ring, - renderedGlowColor, + ringColors.glowColor, layout.strokeWidth + 0.75, dotRadius ); @@ -2495,8 +2305,8 @@ kind, angle, arcDegrees, - color: renderedColor, - opacity: getActiveMemoryMetabolicOpacity(record.id), + color: ringColors.color, + opacity: 0.76, avatarMemoryHoverId: record.avatarMemoryHoverId, activeMemoryId: record.id, citationKey: @@ -2515,7 +2325,7 @@ kind, angle, arcDegrees, - color: renderedColor, + color: ringColors.color, opacity: record.archived ? 0.26 : 0.52, archived: record.archived, dot: record.archived, @@ -2543,7 +2353,7 @@ kind, angle, arcDegrees, - color: renderedColor, + color: ringColors.color, opacity: active ? 0.82 : 0.36, pinned, contextLoaded, @@ -2604,18 +2414,6 @@ const glowColor = record.pinned ? FILE_RING_ACTIVE_COLOR : FILE_RING_LAYOUT.glowColor; - const metabolismChannel = - getMetabolismChannel(); - const renderedColor = - getMetabolismTintedColor( - color, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - glowColor, - metabolismChannel - ); const dotGroup = createSvgElement("g", { class: "jin-avatar-file-dot", "data-avatar-memory-hover-id": record.avatarMemoryHoverId || null, @@ -2656,14 +2454,14 @@ ); setFileDotGlowVariables( dotGroup, - renderedGlowColor + glowColor ); dotGroup.appendChild(createSvgElement("circle", { class: "jin-avatar-file-dot-core", cx: point.x.toFixed(3), cy: point.y.toFixed(3), r: FILE_RING_LAYOUT.dotRadius, - fill: renderedColor, + fill: color, "fill-opacity": opacity.toFixed(2), })); reasoningMotion.content.appendChild(dotGroup); @@ -2831,28 +2629,16 @@ "is-memory-pinned", nextPinned ); - const metabolismChannel = getMetabolismChannel(); - const renderedColor = - getMetabolismTintedColor( - colors.color, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - colors.glowColor, - metabolismChannel - ); - setMemoryDashGlowVariables( dashGroup.closest(".jin-avatar-memory-ring-delayed") || dashGroup, - renderedGlowColor, + colors.glowColor, MEMORY_RING_LAYOUT.delayed.strokeWidth + 0.75 ); if (path) { path.setAttribute( "stroke", - renderedColor + colors.color ); path.setAttribute( "stroke-opacity", @@ -2931,21 +2717,9 @@ nextArchived ); - const metabolismChannel = getMetabolismChannel(); - const renderedColor = - getMetabolismTintedColor( - colors.color, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - colors.glowColor, - metabolismChannel - ); - setMemoryDashGlowVariables( dashGroup.closest(".jin-avatar-memory-ring-l4") || dashGroup, - renderedGlowColor, + colors.glowColor, MEMORY_RING_LAYOUT.l4.strokeWidth + 0.75, dotRadius ); @@ -2956,7 +2730,7 @@ ); path.setAttribute( "stroke", - renderedColor + colors.color ); path.setAttribute( "stroke-opacity", @@ -2987,7 +2761,7 @@ dot.setAttribute("cx", dotPoint.x.toFixed(3)); dot.setAttribute("cy", dotPoint.y.toFixed(3)); dot.setAttribute("r", dotRadius.toFixed(2)); - dot.setAttribute("fill", renderedColor); + dot.setAttribute("fill", colors.color); dot.setAttribute("fill-opacity", nextOpacity.toFixed(2)); return true; @@ -3248,27 +3022,15 @@ active ? FILE_RING_ACTIVE_COLOR : FILE_RING_LAYOUT.glowColor; - const metabolismChannel = getMetabolismChannel(); - const renderedColor = - getMetabolismTintedColor( - baseColor, - metabolismChannel - ); - const renderedGlowColor = - getMetabolismTintedColor( - glowColor, - metabolismChannel - ); - setFileDotGlowVariables( dotGroup, - renderedGlowColor + glowColor ); if (core) { core.setAttribute( "fill", - renderedColor + baseColor ); core.setAttribute( "fill-opacity", @@ -5248,14 +5010,6 @@ scheduleSnapshotRender(snapshot, snapshotIndex); }); - window.addEventListener( - METABOLISM_UPDATE_EVENT, - function () { - refreshRenderedMetabolismTints(); - refreshActiveMemoryMetabolicOpacity(); - } - ); - window.addEventListener( MEMORY_REFERENCE_HIGHLIGHT_EVENT, handleMemoryReferenceHighlight diff --git a/ui/static/js/runtime/runtime-metabolism.js b/ui/static/js/runtime/runtime-metabolism.js deleted file mode 100644 index ca8a271b..00000000 --- a/ui/static/js/runtime/runtime-metabolism.js +++ /dev/null @@ -1,600 +0,0 @@ -(function () { - "use strict"; - - window.JinRuntime = window.JinRuntime || {}; - - const UPDATE_EVENT = "jin:metabolism-update"; - const CHANNELS = Object.freeze([ - "dopamine", - "serotonin", - "oxytocin", - "norepinephrine", - "cortisol", - ]); - const PALETTE = Object.freeze({ - dopamine: "#fbbf24", - serotonin: "#60a5fa", - oxytocin: "#f472b6", - norepinephrine: "#f97316", - cortisol: "#34d399", - }); - const DEFAULTS = Object.freeze({ - dopamine: 0.42, - serotonin: 0.58, - oxytocin: 0.46, - norepinephrine: 0.38, - cortisol: 0.24, - }); - const DEFAULT_HALF_LIVES_SECONDS = Object.freeze({ - dopamine: 45 * 60, - serotonin: 150 * 60, - oxytocin: 120 * 60, - norepinephrine: 32 * 60, - cortisol: 55 * 60, - }); - - // One-switch rollback for the only new avatar motion. Tint/aura and memory - // salience remain independent, so this can be disabled without touching the - // causal metabolism layer. - const AMBIENT_BODY_MOTION_ENABLED = true; - - // Keep the existing five metabolic colour clouds exactly as they are - // visually; only their centre and falloff radius are allowed to move. - // Drift is intentionally glacial: even a maxed channel moves by less than - // one CSS pixel per minute on the normal avatar size. - const METABOLIC_SPOT_DRIFT_ENABLED = true; - const SPOT_GEOMETRY = Object.freeze({ - dopamine: Object.freeze({ x: 22, y: 24, radius: 42 }), - serotonin: Object.freeze({ x: 78, y: 20, radius: 43 }), - oxytocin: Object.freeze({ x: 87, y: 61, radius: 42 }), - norepinephrine: Object.freeze({ x: 54, y: 86, radius: 43 }), - cortisol: Object.freeze({ x: 14, y: 68, radius: 42 }), - }); - const SPOT_RADIUS_RANGE = 0.22; - const SPOT_MIN_CENTER_PERCENT = 10; - const SPOT_MAX_CENTER_PERCENT = 90; - const SPOT_DRIFT_STEP_MIN_PERCENT = 4.0; - const SPOT_DRIFT_STEP_MAX_PERCENT = 9.0; - const SPOT_DRIFT_SPEED_MIN_PERCENT_PER_MINUTE = 0.07; - const SPOT_DRIFT_SPEED_MAX_PERCENT_PER_MINUTE = 0.24; - const SPOT_DRIFT_START_MIN_MS = 8000; - const SPOT_DRIFT_START_MAX_MS = 24000; - const SPOT_DRIFT_RETARGET_MIN_MS = 1800; - const SPOT_DRIFT_RETARGET_MAX_MS = 4500; - const SPOT_DRIFT_PAUSE_MIN_MS = 12000; - const SPOT_DRIFT_PAUSE_MAX_MS = 42000; - const IDLE_HOMEOSTASIS_TICK_MS = 30000; - - let state = { ...DEFAULTS }; - let authoritativeState = { ...DEFAULTS }; - let authoritativeAtMs = Date.now(); - let halfLivesSeconds = { ...DEFAULT_HALF_LIVES_SECONDS }; - let activeMemorySalience = {}; - const spotPositions = Object.fromEntries( - CHANNELS.map(channel => [ - channel, - { - x: SPOT_GEOMETRY[channel].x, - y: SPOT_GEOMETRY[channel].y, - }, - ]) - ); - const spotDriftTimers = new Map(); - - function clamp(value, min, max) { - const number = Number(value); - const safe = Number.isFinite(number) ? number : 0; - return Math.max(min, Math.min(max, safe)); - } - - function normalize(value) { - const source = - value && typeof value === "object" && !Array.isArray(value) - ? value - : {}; - - return CHANNELS.reduce((result, channel) => { - const fallback = DEFAULTS[channel]; - const raw = Number(source[channel]); - result[channel] = clamp( - Number.isFinite(raw) ? raw : fallback, - 0, - 1 - ); - return result; - }, {}); - } - - function normalizeSalience(value) { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return {}; - } - - return Object.entries(value).reduce((result, [key, raw]) => { - const id = String(key || "").trim().toLowerCase(); - const score = Number(raw); - if (id && Number.isFinite(score)) { - result[id] = clamp(score, 0, 1); - } - return result; - }, {}); - } - - function normalizeHalfLives(value) { - const source = - value && typeof value === "object" && !Array.isArray(value) - ? value - : {}; - - return CHANNELS.reduce((result, channel) => { - const raw = Number(source[channel]); - result[channel] = - Number.isFinite(raw) && raw > 0 - ? raw - : DEFAULT_HALF_LIVES_SECONDS[channel]; - return result; - }, {}); - } - - function hexToRgb(color) { - const normalized = String(color || "") - .replace("#", "") - .trim(); - - if (!/^[0-9a-f]{6}$/i.test(normalized)) { - return null; - } - - return { - r: parseInt(normalized.slice(0, 2), 16), - g: parseInt(normalized.slice(2, 4), 16), - b: parseInt(normalized.slice(4, 6), 16), - }; - } - - function rgbToHex(rgb) { - const part = value => clamp(Math.round(value), 0, 255) - .toString(16) - .padStart(2, "0"); - - return `#${part(rgb.r)}${part(rgb.g)}${part(rgb.b)}`; - } - - function mixHex(baseColor, tintColor, amount) { - const base = hexToRgb(baseColor); - const tint = hexToRgb(tintColor); - - if (!base || !tint) { - return String(baseColor || tintColor || "#ffffff"); - } - - const weight = clamp(amount, 0, 1); - - return rgbToHex({ - r: base.r + (tint.r - base.r) * weight, - g: base.g + (tint.g - base.g) * weight, - b: base.b + (tint.b - base.b) * weight, - }); - } - - function getDominantChannel() { - return CHANNELS.reduce((winner, channel) => { - return state[channel] > state[winner] - ? channel - : winner; - }, CHANNELS[0]); - } - - function getBlendedChemistryColor() { - let total = 0; - const blended = { r: 0, g: 0, b: 0 }; - - CHANNELS.forEach(channel => { - const rgb = hexToRgb(PALETTE[channel]); - if (!rgb) { - return; - } - - // Every channel is always present, while elevated channels pull the - // blend slightly harder. This keeps the tint alive without turning the - // avatar into a mood lamp. - const weight = 0.16 + clamp(state[channel], 0, 1) * 0.84; - total += weight; - blended.r += rgb.r * weight; - blended.g += rgb.g * weight; - blended.b += rgb.b * weight; - }); - - if (!total) { - return PALETTE[getDominantChannel()]; - } - - return rgbToHex({ - r: blended.r / total, - g: blended.g / total, - b: blended.b / total, - }); - } - - function tintColor(baseColor) { - const averageDistance = CHANNELS.reduce((sum, channel) => { - return sum + Math.abs(state[channel] - DEFAULTS[channel]); - }, 0) / CHANNELS.length; - - const amount = clamp(0.032 + averageDistance * 0.16, 0.032, 0.085); - - return mixHex( - baseColor, - getBlendedChemistryColor(), - amount - ); - } - - function loggerTintPercent(channel) { - const level = clamp(state[channel], 0, 1); - return 4 + level * 11; - } - - function auraTintPercent(channel) { - const level = clamp(state[channel], 0, 1); - return 7 + level * 10; - } - - function getSpotRadiusPercent(channel, level = state[channel]) { - const geometry = SPOT_GEOMETRY[channel]; - if (!geometry) { - return 42; - } - - const current = clamp(level, 0, 1); - const baseline = clamp(DEFAULTS[channel], 0, 1); - let scale = 1; - - if (current >= baseline) { - const room = Math.max(0.0001, 1 - baseline); - scale += ((current - baseline) / room) * SPOT_RADIUS_RANGE; - } else { - const room = Math.max(0.0001, baseline); - scale -= ((baseline - current) / room) * SPOT_RADIUS_RANGE; - } - - return geometry.radius * scale; - } - - function randomBetween(min, max) { - return min + Math.random() * Math.max(0, max - min); - } - - function getSpotDriftSpeed(channel) { - const level = clamp(state[channel], 0, 1); - return ( - SPOT_DRIFT_SPEED_MIN_PERCENT_PER_MINUTE - + ( - SPOT_DRIFT_SPEED_MAX_PERCENT_PER_MINUTE - - SPOT_DRIFT_SPEED_MIN_PERCENT_PER_MINUTE - ) * level - ); - } - - function getAvatarShell() { - return document.querySelector(".jin-runtime-avatar-shell"); - } - - function applySpotSizeVariables() { - const shell = getAvatarShell(); - if (!shell) { - return; - } - - CHANNELS.forEach(channel => { - shell.style.setProperty( - `--jin-metabolism-${channel}-radius`, - `${getSpotRadiusPercent(channel).toFixed(3)}%` - ); - }); - } - - function getRenderedSpotPosition(shell, channel) { - const fallback = spotPositions[channel]; - if (!shell || !fallback || typeof window.getComputedStyle !== "function") { - return fallback; - } - - const styles = window.getComputedStyle(shell); - const x = parseFloat( - styles.getPropertyValue(`--jin-metabolism-${channel}-x`) - ); - const y = parseFloat( - styles.getPropertyValue(`--jin-metabolism-${channel}-y`) - ); - - return { - x: Number.isFinite(x) ? x : fallback.x, - y: Number.isFinite(y) ? y : fallback.y, - }; - } - - function scheduleSpotDrift(channel, delayMs) { - if (!METABOLIC_SPOT_DRIFT_ENABLED || !SPOT_GEOMETRY[channel]) { - return; - } - - const previousTimer = spotDriftTimers.get(channel); - if (previousTimer) { - window.clearTimeout(previousTimer); - } - - const timer = window.setTimeout(() => { - const shell = getAvatarShell(); - if (!shell) { - scheduleSpotDrift(channel, 1000); - return; - } - - const current = getRenderedSpotPosition(shell, channel); - const angle = randomBetween(0, Math.PI * 2); - const step = randomBetween( - SPOT_DRIFT_STEP_MIN_PERCENT, - SPOT_DRIFT_STEP_MAX_PERCENT - ); - const next = { - x: clamp( - current.x + Math.cos(angle) * step, - SPOT_MIN_CENTER_PERCENT, - SPOT_MAX_CENTER_PERCENT - ), - y: clamp( - current.y + Math.sin(angle) * step, - SPOT_MIN_CENTER_PERCENT, - SPOT_MAX_CENTER_PERCENT - ), - }; - const distance = Math.hypot( - next.x - current.x, - next.y - current.y - ); - const speed = Math.max( - 0.001, - getSpotDriftSpeed(channel) - ); - const durationMs = Math.max( - 60 * 1000, - (distance / speed) * 60 * 1000 - ); - - shell.style.setProperty( - `--jin-metabolism-${channel}-drift-duration`, - `${Math.round(durationMs)}ms` - ); - shell.style.setProperty( - `--jin-metabolism-${channel}-x`, - `${next.x.toFixed(3)}%` - ); - shell.style.setProperty( - `--jin-metabolism-${channel}-y`, - `${next.y.toFixed(3)}%` - ); - spotPositions[channel] = next; - - scheduleSpotDrift( - channel, - durationMs + randomBetween( - SPOT_DRIFT_PAUSE_MIN_MS, - SPOT_DRIFT_PAUSE_MAX_MS - ) - ); - }, Math.max(0, Number(delayMs) || 0)); - - spotDriftTimers.set(channel, timer); - } - - function startSpotDrift() { - if (!METABOLIC_SPOT_DRIFT_ENABLED) { - return; - } - - CHANNELS.forEach(channel => { - scheduleSpotDrift( - channel, - randomBetween( - SPOT_DRIFT_START_MIN_MS, - SPOT_DRIFT_START_MAX_MS - ) - ); - }); - } - - function retargetSpotDriftForCurrentState() { - if (!METABOLIC_SPOT_DRIFT_ENABLED) { - return; - } - - CHANNELS.forEach(channel => { - scheduleSpotDrift( - channel, - randomBetween( - SPOT_DRIFT_RETARGET_MIN_MS, - SPOT_DRIFT_RETARGET_MAX_MS - ) - ); - }); - } - - function getAmbientBodyScale() { - if (!AMBIENT_BODY_MOTION_ENABLED) { - return 0.90; - } - - const factor = clamp( - 1 - + (state.oxytocin - DEFAULTS.oxytocin) * 0.035 - + (state.dopamine - DEFAULTS.dopamine) * 0.012 - - (state.cortisol - DEFAULTS.cortisol) * 0.014, - 0.985, - 1.018 - ); - - return 0.90 * factor; - } - - function getAmbientAvatarOpacity() { - if (!AMBIENT_BODY_MOTION_ENABLED) { - return 0.94; - } - - return clamp( - 0.94 - + (state.serotonin - DEFAULTS.serotonin) * 0.026 - - (state.cortisol - DEFAULTS.cortisol) * 0.018, - 0.920, - 0.956 - ); - } - - function applyCssVariables(options = {}) { - const root = document.documentElement; - const updateSpotSizes = options.updateSpotSizes === true; - - CHANNELS.forEach(channel => { - root.style.setProperty( - `--jin-metabolism-${channel}`, - `${loggerTintPercent(channel).toFixed(2)}%` - ); - root.style.setProperty( - `--jin-metabolism-${channel}-aura`, - `${auraTintPercent(channel).toFixed(2)}%` - ); - }); - - root.style.setProperty( - "--jin-avatar-metabolism-body-scale", - getAmbientBodyScale().toFixed(5) - ); - root.style.setProperty( - "--jin-avatar-metabolism-opacity", - getAmbientAvatarOpacity().toFixed(4) - ); - - if (updateSpotSizes) { - applySpotSizeVariables(); - } - } - - function getState() { - return { ...state }; - } - - function getActiveMemorySalience(activeMemoryId) { - const id = String(activeMemoryId || "").trim().toLowerCase(); - const value = Number(activeMemorySalience[id]); - return Number.isFinite(value) - ? clamp(value, 0, 1) - : null; - } - - function dispatchUpdate(source) { - window.dispatchEvent( - new CustomEvent(UPDATE_EVENT, { - detail: { - levels: getState(), - dominant: getDominantChannel(), - blendedColor: getBlendedChemistryColor(), - activeMemorySalience: { ...activeMemorySalience }, - source: String(source || "server"), - }, - }) - ); - } - - function applyServerUpdate(value) { - const envelope = - value && typeof value === "object" && value.levels - ? value - : { levels: value }; - - authoritativeState = normalize(envelope.levels); - state = { ...authoritativeState }; - activeMemorySalience = normalizeSalience( - envelope.active_memory_salience || envelope.activeMemorySalience - ); - halfLivesSeconds = normalizeHalfLives( - envelope.half_lives_seconds || envelope.halfLivesSeconds - ); - - const serverTimestamp = Number(envelope.updated_at || envelope.updatedAt); - authoritativeAtMs = - Number.isFinite(serverTimestamp) && serverTimestamp > 0 - ? serverTimestamp * 1000 - : Date.now(); - - applyCssVariables({ updateSpotSizes: true }); - retargetSpotDriftForCurrentState(); - dispatchUpdate("server"); - - return getState(); - } - - function getIdleHomeostaticState(nowMs) { - const elapsedSeconds = Math.max( - 0, - (Number(nowMs || Date.now()) - authoritativeAtMs) / 1000 - ); - - return CHANNELS.reduce((result, channel) => { - const baseline = DEFAULTS[channel]; - const halfLife = Math.max(1, halfLivesSeconds[channel]); - const factor = Math.pow(0.5, elapsedSeconds / halfLife); - result[channel] = clamp( - baseline + (authoritativeState[channel] - baseline) * factor, - 0, - 1 - ); - return result; - }, {}); - } - - function applyIdleHomeostasis() { - const next = getIdleHomeostaticState(Date.now()); - const changed = CHANNELS.some(channel => { - return Math.abs(next[channel] - state[channel]) >= 0.0005; - }); - - if (!changed) { - return; - } - - state = next; - applyCssVariables(); - dispatchUpdate("homeostasis"); - } - - state = normalize(state); - authoritativeState = { ...state }; - applyCssVariables({ updateSpotSizes: true }); - startSpotDrift(); - - window.setInterval( - applyIdleHomeostasis, - IDLE_HOMEOSTASIS_TICK_MS - ); - - window.JinRuntime.metabolism = { - UPDATE_EVENT, - CHANNELS, - PALETTE, - DEFAULTS, - AMBIENT_BODY_MOTION_ENABLED, - METABOLIC_SPOT_DRIFT_ENABLED, - SPOT_GEOMETRY, - normalize, - getState, - getDominantChannel, - getBlendedChemistryColor, - getActiveMemorySalience, - tintColor, - loggerTintPercent, - auraTintPercent, - getSpotRadiusPercent, - applyServerUpdate, - }; -})(); diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 4fdd18ed..f3473950 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1154,6 +1154,9 @@ ? String(field.l4_analyzed_at || "").trim() : "", }; + delete signals[normalizedKey].significance; + delete signals[normalizedKey].metabolic_significance; + delete signals[normalizedKey].significance_updated_at; } ); diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 38dfa154..133946dd 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -400,14 +400,6 @@ function persistRuntimeFactsMemory( content ) : content; - const metabolicSignificance = Math.max( - 0, - Math.min( - 1, - Number(line && line.metabolic_significance) || 0 - ) - ); - if (!existing) { fields[key] = { content, @@ -416,7 +408,6 @@ function persistRuntimeFactsMemory( l4_status: "pending", l4_content_hash: contentHash, l4_analyzed_at: "", - significance: metabolicSignificance, }; return; @@ -446,11 +437,10 @@ function persistRuntimeFactsMemory( l4_analyzed_at: contentChanged ? "" : String(existing.l4_analyzed_at || "").trim(), - significance: Math.max( - Number(existing.significance) || 0, - metabolicSignificance - ), }; + delete fields[key].significance; + delete fields[key].metabolic_significance; + delete fields[key].significance_updated_at; } ); diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index 7b075d36..8697943c 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -330,23 +330,6 @@ function handleThinkingChunk( } -function handleMetabolismUpdate( - data -) { - const metabolism = - window.JinRuntime - && window.JinRuntime.metabolism; - - if ( - metabolism - && typeof metabolism.applyServerUpdate === "function" - ) { - metabolism.applyServerUpdate( - data - ); - } -} - function handleAgentRuntimeStart(data) { window.jinCurrentResponseRetryable = Boolean( data && data.retryable_response @@ -657,11 +640,6 @@ registerSocketMessageHandler( handleThinkingChunk ); -registerSocketMessageHandler( - "metabolism_update", - handleMetabolismUpdate -); - registerSocketMessageHandler( "agent_runtime_start", handleAgentRuntimeStart diff --git a/ui/templates/index.html b/ui/templates/index.html index d450946c..a192baf5 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,10 +14,10 @@ - + - + @@ -507,20 +507,19 @@ - - + - + - + diff --git a/utils/actions/active_memory_utils.py b/utils/actions/active_memory_utils.py index 81173e7c..bd1cdd2b 100644 --- a/utils/actions/active_memory_utils.py +++ b/utils/actions/active_memory_utils.py @@ -34,8 +34,6 @@ ) ACTIVE_MEMORY_UPDATED_AT_SUFFIX_NAME = "updated_at" -ACTIVE_MEMORY_SIGNIFICANCE_SUFFIX_NAME = "significance" - ACTIVE_MEMORY_LIFECYCLE_SUFFIX_NAMES = ( "creation_time", "created_session_id", @@ -47,7 +45,9 @@ ACTIVE_MEMORY_RUNTIME_MANAGED_SUFFIX_NAMES = ( "active_memory_id", *ACTIVE_MEMORY_LIFECYCLE_SUFFIX_NAMES, - ACTIVE_MEMORY_SIGNIFICANCE_SUFFIX_NAME, + # Removed metadata is still consumed so historical records cannot expose + # it as a custom field after the attention-only migration. + "significance", "status", ACTIVE_MEMORY_UPDATED_AT_SUFFIX_NAME, ) @@ -829,6 +829,12 @@ def _attach_active_memory_lifecycle_suffixes_to_value( " ", str(value or ""), ) + cleaned = re.sub( + r"\s*\[\s*significance\s*:\s*[^\]]*\]\s*", + " ", + cleaned, + flags=re.IGNORECASE, + ) cleaned = re.sub( r"\s+", " ", diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index 2ffe6304..8175e252 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -1904,16 +1904,6 @@ async def save_active_memory_runtime_record( context=context, ) - try: - from runtime.metabolism import seed_active_memory_significance - - active_memory_line = seed_active_memory_significance( - active_memory_line, - context=context, - ) - except Exception: - pass - active_records = getattr( context, "active_memory_records", diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index e4abe409..70870694 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -132,69 +132,6 @@ def apply_active_memory_records( context.active_memory_records = records -def apply_metabolism_bootstrap_state( - context, - message_data: dict, -) -> None: - - from runtime.metabolism import ( - METABOLISM_DEFAULT_LEVELS, - normalize_metabolism_associations, - normalize_metabolism_levels, - ) - - raw_levels = message_data.get( - "metabolism_levels", - None, - ) - - if isinstance(raw_levels, dict): - context.runtime_metabolism_levels = normalize_metabolism_levels( - raw_levels - ) - else: - context.runtime_metabolism_levels = normalize_metabolism_levels( - getattr( - context, - "runtime_metabolism_levels", - METABOLISM_DEFAULT_LEVELS, - ) - ) - - try: - updated_at = float( - message_data.get( - "metabolism_updated_at", - 0.0, - ) - or 0.0 - ) - except (TypeError, ValueError): - updated_at = 0.0 - - context.runtime_metabolism_last_tick_at = max(0.0, updated_at) - context.runtime_metabolism_instruction = str( - message_data.get( - "metabolism_instruction", - getattr(context, "runtime_metabolism_instruction", ""), - ) - or "" - )[:900].strip() - context.runtime_metabolism_associations = normalize_metabolism_associations( - message_data.get( - "metabolism_associations", - getattr(context, "runtime_metabolism_associations", []), - ) - ) - context.runtime_metabolism_last_committed_l1_id = str( - message_data.get( - "metabolism_last_committed_l1_id", - getattr(context, "runtime_metabolism_last_committed_l1_id", ""), - ) - or "" - ).strip() - - def active_memory_records_text(context) -> str: return "\n".join( @@ -2137,10 +2074,6 @@ def apply_runtime_resume( context, message_data, ) - apply_metabolism_bootstrap_state( - context, - message_data, - ) apply_delayed_memory_reports( context, message_data, @@ -2955,10 +2888,6 @@ def apply_session_bootstrap( context, message_data, ) - apply_metabolism_bootstrap_state( - context, - message_data, - ) apply_delayed_memory_reports( context, message_data, @@ -3240,14 +3169,6 @@ async def initialize_connection( }, ) - # Chemistry is runtime state, not a page artifact. Re-emit it immediately - # on connect/reconnect so the avatar never falls back to browser defaults. - from runtime.metabolism import emit_metabolism_state - - await emit_metabolism_state( - context - ) - if skip_initial_runtime_state: await context.logger.log_system( "[WS] soft reconnect: initial runtime state skipped" diff --git a/websocket/logger.py b/websocket/logger.py index c1dd0191..e4d4c748 100644 --- a/websocket/logger.py +++ b/websocket/logger.py @@ -181,24 +181,6 @@ async def log_error( async def log_runtime(self, message: str): await self.log("[RUNTIME]", message) - async def log_metabolism( - self, - message: str, - details: str | None = None, - event: str | None = None, - request_id: str | None = None, - metabolism_levels: dict | None = None, - ): - await self.log( - "[METABOLISM]", - message, - details=details, - channel="metabolism", - metabolism_event=event, - metabolism_request_id=request_id, - metabolism_levels=metabolism_levels, - ) - async def log_flow( self, message: str, diff --git a/websocket/messages.py b/websocket/messages.py index 025080f4..f60a6fc8 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -28,12 +28,6 @@ build_runtime_session_checkpoint, record_runtime_memory_reasoning_quotes, ) -from runtime.metabolism import ( - append_metabolism_turn, - cancel_metabolism_update, - prepare_metabolism_for_turn, - settle_metabolism_after_turn, -) from runtime.state_sync import refresh_runtime_state from utils.brain_client_utils import ( get_brain_runtime_config, @@ -865,14 +859,6 @@ def discard_latest_visible_turn_for_user_retry( if isinstance(candidate, dict): previous_turn = candidate - metabolism_turns = getattr( - context, - "runtime_metabolism_recent_turns", - None, - ) - if isinstance(metabolism_turns, list) and metabolism_turns: - metabolism_turns.pop() - # The previous reasoning belongs to the discarded answer and must not be # re-injected beside the explicit retry marker. context.runtime_previous_reasoning_content = "" @@ -1069,21 +1055,6 @@ async def process_message( is_action_guard_retry = bool( action_guard_retry ) - metabolism_turn_feedback = deepcopy( - getattr( - context, - "runtime_last_response_feedback", - None, - ) - ) - - if not is_action_guard_retry: - # A real foreground turn always wins over the previous background - # metabolism estimate, especially when SERVICE is also the brain. - cancel_metabolism_update( - context - ) - if is_session_restore_resume: # The restore tick has no user message, so take the live browser # geometry from the resume request before building its context. @@ -1210,17 +1181,6 @@ async def process_message( message_data, ) - if not is_user_retry: - # Homeostat first: the current user event must be able to affect - # this very generation, not only the next one. No bubble/event - # card is emitted; only the existing ambient metabolism signal. - await prepare_metabolism_for_turn( - context, - get_message_user_text( - message_data - ), - ) - context.runtime_turn_memory_user_message = ( format_runtime_memory_user_message( context, @@ -1387,18 +1347,6 @@ async def process_message( ), assistant_created_at=assistant_created_at, ) - append_metabolism_turn( - context, - user_message=user_text, - assistant_message=assistant_message, - reasoning=getattr( - context, - "runtime_turn_reasoning_content", - "", - ), - feedback=metabolism_turn_feedback, - ) - if not is_action_guard_retry and not is_user_retry: context.assistant_message_count += 1 context.turn_number += 1 @@ -1462,14 +1410,6 @@ async def process_message( "completed_turn_commit": completed_turn_commit, }) - if not is_action_guard_retry: - # Runtime actions/validators leave a small causal trace immediately. - # The slower SERVICE integration still runs afterwards and may - # correct the reflex from the full turn snapshot. - await settle_metabolism_after_turn( - context - ) - if is_session_restore_resume: # The Brain node consumes restore priming immediately after the # first response and replays archived resources through the real From 2c392a0065fc1fe4ef8eb8d0af2191870a16174e Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:02:03 +0300 Subject: [PATCH 107/168] chore: ui - fix pinned report L-T facts visibility in memory panel - add tooltip for L-T facts metadata preview --- runtime/L4_memory.py | 55 ++++- tests/test_delayed_memory_client_contract.py | 52 ++++ tests/test_l4_hover_card_client_contract.py | 72 ++++++ tests/test_l4_memory.py | 54 +++++ ui/static/css/runtime-memory.css | 104 ++++++++ ui/static/js/runtime/runtime-avatar.js | 13 + ui/static/js/runtime/runtime-memory-view.js | 242 ++++++++++++++++++- ui/static/js/runtime/runtime.js | 108 ++++++++- ui/templates/index.html | 4 +- 9 files changed, 684 insertions(+), 20 deletions(-) create mode 100644 tests/test_l4_hover_card_client_contract.py diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index c6b16228..468b1458 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -1128,12 +1128,63 @@ def get_loaded_delayed_memory_reports( ) +def get_context_delayed_memory_reports( + context, +) -> dict: + + loaded_reports = get_loaded_delayed_memory_reports( + context + ) + context_reports = { + report_id: { + **report, + "id": report_id, + } + for report_id, report in loaded_reports.items() + if isinstance(report, dict) + } + stored_reports = getattr( + context, + "delayed_memory_reports", + {}, + ) + + if not isinstance(stored_reports, dict): + return context_reports + + # Pinning is a direct context state, just like an explicit load. Keep this + # helper pure: L4 visibility must not depend on another prompt builder + # having called include_pinned_delayed_memory_reports() first. + for raw_report_id, report in stored_reports.items(): + if ( + not isinstance(report, dict) + or not bool(report.get("pinned", False)) + ): + continue + + report_id = str( + raw_report_id + or report.get("id", "") + or "" + ).strip().casefold() + + if not report_id: + continue + + context_reports[report_id] = { + **report, + "id": report_id, + } + + return context_reports + + def collect_loaded_delayed_memory_fact_ids( context, ) -> set[str]: return collect_long_term_fact_ids_from_reports( - get_loaded_delayed_memory_reports( + get_context_delayed_memory_reports( context ) ) @@ -1143,7 +1194,7 @@ def collect_loaded_delayed_memory_fact_report_ids( context, ) -> dict[str, list[str]]: - reports = get_loaded_delayed_memory_reports( + reports = get_context_delayed_memory_reports( context ) diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 9aaeb7c3..c3c188a9 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -267,6 +267,58 @@ def test_context_loaded_delayed_memory_highlights_report_and_linked_l4(self): self.assertIn("loaded_delayed_memory_ids", socket_source) self.assertIn("replaceLoadedDelayedMemoryReportIds", socket_source) + def test_context_loaded_report_facts_override_l4_archive_filter(self): + + runtime_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" + ).read_text(encoding="utf-8") + avatar_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" + ).read_text(encoding="utf-8") + + visible_start = runtime_source.index( + "function getContextLoadedLongTermFactIds()" + ) + visible_end = runtime_source.index( + "function setDelayedMemoryPinnedOnAvatar(", + visible_start, + ) + visible_block = runtime_source[visible_start:visible_end] + + self.assertIn( + "loadedDelayedMemoryReportIds.has(reportId)", + visible_block, + ) + self.assertIn("Boolean(report.pinned)", visible_block) + self.assertIn("report.facts_ids", visible_block) + self.assertIn("report.absorbed_fact_ids", visible_block) + self.assertIn("report.long_term_facts_ids", visible_block) + self.assertIn("fact.source_fact_ids", visible_block) + self.assertIn("getFactsWithArchiveState", visible_block) + self.assertIn("longTermFactMatchesIdSet", visible_block) + + pin_start = runtime_source.index( + "function setDelayedMemoryReportPinned(" + ) + pin_end = runtime_source.index( + "function setDelayedMemoryReportAnchorFactIds(", + pin_start, + ) + pin_block = runtime_source[pin_start:pin_end] + self.assertIn("renderRuntimeMemorySnapshot();", pin_block) + + avatar_start = avatar_source.index( + "function getL4MemoryAvatarRecords()" + ) + avatar_end = avatar_source.index( + "function getPersistentFileAvatarRecords()", + avatar_start, + ) + avatar_block = avatar_source[avatar_start:avatar_end] + self.assertIn("record.pinned || record.loaded", avatar_block) + self.assertIn("contextLoadedFactIds", avatar_block) + self.assertIn("record.linkedFactIds", avatar_block) + def test_pinning_delayed_memory_does_not_create_secondary_report_glow(self): memory_view_source = ( diff --git a/tests/test_l4_hover_card_client_contract.py b/tests/test_l4_hover_card_client_contract.py new file mode 100644 index 00000000..f71fc91f --- /dev/null +++ b/tests/test_l4_hover_card_client_contract.py @@ -0,0 +1,72 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-memory-view.js" +) +MEMORY_CSS = ROOT / "ui" / "static" / "css" / "runtime-memory.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class L4HoverCardClientContractTests(unittest.TestCase): + + def test_l4_rows_use_custom_hover_card_instead_of_native_title(self): + source = MEMORY_VIEW.read_text(encoding="utf-8") + + self.assertIn( + "if (options.interactiveLongTermMemory) {\n" + " bindLongTermMemoryHoverCard(\n" + " row,\n" + " line\n" + " );", + source, + ) + self.assertIn('row.removeAttribute("title");', source) + self.assertIn('card.setAttribute("role", "tooltip");', source) + self.assertNotIn("runtime-memory-l4-hover-icon", source) + + def test_hover_card_reads_the_same_value_and_metadata_used_by_old_title(self): + source = MEMORY_VIEW.read_text(encoding="utf-8") + + self.assertIn( + "memoryModel.splitMemoryMeta(line.value || \"\")", + source, + ) + self.assertIn("valuePresentation.text", source) + self.assertIn("valuePresentation.tags.forEach((tag) => {", source) + self.assertIn("String(tag && tag.key || \"\")", source) + self.assertIn("String(tag && tag.value || \"\")", source) + + def test_hover_card_sits_left_of_panel_and_does_not_capture_pointer(self): + source = MEMORY_VIEW.read_text(encoding="utf-8") + css = MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn( + "panelRect.left - cardRect.width - gap", + source, + ) + self.assertIn( + '"--runtime-memory-l4-hover-arrow-y"', + source, + ) + self.assertIn(".runtime-memory-l4-hover-card {", css) + self.assertIn("position: fixed;", css) + self.assertIn("pointer-events: none;", css) + self.assertIn(".runtime-memory-l4-hover-card::before", css) + self.assertNotIn(".runtime-memory-l4-hover-icon", css) + + def test_hover_card_assets_are_cache_busted(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertEqual(source.count("l4-hover-card=2"), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 87f577d0..cc9b4f37 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -2628,6 +2628,60 @@ def test_loaded_delayed_report_fact_ids_are_visible_with_report_suffix(self): set(), ) + def test_pinned_delayed_report_fact_ids_are_visible_with_report_suffix(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "user.name", + "value": "Sergey", + }, + { + "id": "F2", + "key": "social.friend", + "value": "Taras is a personal friend.", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social context", + "pinned": True, + "anchor_fact_ids": [ + "F1", + ], + "facts_ids": [ + "F1", + "F2", + ], + }, + } + + context_block = build_runtime_l4_memory_context( + context=context + ) + + self.assertIn( + "user.name: Sergey [ id: F1 ] [ significance: 0.000 ] " + "[ delayed_memory_id: abc123 ]", + context_block, + ) + self.assertIn( + "social.friend: Taras is a personal friend. [ id: F2 ] " + "[ significance: 0.000 ] [ delayed_memory_id: abc123 ]", + context_block, + ) + self.assertEqual( + context.runtime_l4_archived_fact_ids, + set(), + ) + def test_anchor_visibility_wins_over_absorbed_reference_in_other_report(self): context = RuntimeContext( websocket=None, diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 12685856..c78bf79a 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -193,6 +193,110 @@ -webkit-line-clamp: 2; } +.runtime-memory-l4-hover-card { + --runtime-memory-l4-hover-arrow-y: 50%; + position: fixed; + z-index: 90; + width: min(430px, calc(100vw - 24px)); + box-sizing: border-box; + pointer-events: none; + border: 1px solid rgba(148, 163, 184, 0.52); + border-radius: 8px; + background: rgba(17, 20, 25, 0.97); + box-shadow: + 0 18px 42px rgba(0, 0, 0, 0.44), + inset 0 1px 0 rgba(255, 255, 255, 0.035); + padding: 12px 14px 13px; + color: rgba(228, 228, 231, 0.92); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 11px; + line-height: 1.45; +} + +.runtime-memory-l4-hover-card::before, +.runtime-memory-l4-hover-card::after { + position: absolute; + top: var(--runtime-memory-l4-hover-arrow-y); + width: 0; + height: 0; + content: ""; + transform: translateY(-50%); +} + +.runtime-memory-l4-hover-card::before { + right: -10px; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-left: 10px solid rgba(148, 163, 184, 0.52); +} + +.runtime-memory-l4-hover-card::after { + right: -8px; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 8px solid rgba(17, 20, 25, 0.97); +} + +.runtime-memory-l4-hover-header { + display: flex; + align-items: center; + min-width: 0; + gap: 9px; + margin-bottom: 9px; +} + + +.runtime-memory-l4-hover-title { + min-width: 0; + overflow: hidden; + flex: 1 1 auto; + color: rgba(244, 244, 245, 0.97); + font-size: 12px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.runtime-memory-l4-hover-age { + flex: 0 0 auto; + color: rgba(161, 161, 170, 0.78); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.runtime-memory-l4-hover-summary { + margin: 0 0 10px; + padding: 0 0 10px; + border-bottom: 1px solid rgba(82, 82, 91, 0.52); + color: rgba(212, 212, 216, 0.78); + overflow-wrap: anywhere; +} + +.runtime-memory-l4-hover-metadata { + display: grid; + gap: 4px; +} + +.runtime-memory-l4-hover-metadata-row { + display: grid; + grid-template-columns: minmax(118px, 0.9fr) minmax(0, 1.8fr); + align-items: baseline; + gap: 10px; +} + +.runtime-memory-l4-hover-metadata-key { + min-width: 0; + color: rgba(161, 161, 170, 0.86); + overflow-wrap: anywhere; +} + +.runtime-memory-l4-hover-metadata-value { + min-width: 0; + color: rgba(226, 232, 240, 0.90); + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; +} + .runtime-memory-key, .runtime-memory-value { color: rgba(244,244,245,0.62); diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index f4fccc67..1f0ced2f 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -1856,6 +1856,16 @@ : l4Memory && typeof l4Memory.getFacts === "function" ? l4Memory.getFacts() : []; + const contextLoadedFactIds = new Set(); + + getDelayedMemoryAvatarRecords() + .filter(record => Boolean( + record && (record.pinned || record.loaded) + )) + .forEach((record) => { + normalizeL4FactIds(record.linkedFactIds) + .forEach(factId => contextLoadedFactIds.add(factId)); + }); return (Array.isArray(facts) ? facts : []) .map((fact) => { @@ -1891,6 +1901,9 @@ Boolean( fact.archived || fact.hidden_from_context + ) + && !l4FactIds.some( + factId => contextLoadedFactIds.has(factId) ), avatarMemoryHoverId: buildAvatarMemoryHoverId( diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 96ed97c5..3e4685da 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -2496,6 +2496,211 @@ const runtimeMemoryHoverTitleSources = new WeakMap(); const runtimeMemoryHoverTitleBoundNodes = new WeakSet(); + let longTermMemoryHoverCard = null; + let longTermMemoryHoverCardAnchor = null; + + function appendLongTermMemoryHoverMetadataRow( + container, + key, + value + ) { + const row = document.createElement("div"); + const keyNode = document.createElement("span"); + const valueNode = document.createElement("span"); + + row.className = + "runtime-memory-l4-hover-metadata-row"; + keyNode.className = + "runtime-memory-l4-hover-metadata-key"; + keyNode.textContent = key ? `${key}:` : ""; + valueNode.className = + "runtime-memory-l4-hover-metadata-value"; + valueNode.textContent = String(value || ""); + + row.appendChild(keyNode); + row.appendChild(valueNode); + container.appendChild(row); + } + + function positionLongTermMemoryHoverCard(card, anchor) { + if (!card || !anchor || !anchor.isConnected) { + return; + } + + const anchorRect = anchor.getBoundingClientRect(); + const panelRect = memoryPanel + ? memoryPanel.getBoundingClientRect() + : anchorRect; + const cardRect = card.getBoundingClientRect(); + const viewportWidth = Math.max( + document.documentElement.clientWidth || 0, + window.innerWidth || 0 + ); + const viewportHeight = Math.max( + document.documentElement.clientHeight || 0, + window.innerHeight || 0 + ); + const margin = 12; + const gap = 14; + const anchorCenterY = + anchorRect.top + (anchorRect.height / 2); + const left = Math.min( + Math.max( + margin, + panelRect.left - cardRect.width - gap + ), + Math.max( + margin, + viewportWidth - cardRect.width - margin + ) + ); + const top = Math.min( + Math.max( + margin, + anchorCenterY - (cardRect.height / 2) + ), + Math.max( + margin, + viewportHeight - cardRect.height - margin + ) + ); + const arrowY = Math.min( + Math.max(18, anchorCenterY - top), + Math.max(18, cardRect.height - 18) + ); + + card.style.left = `${Math.round(left)}px`; + card.style.top = `${Math.round(top)}px`; + card.style.setProperty( + "--runtime-memory-l4-hover-arrow-y", + `${Math.round(arrowY)}px` + ); + } + + function hideLongTermMemoryHoverCard(anchor = null) { + if ( + anchor + && longTermMemoryHoverCardAnchor + && anchor !== longTermMemoryHoverCardAnchor + ) { + return; + } + + if ( + longTermMemoryHoverCard + && longTermMemoryHoverCard.isConnected + ) { + longTermMemoryHoverCard.remove(); + } + + longTermMemoryHoverCard = null; + longTermMemoryHoverCardAnchor = null; + } + + function showLongTermMemoryHoverCard(anchor, line) { + if (!anchor || !line) { + return; + } + + hideLongTermMemoryHoverCard(); + + const valuePresentation = + memoryModel.splitMemoryMeta(line.value || ""); + const card = document.createElement("div"); + const header = document.createElement("div"); + const title = document.createElement("span"); + const age = document.createElement("span"); + const metadata = document.createElement("div"); + const displayKey = + memoryModel.runtimeMemoryDisplay.convertKeyToName( + String(line.key || "") + ) + || String(line.key || "").trim() + || "Long term memory"; + + card.className = + "runtime-memory-l4-hover-card"; + card.setAttribute("role", "tooltip"); + header.className = + "runtime-memory-l4-hover-header"; + title.className = + "runtime-memory-l4-hover-title"; + title.textContent = displayKey; + age.className = + "runtime-memory-l4-hover-age"; + metadata.className = + "runtime-memory-l4-hover-metadata"; + + if ( + Number.isFinite(Number(line.context_age_timestamp)) + && Number(line.context_age_timestamp) > 0 + ) { + age.textContent = + formatLongTermFactAgeLabel( + line.context_age_timestamp + ); + } + + header.appendChild(title); + + if (age.textContent) { + header.appendChild(age); + } + + card.appendChild(header); + + if (String(valuePresentation.text || "").trim()) { + const summary = document.createElement("div"); + + summary.className = + "runtime-memory-l4-hover-summary"; + summary.textContent = String( + valuePresentation.text || "" + ).replace(/\\n/g, " ↵ "); + card.appendChild(summary); + } + + valuePresentation.tags.forEach((tag) => { + appendLongTermMemoryHoverMetadataRow( + metadata, + String(tag && tag.key || ""), + String(tag && tag.value || "") + ); + }); + + if (metadata.childElementCount) { + card.appendChild(metadata); + } + + longTermMemoryHoverCard = card; + longTermMemoryHoverCardAnchor = anchor; + document.body.appendChild(card); + positionLongTermMemoryHoverCard(card, anchor); + } + + function bindLongTermMemoryHoverCard(row, line) { + if (!row || !line) { + return; + } + + row.removeAttribute("title"); + row.addEventListener("mouseenter", () => { + showLongTermMemoryHoverCard(row, line); + }); + row.addEventListener("mouseleave", () => { + hideLongTermMemoryHoverCard(row); + }); + } + + window.addEventListener("resize", () => { + hideLongTermMemoryHoverCard(); + }); + + if (memoryScroll) { + memoryScroll.addEventListener("scroll", () => { + hideLongTermMemoryHoverCard(); + }, { passive: true }); + } function resolveRuntimeMemoryHoverTitle(node) { if (!node) { @@ -3796,21 +4001,28 @@ runtimeMemoryValueFontWeight(line) ); - let hoverTitle = null; + if (options.interactiveLongTermMemory) { + bindLongTermMemoryHoverCard( + row, + line + ); + } else { + let hoverTitle = null; - bindRuntimeMemoryHoverTitle( - row, - () => { - if (hoverTitle === null) { - hoverTitle = - formatRuntimeMemoryHoverTitle( - `${key}: ${valuePresentation.raw}` - ); - } + bindRuntimeMemoryHoverTitle( + row, + () => { + if (hoverTitle === null) { + hoverTitle = + formatRuntimeMemoryHoverTitle( + `${key}: ${valuePresentation.raw}` + ); + } - return hoverTitle; - } - ); + return hoverTitle; + } + ); + } if (longTermHeader) { row.appendChild(longTermHeader); @@ -4203,6 +4415,8 @@ configureRuntimeMemoryDeleteHold( row, () => { + hideLongTermMemoryHoverCard(row); + if (typeof deleteLongTermMemoryFact === "function") { deleteLongTermMemoryFact( line.id @@ -8098,6 +8312,8 @@ function renderLongTermMemoryFacts() { + hideLongTermMemoryHoverCard(); + const records = getLongTermMemoryFactRecords(); const delayedReports = diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 133946dd..f544e6e0 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -718,7 +718,108 @@ function refreshRuntimeAvatar() { } } +function getContextLoadedLongTermFactIds() { + const factIds = new Set(); + const reports = readDelayedMemoryReports(); + + Object.entries( + reports && typeof reports === "object" && !Array.isArray(reports) + ? reports + : {} + ).forEach(([rawReportId, report]) => { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return; + } + + const reportId = + normalizeRuntimeDelayedMemoryReportId(rawReportId); + const inContext = + Boolean(report.pinned) + || Boolean( + reportId + && loadedDelayedMemoryReportIds.has(reportId) + ); + + if (!inContext) { + return; + } + + normalizeRuntimeLongTermFactIds([ + report.anchor_fact_ids, + report.facts_ids, + report.absorbed_fact_ids, + report.long_term_facts_ids, + ]).forEach(factId => factIds.add(factId)); + }); + + return factIds; +} + +function longTermFactMatchesIdSet( + fact, + factIds +) { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + || !(factIds instanceof Set) + || !factIds.size + ) { + return false; + } + + return normalizeRuntimeLongTermFactIds([ + fact.id, + fact.source_fact_ids, + ]).some(factId => factIds.has(factId)); +} + +function stripLongTermFactArchiveState(fact) { + if ( + !fact + || typeof fact !== "object" + || Array.isArray(fact) + ) { + return fact; + } + + const visibleFact = { + ...fact, + }; + + delete visibleFact.archived; + delete visibleFact.hidden_from_context; + + return visibleFact; +} + function getVisibleLongTermMemoryFacts() { + if ( + l4Memory + && typeof l4Memory.getFactsWithArchiveState === "function" + ) { + const contextLoadedFactIds = + getContextLoadedLongTermFactIds(); + + return l4Memory.getFactsWithArchiveState() + .filter((fact) => ( + !Boolean( + fact + && (fact.archived || fact.hidden_from_context) + ) + || longTermFactMatchesIdSet( + fact, + contextLoadedFactIds + ) + )) + .map(stripLongTermFactArchiveState); + } + if ( l4Memory && typeof l4Memory.getVisibleFacts === "function" @@ -1460,9 +1561,10 @@ function setDelayedMemoryReportPinned( reports ); - if (runtimeMemoryDisplayMode === "delayed") { - renderRuntimeMemorySnapshot(); - } + // Pinning changes context visibility for linked L4 facts (and can affect + // other linked-memory rows), so refresh the currently open panel + // immediately instead of waiting for the next message/server echo. + renderRuntimeMemorySnapshot(); // Pin state affects more than the delayed-memory dash itself: it also // controls linked L4/attachment highlights. Re-sync the whole delayed diff --git a/ui/templates/index.html b/ui/templates/index.html index a192baf5..de390613 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,7 +14,7 @@ - + @@ -508,7 +508,7 @@ - + From 3accf89ba21843f4c78bdd3fdc8a1415ad3faff4 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:42:31 +0300 Subject: [PATCH 108/168] chore: memory panel - add tabs for all memory types in panel - add draggable zone below memory content - update reasoning loop detection for ascii patters - fix for reasoning loop append latest reasoning - cleanup visual restore redundant logging - fix for delayed memory session actions logging display --- agent/nodes/brain.py | 78 +--- clients/brain_client.py | 67 +++- config.example.py | 2 +- docs/JIN_ARCHITECTURE.md | 6 +- docs/JIN_CURRENT_STATE.md | 9 +- docs/JIN_DECISIONS.md | 10 +- rules/brain_context_builder.py | 23 +- runtime/runtime_context.py | 9 - runtime/stream.py | 2 +- tests/test_archived_session_restore.py | 18 +- tests/test_brain_asset_flow.py | 16 +- tests/test_brain_prompt_memory.py | 8 +- tests/test_brain_runtime_actions.py | 9 + ...est_runtime_memory_tabs_client_contract.py | 78 ++++ ...untime_memory_timestamp_client_contract.py | 2 +- tests/test_runtime_stream_tokens.py | 16 +- tests/test_stream_validator.py | 102 ++++- ui/static/css/runtime-memory.css | 118 +++++- ui/static/css/theme-win95.css | 31 +- ui/static/js/logger/logger.js | 14 +- ui/static/js/runtime/runtime-memory-view.js | 367 +++++++----------- ui/templates/index.html | 59 ++- utils/context/session_actions.py | 10 + utils/session_actions_history.py | 12 +- utils/stream_validator.py | 119 +++++- websocket/bootstrap.py | 37 -- 26 files changed, 749 insertions(+), 473 deletions(-) create mode 100644 tests/test_runtime_memory_tabs_client_contract.py diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index 0e4c58f3..a805cfb6 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -28,10 +28,6 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_ATTACH_FILE, RUNTIME_ACTION_DEEP_WEB_SEARCH, - RUNTIME_ACTION_JIN_COLOR, - RUNTIME_ACTION_JIN_POSITION, - RUNTIME_ACTION_JIN_SIZE, - RUNTIME_ACTION_JIN_SPEED, RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_WEB_SEARCH, ) @@ -354,40 +350,12 @@ async def replay_session_restore_resource_actions( file_seen.add(file_id) file_ids.append(file_id) - jin_color = str( - getattr( - context, - "runtime_session_restore_pending_jin_color", - "", - ) - or "" - ).strip() - jin_size = getattr( - context, - "runtime_session_restore_pending_jin_size", - None, - ) - jin_position = getattr( - context, - "runtime_session_restore_pending_jin_position", - None, - ) - jin_speed = getattr( - context, - "runtime_session_restore_pending_jin_speed", - None, - ) - # Consume the restoration envelope before any possible contract-driven # follow-up. The initial answer was generated with metadata only; from # this point forward the normal runtime action pipeline owns the loaded # resources and any follow-up sees their real context. context.runtime_session_restore_pending_loaded_memory_ids = [] context.runtime_session_restore_pending_attached_file_ids = [] - context.runtime_session_restore_pending_jin_color = "" - context.runtime_session_restore_pending_jin_size = None - context.runtime_session_restore_pending_jin_position = None - context.runtime_session_restore_pending_jin_speed = None context.runtime_session_restore_priming = False context.runtime_session_restore_reasoning_dump = "" context.runtime_session_restore_l4_fact_ids = [] @@ -409,30 +377,6 @@ async def replay_session_restore_resource_actions( ) for file_id in file_ids ] - + ([ - RuntimeActionCall( - name=RUNTIME_ACTION_JIN_COLOR, - payload=jin_color, - ) - ] if jin_color else []) - + ([ - RuntimeActionCall( - name=RUNTIME_ACTION_JIN_SIZE, - payload=jin_size, - ) - ] if jin_size else []) - + ([ - RuntimeActionCall( - name=RUNTIME_ACTION_JIN_POSITION, - payload=jin_position, - ) - ] if jin_position else []) - + ([ - RuntimeActionCall( - name=RUNTIME_ACTION_JIN_SPEED, - payload=jin_speed, - ) - ] if jin_speed else []) ) if not actions: @@ -663,22 +607,12 @@ def remember_recovery_reasoning_for_followup( if not normalized_reasoning: return - loop_contents = getattr( - context, - "runtime_previous_reasoning_loop_contents", - [], - ) - - if not isinstance( - loop_contents, - list, - ): - loop_contents = [] - - loop_contents.append( + # Recovery follow-ups only need the immediately preceding failed + # reasoning. Replacing the slot prevents repeated loop retries from + # accumulating older reasoning blocks in the next prompt. + context.runtime_previous_reasoning_loop_contents = [ normalized_reasoning - ) - context.runtime_previous_reasoning_loop_contents = loop_contents + ] def remember_successful_previous_reasoning( @@ -2006,7 +1940,7 @@ def ensure_runtime_list( if replayed_restore_actions: # ATTACH_FILE / LOAD_DELAYED_MEMORY replay after the very first # restored JIN message is state reconstruction, not a new model - # decision. Run the real action dispatcher (bubbles, avatar, + # decision. Run the real action dispatcher (bubbles, # tool results, pinning, etc.) but consume its action/results as # already handled so the follow-up scheduler cannot fire even # when the normal runtime contract says emit_followup=true. diff --git a/clients/brain_client.py b/clients/brain_client.py index 6ba75496..772057f2 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -1223,11 +1223,72 @@ async def finalize_session_action_history() -> None: get_action_counter_display_payloads() ), ) - replace_session_action_history_since( + + history = getattr( context, - session_action_history_start, - marker_actions, + "runtime_session_action_history", + None, ) + saved_delayed_memory_items = [] + safe_start_index = 0 + + if isinstance(history, list): + safe_start_index = max( + 0, + min( + int( + session_action_history_start + or 0 + ), + len(history), + ), + ) + saved_delayed_memory_items = [ + dict(item) + for item in history[ + safe_start_index: + ] + if isinstance(item, dict) + and item.get( + "runtime_session_action_marker_item" + ) is not True + and str( + item.get("text", "") + or "" + ).strip().startswith( + "Delayed memory saved:" + ) + ] + + if saved_delayed_memory_items: + remaining_marker_actions = [ + action + for action in marker_actions + if str( + action.get("name", "") + or "" + ).strip().upper() + != RUNTIME_ACTION_SAVE_DELAYED_MEMORY + ] + + if remaining_marker_actions: + replace_session_action_history_since( + context, + session_action_history_start, + remaining_marker_actions, + ) + else: + del history[safe_start_index:] + + history.extend( + saved_delayed_memory_items + ) + else: + replace_session_action_history_since( + context, + session_action_history_start, + marker_actions, + ) if marker_actions and session_action_message_content: attach_session_action_jin_message_since( diff --git a/config.example.py b/config.example.py index c6922652..21cbdb48 100644 --- a/config.example.py +++ b/config.example.py @@ -45,7 +45,7 @@ STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_SIZE = 6 STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_REPETITIONS = 6 STREAM_VALIDATOR_MAX_REPEAT_SENTENCES = 5 -STREAM_VALIDATOR_MAX_REPEAT_SYMBOLIC_MOTIFS = 4 +STREAM_VALIDATOR_MAX_REPEAT_SYMBOLIC_MOTIFS = 5 STREAM_VALIDATOR_SYMBOLIC_MOTIF_HISTORY_LINES = 48 STREAM_VALIDATOR_MAX_SENTENCE_LOOP_SEQUENCE_SIZE = 16 STREAM_VALIDATOR_MIN_RECURRENT_SENTENCE_WORDS = 5 diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index d129c7b9..35bed7b7 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -456,8 +456,8 @@ Archived restore is a distinct path: 3. Backend sets `runtime_session_restore_priming` and stages historical resource IDs/metadata. 4. A hidden `archived_session_resume` Brain tick receives restore-specific context. 5. The restore prompt includes the newest complete visible dialog pairs plus a bounded recent reasoning dump. -6. Historical loaded Delayed IDs, attached files, and avatar state are not blindly applied before the restore greeting. -7. `BrainNode` consumes the staged restore envelope and replays those resources through the normal runtime-action dispatcher. +6. Historical loaded Delayed IDs and attached files are staged until after the restore greeting; room/avatar state is already restored by the bootstrap path and is not replayed as runtime actions. +7. `BrainNode` consumes the staged restore envelope and replays only those delayed-memory/file resources through the normal runtime-action dispatcher. 8. The WebSocket tail performs defensive cleanup only; it must not apply the same resources a second time. The restore instruction is stamped at prompt-build time with the current timezone-aware bootstrap time immediately before `Current session was bootstrapped in a browser tab!`. Archived USER timestamps remain historical dialogue metadata and are never reused as the bootstrap time. @@ -531,6 +531,8 @@ Do not collapse these into a generic highlight state. The L4 panel deliberately separates compact browsing from surfaced evidence: ordinary fact rows use a 50-character value preview, while a row bubbled by runtime reference, explicit reasoning citation, or context-loaded state renders its full value. The storage value is never truncated; this is projection-only behavior. +The memory panel exposes five persistent navigation tabs: `FRAME` (live runtime-memory snapshots), `ACTIVE`, `DELAYED`, `L-T` (L4), and `FILES`. Their shared count control is projected below the active tab; only FRAME exposes previous/next snapshot controls. The temporary unprocessed-facts projection is intentionally omitted from the tab bar. + Session-action logger rows are also a projection of structured history. The compact logger keeps the most recent five items in chronological order with their original numbering and reuses the existing attached-files header/button primitive for `FULL`. JIN_COLOR parts render one swatch per applied color and expose the normalized hex on hover; bootstrap must preserve the `colors` payload for this to work after reload. Runtime-action bubble details are retained across counter-only updates so a count refresh cannot erase existing hover metadata. JIN visual-action chat bubbles are currently release-gated off, but parsing, execution, avatar updates, raw event persistence, and Session Actions logging remain active. A UI visibility flag must not be mistaken for a disabled runtime action. diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index 6576ae25..30a2ea09 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -316,9 +316,9 @@ Verified behavior: - archived visible dialogue is rebuilt from logs; - the newest three real USER moves are used for the restore context in chronological order, with empty JIN retained where the turn was interrupted/action-only; - a bounded recent reasoning dump is included; -- loaded Delayed IDs, attached files, and avatar state are staged; +- loaded Delayed IDs and attached files are staged; room/avatar state is restored by bootstrap and is not replayed as runtime actions; - restore Brain response occurs before normal resource reactivation; -- `BrainNode.replay_session_restore_resource_actions()` consumes the staged envelope and applies it through the real action dispatcher; +- `BrainNode.replay_session_restore_resource_actions()` consumes the staged Delayed/file envelope and applies only those resources through the real action dispatcher; - WebSocket tail only clears defensive state and explicitly warns against mutating/emitting resource state a second time. This is a strong architectural clue for future restore fixes: find duplicate writers before adding another apply. @@ -407,9 +407,9 @@ Owner intent from the latest UX pass was approximately 250 ms reveal and 1 s hid ### 13.2 FRAME naming -Owner prefers `FRAME` over generic `STATE` for the question -> answer -> live-context cycle. +The live runtime-memory view is now the `FRAME` tab. The rename is deliberately limited to that visible tab; internal runtime/state names remain unchanged. -**State:** preference recorded, but this snapshot does not establish a safe complete rename target. Do not global-replace `RUNTIME`/`STATE`. +The panel always shows `FRAME`, `ACTIVE`, `DELAYED`, `L-T`, and `FILES`, even when a non-FRAME view is empty. The shared counter moves below the selected tab; FRAME keeps the existing snapshot arrows while the other tabs show only their record count. The temporary unprocessed-facts projection is not exposed as a tab. ### 13.3 Visual reuse @@ -529,7 +529,6 @@ Do not present these as settled without fresh code evidence: - which remaining L2/L3-named compatibility fields are still required for real historical data; - whether all old `SAVE_SESSION` tests should be deleted or rewritten around checkpoints; - final intended internal storage format for Active Memory after the flat-JSON boundary migration; -- exact final semantics/name for the UI `FRAME` label; - whether reveal debounce should now be restored from 333 ms to the earlier 250 ms preference; - final canonical list of supported noncanonical action marker aliases after compatibility cleanup; - which current full-suite failures are regressions versus intentionally stale tests; diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index 97e04948..4b5ada73 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -329,13 +329,15 @@ The existing JIN skeleton already has state, named prompt sections, dynamic cont --- -## D024 — `FRAME` is the preferred conceptual direction over generic `STATE`, but rename is not automatic +## D024 — `FRAME` names the live runtime-memory tab, not the runtime system -**Status:** Accepted / not fully implemented +**Status:** Accepted / implemented + +The first memory-panel tab is labelled `FRAME`. It presents the live runtime-memory snapshot and retains the existing snapshot/diff paging controls. -The owner prefers `FRAME` for the concrete question -> answer -> live-context cycle because `STATE` is too broad. +**Constraint:** this is a UI label with a narrow scope. Do not globally rename runtime/state concepts, identifiers, events, or storage fields to `FRAME`. -**Constraint:** do not perform a blind RUNTIME/STATE -> FRAME rename. First identify the exact UI label and its semantic scope. +The memory panel always exposes exactly five tabs: `FRAME`, `ACTIVE`, `DELAYED`, `L-T`, and `FILES`. The temporary unprocessed-facts view is not part of this tab bar. The shared count/paging control sits below the active tab; arrows are visible only for `FRAME`. --- diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index c095f0cc..ef6a853b 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -1085,26 +1085,25 @@ def build_previous_reasoning_loop_context( ): return "" - blocks = [] - - for reasoning in loop_reasonings: + # Keep this defensive too: even if older/stale state contains several + # entries, only the latest non-empty failed reasoning belongs in the + # current recovery prompt. + for reasoning in reversed( + loop_reasonings + ): if not str( reasoning or "" ).strip(): continue - blocks.append( - _format_previous_reasoning_context( - tag_name="PREVIOUS_REASONING_LOOP_CONTENT", - reasoning=reasoning, - edge_percent=PREVIOUS_REASONING_LOOP_EDGE_PERCENT, - ) + return _format_previous_reasoning_context( + tag_name="PREVIOUS_REASONING_LOOP_CONTENT", + reasoning=reasoning, + edge_percent=PREVIOUS_REASONING_LOOP_EDGE_PERCENT, ) - return "\n".join( - blocks - ) + return "" # Runtime action rules are assembled from contracts/rules_assembler.py. diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 9ffd834f..d51b8c8a 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -350,15 +350,6 @@ class RuntimeContext: default_factory=list ) - # Archived avatar state is replayed through the normal runtime-action - # pipeline after the hidden restore greeting, exactly like restored files - # and delayed memory. This keeps visual restoration visible as action bubbles - # instead of silently mutating the browser before JIN resumes. - runtime_session_restore_pending_jin_color: str = "" - runtime_session_restore_pending_jin_size: object | None = None - runtime_session_restore_pending_jin_position: object | None = None - runtime_session_restore_pending_jin_speed: int | None = None - runtime_memory_update_task: object | None = None fact_check_idle_task: object | None = None diff --git a/runtime/stream.py b/runtime/stream.py index 75cb867c..b27debfe 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -1485,7 +1485,7 @@ async def abort_duplicate_delayed_memory_save( record_session_action_history( self.context, ( - "SAVE_DELAYED_MEMORY - failed: " + "SAVE_DELAYED_MEMORY: failed - " f"{duplicate_title} " "(duplicate delayed memory title; potential loop blocked)" ), diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index 4ebd7507..dc794126 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -1026,11 +1026,6 @@ def test_restore_resource_replay_uses_real_runtime_action_dispatcher(self): "file001", "file001", ] - context.runtime_session_restore_pending_jin_color = "#9370db" - context.runtime_session_restore_pending_jin_size = { - "width": 333, - "height": 333, - } context.runtime_session_restore_reasoning_dump = "reasoning" context.runtime_session_restore_l4_fact_ids = ["F7"] context.runtime_session_restore_delayed_memory_metadata = [ @@ -1064,14 +1059,12 @@ async def fake_apply( ) ) - self.assertEqual(applied, 4) + self.assertEqual(applied, 2) self.assertEqual( [action.name for action in captured["actions"]], [ "LOAD_DELAYED_MEMORY", "ATTACH_FILE", - "JIN_COLOR", - "JIN_SIZE", ], ) self.assertEqual( @@ -1079,8 +1072,6 @@ async def fake_apply( [ "abc123", "file001", - "#9370db", - {"width": 333, "height": 333}, ], ) self.assertEqual( @@ -1100,13 +1091,6 @@ async def fake_apply( context.runtime_session_restore_pending_attached_file_ids, [], ) - self.assertEqual( - context.runtime_session_restore_pending_jin_color, - "", - ) - self.assertIsNone( - context.runtime_session_restore_pending_jin_size - ) self.assertEqual(context.runtime_session_restore_reasoning_dump, "") self.assertEqual(context.runtime_session_restore_l4_fact_ids, []) self.assertEqual( diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index 05e9f917..cece1670 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -1794,15 +1794,15 @@ async def fake_run_brain_stream(**kwargs): system_prompt.count( "" ), - 2, + 1, ) - self.assertLess( - system_prompt.index( - "first failed reasoning" - ), - system_prompt.index( - "second failed reasoning" - ), + self.assertNotIn( + "first failed reasoning", + system_prompt, + ) + self.assertIn( + "second failed reasoning", + system_prompt, ) context.runtime_turn_interrupted = False return ( diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index bff65403..4daf3be6 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -677,12 +677,10 @@ def test_previous_reasoning_loop_blocks_render_in_existing_slot(self): self.assertEqual( prompt.count(""), - 2, + 1, ) - self.assertIn( - ( - "---------------------------- CUTTED " - ), + self.assertNotIn( + "loop one opening", prompt, ) self.assertIn( diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index bbf39d21..aa10acb4 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -2873,6 +2873,15 @@ async def collect(context): lifecycle_events[1]["text"], "Saved delayed memory: Test delayed memory report", ) + self.assertEqual( + [ + item["text"] + for item in context.runtime_session_action_history + ], + [ + "Delayed memory saved: Test delayed memory report", + ], + ) def test_agent_runtime_action_flags_follow_assembler_constants(self): diff --git a/tests/test_runtime_memory_tabs_client_contract.py b/tests/test_runtime_memory_tabs_client_contract.py new file mode 100644 index 00000000..917db657 --- /dev/null +++ b/tests/test_runtime_memory_tabs_client_contract.py @@ -0,0 +1,78 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" +MEMORY_VIEW_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +) +MEMORY_CSS = ROOT / "ui" / "static" / "css" / "runtime-memory.css" + + +class RuntimeMemoryTabsClientContractTests(unittest.TestCase): + + def test_all_five_memory_tabs_are_always_present(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + expected_tabs = ( + ('data-runtime-memory-mode="runtime"', "FRAME"), + ('data-runtime-memory-mode="active"', "ACTIVE"), + ('data-runtime-memory-mode="delayed"', "DELAYED"), + ('data-runtime-memory-mode="long_term"', "L-T"), + ('data-runtime-memory-mode="files"', "FILES"), + ) + + for mode, label in expected_tabs: + self.assertIn(mode, source) + self.assertIn(f">{label}", source) + + self.assertNotIn('data-runtime-memory-mode="facts"', source) + + def test_tabs_replace_title_cycling_and_skip_unprocessed_facts(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + modes_start = source.index("const RUNTIME_MEMORY_DISPLAY_MODES = [") + modes_end = source.index("];", modes_start) + modes = source[modes_start:modes_end] + + self.assertNotIn('"facts"', modes) + self.assertNotIn("toggleRuntimeMemoryDisplayMode", source) + self.assertNotIn("isRuntimeMemoryTitleBackZone", source) + self.assertIn('tab.addEventListener("click"', source) + self.assertIn("updateRuntimeMemoryTabsState(availableModes);", source) + + def test_shared_counter_tracks_active_tab_and_frame_owns_arrows(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + css = MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn("runtimeMemoryNavigation.dataset.activeIndex", source) + self.assertIn("runtimeMemoryNavigation.dataset.activeMode", source) + self.assertIn("syncRuntimeMemoryNavigationGeometry(displayMode);", source) + self.assertIn("new ResizeObserver", source) + self.assertIn( + '.runtime-memory-navigation:not([data-active-mode="runtime"]) ' + "#runtime-memory-prev", + css, + ) + self.assertIn("display: flex;", css) + self.assertIn("flex: 0 0 auto;", css) + self.assertIn("min-width: max-content;", css) + self.assertIn("--runtime-memory-active-tab-left", css) + self.assertIn("--runtime-memory-active-tab-width", css) + self.assertIn( + '.runtime-memory-navigation[data-active-mode="runtime"] ' + ".runtime-memory-navigation-slot", + css, + ) + self.assertIn("justify-content: flex-start;", css) + self.assertIn( + "transform 0.18s cubic-bezier(0.22, 0.61, 0.36, 1)", + css, + ) + self.assertNotIn("grid-template-columns: repeat(5", css) + self.assertNotIn('width: 20%;', css) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_memory_timestamp_client_contract.py b/tests/test_runtime_memory_timestamp_client_contract.py index 529ea302..3447254e 100644 --- a/tests/test_runtime_memory_timestamp_client_contract.py +++ b/tests/test_runtime_memory_timestamp_client_contract.py @@ -24,7 +24,7 @@ def test_runtime_memory_header_stays_static(self): source, ) self.assertIn( - ': "[ runtime memory ]";', + 'const RUNTIME_MEMORY_DISPLAY_MODES = [', source, ) diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index 542203fe..474aac99 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -1940,9 +1940,17 @@ async def emit( 0, ) self.assertIn( - "SAVE_DELAYED_MEMORY - failed: Unrequested report", + "SAVE_DELAYED_MEMORY: failed - Unrequested report", context.runtime_session_action_history[-1]["text"], ) + self.assertEqual( + context.runtime_session_action_history[-1]["parts"][0]["text"], + "SAVE_DELAYED_MEMORY: failed", + ) + self.assertIn( + "Unrequested report", + context.runtime_session_action_history[-1]["parts"][0]["detail"], + ) followup_prompt = BrainNode.build_followup_system_prompt( "\n", "выполни другое действие", @@ -2857,7 +2865,7 @@ async def emit( ( "SAVE_ACTIVE_MEMORY - " "current session context and task status, " - "SAVE_DELAYED_MEMORY - failed: Unrequested report " + "SAVE_DELAYED_MEMORY: failed - Unrequested report " "(user did not provided system allowed trigger words for this action)" ), ) @@ -2906,9 +2914,9 @@ async def emit( "detail": "current session context and task status", }, { - "text": "SAVE_DELAYED_MEMORY", + "text": "SAVE_DELAYED_MEMORY: failed", "detail": ( - "failed: Unrequested report " + "Unrequested report " "(user did not provided system allowed trigger words for this action)" ), }, diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index f8859ca4..8d3ba9cd 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -324,19 +324,15 @@ def test_stream_validator_stops_repeated_short_word_sequence(): def test_stream_validator_stops_repeated_complex_symbolic_motif_in_mixed_reasoning(): validator = StreamValidator() - blocks = [ - "Wait, I'll do:\n```\n (😼) ⚡\n```\nLet's go.\n", - "*Final decision:*\n```\n (😼) ⚡\n```\nActually, I'll do it.\n", - "Let's inspect one more thing.\n```\n (😼) ⚡\n```\nThen continue.\n", - "*Actually, I'll do:*\n```\n (😼) ⚡\n```\nLet's go.\n", - ] - - assert len(blocks) == MAX_REPEAT_SYMBOLIC_MOTIFS - - for repeat_index, block in enumerate(blocks): - clean, is_valid = validator.filter_chunk( - block + for repeat_index in range(MAX_REPEAT_SYMBOLIC_MOTIFS): + block = ( + f"alpha{repeat_index} beta{repeat_index} gamma{repeat_index}\n" + "```\n" + " (😼) ⚡\n" + "```\n" + f"delta{repeat_index} epsilon{repeat_index} zeta{repeat_index}\n" ) + clean, is_valid = validator.filter_chunk(block) if repeat_index < MAX_REPEAT_SYMBOLIC_MOTIFS - 1: assert clean == block @@ -351,6 +347,88 @@ def test_stream_validator_stops_repeated_complex_symbolic_motif_in_mixed_reasoni ) assert validator.last_failure_loop_preview == "(😼) ⚡" +def test_stream_validator_stops_long_inline_symbolic_motif(): + validator = StreamValidator() + motif = "▙▟▛" + + # The real failure from the captured MHTML was one physical line with the + # same three-character motif repeated for hundreds of characters. Provider + # chunk boundaries must not hide that, but shorter intentional art should + # remain valid. + for repeat_index, chunk in enumerate([ + motif * 12, + motif * 12, + motif * 12, + ]): + clean, is_valid = validator.filter_chunk(chunk) + + if repeat_index < 2: + assert clean == chunk + assert is_valid + continue + + assert clean == "" + assert not is_valid + + assert validator.last_failure_reason == ( + "Repeated symbolic motif loop detected." + ) + assert validator.last_failure_loop_preview == motif + +def test_stream_validator_allows_spaced_geometric_art_across_uneven_rows(): + validator = StreamValidator() + art = ( + "```\n" + " ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤ ◢ ◤\n" + "```\n" + ) + + # A finite patterned drawing is allowed even though adjacent rows share a + # tiny visual motif. Newlines are intentional structure, not loop evidence. + text = collect( + validator, + [ + art[:47], + art[47:103], + art[103:], + ], + ) + + assert text == art + assert validator.last_failure_reason is None + assert validator.last_failure_loop_preview == "" + +def test_stream_validator_allows_short_spaced_geometric_art(): + validator = StreamValidator() + art = ( + "```\n" + "◢ ◤ ◢ ◤ ◢ ◤\n" + " ◢ ◤ ◢ ◤ ◢ ◤\n" + ) + + clean, is_valid = validator.filter_chunk(art) + + assert clean == art + assert is_valid + assert validator.last_failure_reason is None + + +def test_stream_validator_allows_long_single_symbol_art_row(): + validator = StreamValidator() + line = "█" * 160 + "\n" + + clean, is_valid = validator.filter_chunk(line) + + assert clean == line + assert is_valid + assert validator.last_failure_reason is None + def test_stream_validator_allows_repeated_ascii_art_rows(): validator = StreamValidator() diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index c78bf79a..3a04f9ba 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -8,6 +8,94 @@ user-select: none; } +.runtime-memory-tabs { + display: flex; + min-height: 22px; + align-items: center; + max-width: 100%; + overflow: hidden; + margin-left: -0.35rem; +} + +.runtime-memory-tab { + display: inline-flex; + flex: 0 0 auto; + min-width: max-content; + min-height: 20px; + align-items: center; + justify-content: center; + overflow: hidden; + padding: 4px 9px 4px; + border-right: 1px solid rgba(100,116,139,0.7); + opacity: 0.58; + text-align: center; + text-overflow: clip; + white-space: nowrap; + transition: + opacity 0.18s ease, + color 0.18s ease; +} + +.runtime-memory-tab:last-child { + border-right: 0; +} + +.runtime-memory-tab[aria-selected="true"] { + opacity: 1; + font-weight: 600; +} + +.runtime-memory-navigation { + --runtime-memory-active-tab-left: 0px; + --runtime-memory-active-tab-width: 0px; + position: relative; + height: 18px; +} + +.runtime-memory-navigation::before { + content: ""; + position: absolute; + top: 50%; + right: 0; + left: 0; + border-top: 1px solid rgba(100,116,139,0.7); +} + +.runtime-memory-navigation-slot { + position: absolute; + top: 0; + left: 0; + display: flex; + width: var(--runtime-memory-active-tab-width); + height: 18px; + align-items: center; + justify-content: center; + transform: translateX(var(--runtime-memory-active-tab-left)); + transition: + width 0.18s cubic-bezier(0.22, 0.61, 0.36, 1), + transform 0.18s cubic-bezier(0.22, 0.61, 0.36, 1); + will-change: transform; +} + +.runtime-memory-navigation[data-active-mode="runtime"] .runtime-memory-navigation-slot { + justify-content: flex-start; +} + +.runtime-memory-navigation[data-active-mode="runtime"] .runtime-memory-navigation-controls { + padding-left: 0; +} + +.runtime-memory-navigation-controls { + position: relative; + padding: 0 5px; + background: rgb(24,24,27); +} + +.runtime-memory-navigation:not([data-active-mode="runtime"]) #runtime-memory-prev, +.runtime-memory-navigation:not([data-active-mode="runtime"]) #runtime-memory-next { + display: none !important; +} + .runtime-memory-line { margin: 0 0 10px 0; line-height: 1.5; @@ -313,7 +401,7 @@ } #runtime-memory-position { - cursor: pointer; + cursor: default; border-radius: 9999px; padding: 0 3px; transition: @@ -322,13 +410,24 @@ background-color 0.25s ease; } -#runtime-memory-position:hover { +.runtime-memory-navigation[data-active-mode="runtime"] #runtime-memory-position { + cursor: pointer; +} + +.runtime-memory-navigation[data-active-mode="runtime"] #runtime-memory-position:hover { color: rgba(167,243,208,0.98); text-shadow: 0 0 8px rgba(16,185,129,0.35); } -#runtime-memory-position.runtime-memory-position-pinned { +.runtime-memory-navigation:not([data-active-mode="runtime"]) #runtime-memory-position { + pointer-events: none; + color: rgba(244,244,245,0.96); + background: transparent; + text-shadow: none; +} + +.runtime-memory-navigation[data-active-mode="runtime"] #runtime-memory-position.runtime-memory-position-pinned { color: rgba(134,239,172,0.98); background-color: rgba(16,185,129,0.15); text-shadow: @@ -353,19 +452,6 @@ font-weight: 600!important; } -#runtime-memory-title.runtime-memory-title-clickable { - cursor: pointer; - transition: - color 0.2s ease, - text-shadow 0.2s ease; -} - -#runtime-memory-title.runtime-memory-title-clickable:hover { - color: rgba(167,243,208,0.98); - text-shadow: - 0 0 8px rgba(16,185,129,0.35); -} - .delayed-memory-modal-panel { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index c3400fc9..3e76183e 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -379,7 +379,7 @@ body.theme-win95 #context-panel-model { } body.theme-win95 .runtime-memory-header { - border-bottom: 1px solid var(--win95-mid); + border-bottom: 0; font-size: 14px; font-weight: 500; letter-spacing: 0; @@ -387,6 +387,24 @@ body.theme-win95 .runtime-memory-header { text-transform: uppercase; } +body.theme-win95 .runtime-memory-tab { + border-right-color: var(--win95-mid); + padding:4px 11px 4px; +} + +body.theme-win95 .runtime-memory-tab[aria-selected="true"] { + font-weight: 700 !important; +} + +body.theme-win95 .runtime-memory-navigation::before { + border-top-color: var(--win95-mid); +} + +body.theme-win95 .runtime-memory-navigation-controls { + background: var(--win95-face); + gap: 0.1rem; +} + body.theme-win95 #runtime-memory-prev, body.theme-win95 #runtime-memory-next, body.theme-win95 #runtime-memory-position { @@ -413,18 +431,9 @@ body.theme-win95 #runtime-memory-next { line-height: 1; } -body.theme-win95 #runtime-memory-title.runtime-memory-title-clickable { - cursor: pointer; -} - -body.theme-win95 #runtime-memory-title.runtime-memory-title-clickable:hover { - color: #00824a; - text-shadow: none; -} - body.theme-win95 #runtime-memory-prev:hover, body.theme-win95 #runtime-memory-next:hover, -body.theme-win95 #runtime-memory-position:hover { +body.theme-win95 .runtime-memory-navigation[data-active-mode="runtime"] #runtime-memory-position:hover { background: var(--win95-face); border-top-color: var(--win95-light); border-left-color: var(--win95-light); diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index 5da7bc76..b12517f4 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -3574,6 +3574,7 @@ const consolePanel = document.getElementById("console-panel"); const memoryPanel = document.getElementById("memory-panel"); const memoryDragHandle = document.getElementById("memory-drag-handle"); +const memoryPanelDragSpacer = document.getElementById("memory-panel-drag-spacer"); const consoleStreamPlaceholder = document.createComment( "console-stream detached while console panel is collapsed" @@ -4975,7 +4976,7 @@ let memoryDragStartX = 0; let memoryDragStartY = 0; let memoryHasMoved = false; -memoryDragHandle.addEventListener("mousedown", (event) => { +function beginMemoryPanelDrag(event) { if (event.detail > 1) { return; } @@ -4994,7 +4995,16 @@ memoryDragHandle.addEventListener("mousedown", (event) => { memoryHasMoved = false; document.body.style.userSelect = "none"; -}); +} + +memoryDragHandle.addEventListener("mousedown", beginMemoryPanelDrag); + +if (memoryPanelDragSpacer) { + memoryPanelDragSpacer.addEventListener( + "mousedown", + beginMemoryPanelDrag + ); +} window.addEventListener("mousemove", (event) => { if (!isMemoryDragging) return; diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 3e4685da..3df0c34f 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -58,6 +58,7 @@ let memoryReferenceEventsBound = false; let runtimeMemorySortTransitionSequence = 0; let longTermMemoryAgeTimer = null; + let runtimeMemoryTabsResizeObserver = null; const pinnedRuntimeMemorySnapshotIndexes = new Set(); @@ -93,6 +94,14 @@ const runtimeMemoryTitle = document.getElementById("runtime-memory-title"); + const runtimeMemoryTabs = + Array.from( + document.querySelectorAll("[data-runtime-memory-mode]") + ); + + const runtimeMemoryNavigation = + document.getElementById("runtime-memory-navigation"); + const runtimeMemoryPosition = document.getElementById("runtime-memory-position"); @@ -137,6 +146,13 @@ const LONG_TERM_MEMORY_VALUE_DISPLAY_MAX_CHARS = 78; const FILES_MEMORY_LAZY_BATCH_SIZE = 50; const RUNTIME_MEMORY_LAZY_BOTTOM_THRESHOLD_PX = 160; + const RUNTIME_MEMORY_DISPLAY_MODES = [ + "runtime", + "active", + "delayed", + "long_term", + "files", + ]; let runtimeMemoryLazyMode = ""; let runtimeMemoryLazyTotalCount = 0; @@ -2159,126 +2175,8 @@ }); } - function hasActiveMemoryRecords() { - const records = - typeof getActiveMemoryRecords === "function" - ? getActiveMemoryRecords() - : []; - - return Array.isArray(records) && records.length > 0; - } - - function hasDelayedMemoryReportRecords() { - const reports = - typeof getDelayedMemoryReports === "function" - ? getDelayedMemoryReports() - : {}; - - return Boolean( - reports - && typeof reports === "object" - && !Array.isArray(reports) - && Object.values(reports).some(report => ( - report - && typeof report === "object" - && !Array.isArray(report) - )) - ); - } - - function hasFactsMemoryFieldRecords() { - const fields = - typeof getFactsMemoryFields === "function" - ? getFactsMemoryFields() - : {}; - - return Boolean( - fields - && typeof fields === "object" - && !Array.isArray(fields) - && Object.values(fields).some((field) => ( - field - && typeof field === "object" - && !Array.isArray(field) - && String(field.content || "").trim() - && String(field.l4_status || "pending") - .trim() - .toLocaleLowerCase() !== "analyzed" - )) - ); - } - - function hasLongTermMemoryFactRecords() { - const facts = - typeof getLongTermMemoryFacts === "function" - ? getLongTermMemoryFacts() - : []; - - return Boolean( - Array.isArray(facts) - && facts.some(fact => ( - fact - && typeof fact === "object" - && !Array.isArray(fact) - && String(fact.key || "").trim() - && String(fact.value || "").trim() - )) - ); - } - - function hasPersistentFileRecords() { - const records = - window.JinFiles - && typeof window.JinFiles.getFiles === "function" - ? window.JinFiles.getFiles() - : []; - - return Boolean( - Array.isArray(records) - && records.some(record => ( - record - && record.id - && record.name - )) - ); - } - function getAvailableRuntimeMemoryDisplayModes() { - const modes = [ - "runtime", - ]; - - if (hasActiveMemoryRecords()) { - modes.push( - "active" - ); - } - - if (hasDelayedMemoryReportRecords()) { - modes.push( - "delayed" - ); - } - - if (hasFactsMemoryFieldRecords()) { - modes.push( - "facts" - ); - } - - if (hasLongTermMemoryFactRecords()) { - modes.push( - "long_term" - ); - } - - if (hasPersistentFileRecords()) { - modes.push( - "files" - ); - } - - return modes; + return RUNTIME_MEMORY_DISPLAY_MODES.slice(); } function ensureRuntimeMemoryDisplayModeAvailable(availableModes = null) { @@ -2301,11 +2199,7 @@ return "runtime"; } - function updateRuntimeMemoryTitleState(availableModes = null) { - if (!runtimeMemoryTitle) { - return; - } - + function updateRuntimeMemoryTabsState(availableModes = null) { const modes = Array.isArray(availableModes) ? availableModes @@ -2319,49 +2213,106 @@ ? currentMode : "runtime"; - runtimeMemoryTitle.textContent = - displayMode === "active" - ? "[ active memory ]" - : displayMode === "delayed" - ? "[ delayed memory ]" - : displayMode === "facts" - ? "[ facts memory ]" - : displayMode === "long_term" - ? "[ long term memory ]" - : displayMode === "files" - ? "[ files ]" - : "[ runtime memory ]"; - - const hasAlternativeMemory = - modes.length > 1; - - runtimeMemoryTitle.classList.toggle( - "runtime-memory-title-clickable", - hasAlternativeMemory - ); - - if (hasAlternativeMemory) { - runtimeMemoryTitle.setAttribute( - "role", - "button" - ); + const activeIndex = Math.max( + 0, + RUNTIME_MEMORY_DISPLAY_MODES.indexOf(displayMode) + ); - runtimeMemoryTitle.setAttribute( - "tabindex", - "0" + runtimeMemoryTabs.forEach((tab) => { + const selected = + tab.dataset.runtimeMemoryMode === displayMode; + + tab.setAttribute( + "aria-selected", + selected ? "true" : "false" ); + tab.tabIndex = selected ? 0 : -1; + }); + + if (runtimeMemoryNavigation) { + runtimeMemoryNavigation.dataset.activeIndex = + String(activeIndex); + runtimeMemoryNavigation.dataset.activeMode = + displayMode; + } + + syncRuntimeMemoryNavigationGeometry(displayMode); + + if (runtimeMemoryPosition) { + if (displayMode === "runtime") { + runtimeMemoryPosition.setAttribute("role", "button"); + runtimeMemoryPosition.setAttribute("tabindex", "0"); + runtimeMemoryPosition.setAttribute( + "title", + "Toggle persistent runtime memory highlight" + ); + } else { + runtimeMemoryPosition.removeAttribute("role"); + runtimeMemoryPosition.removeAttribute("tabindex"); + runtimeMemoryPosition.removeAttribute("title"); + } + } + } + + function syncRuntimeMemoryNavigationGeometry(displayMode = null) { + if (!runtimeMemoryNavigation) { return; } - runtimeMemoryTitle.removeAttribute( - "role" - ); + const mode = + String(displayMode || getRuntimeMemoryDisplayMode()).trim(); + const activeTab = + runtimeMemoryTabs.find( + tab => tab.dataset.runtimeMemoryMode === mode + ) || runtimeMemoryTitle; + + if (!activeTab || !activeTab.isConnected) { + return; + } - runtimeMemoryTitle.removeAttribute( - "tabindex" + const navigationRect = + runtimeMemoryNavigation.getBoundingClientRect(); + const tabRect = + activeTab.getBoundingClientRect(); + + if ( + !Number.isFinite(navigationRect.left) + || !Number.isFinite(tabRect.left) + || tabRect.width <= 0 + ) { + return; + } + + runtimeMemoryNavigation.style.setProperty( + "--runtime-memory-active-tab-left", + `${Math.max(0, tabRect.left - navigationRect.left)}px` + ); + runtimeMemoryNavigation.style.setProperty( + "--runtime-memory-active-tab-width", + `${tabRect.width}px` ); } + function bindRuntimeMemoryTabsGeometryObserver() { + if ( + runtimeMemoryTabsResizeObserver + || !runtimeMemoryNavigation + || typeof ResizeObserver !== "function" + ) { + return; + } + + runtimeMemoryTabsResizeObserver = + new ResizeObserver(() => { + syncRuntimeMemoryNavigationGeometry(); + }); + + runtimeMemoryTabs.forEach((tab) => { + runtimeMemoryTabsResizeObserver.observe(tab); + }); + runtimeMemoryTabsResizeObserver.observe(runtimeMemoryNavigation); + } + function updateUserIdleTimerText( text = getUserIdleText() ) { @@ -2694,6 +2645,7 @@ window.addEventListener("resize", () => { hideLongTermMemoryHoverCard(); + syncRuntimeMemoryNavigationGeometry(); }); if (memoryScroll) { @@ -2850,8 +2802,18 @@ : 0; } + function getRuntimeMemoryMetricsTab() { + const displayMode = getRuntimeMemoryDisplayMode(); + + return runtimeMemoryTabs.find( + tab => tab.dataset.runtimeMemoryMode === displayMode + ) || runtimeMemoryTitle; + } + function bindRuntimeMemoryTitleMetrics(charCount) { - if (!runtimeMemoryTitle) { + const metricsTab = getRuntimeMemoryMetricsTab(); + + if (!metricsTab) { return; } @@ -2863,7 +2825,7 @@ ); bindRuntimeMemoryHoverTitle( - runtimeMemoryTitle, + metricsTab, `${normalizedCharCount} chars / ~${tokenCount} tokens` ); } @@ -2928,7 +2890,7 @@ } function updateRuntimeMemoryTitleMetrics(snapshot) { - if (!runtimeMemoryTitle) { + if (!getRuntimeMemoryMetricsTab()) { return; } @@ -2941,7 +2903,7 @@ } function updateRuntimeMemoryTitleMetricsFromText(text) { - if (!runtimeMemoryTitle) { + if (!getRuntimeMemoryMetricsTab()) { return; } @@ -2957,7 +2919,9 @@ items, resolveText ) { - if (!runtimeMemoryTitle) { + const metricsTab = getRuntimeMemoryMetricsTab(); + + if (!metricsTab) { return; } @@ -2969,7 +2933,7 @@ let cachedTitle = null; bindRuntimeMemoryHoverTitle( - runtimeMemoryTitle, + metricsTab, () => { if (cachedTitle !== null) { return cachedTitle; @@ -3120,7 +3084,7 @@ const displayMode = ensureRuntimeMemoryDisplayModeAvailable(availableModes); - updateRuntimeMemoryTitleState(availableModes); + updateRuntimeMemoryTabsState(availableModes); const renderHighlightOptions = { animateSort: false, @@ -8562,49 +8526,6 @@ runtimeMemoryNext.classList.toggle("text-slate-600", !canGoNext); } - function toggleRuntimeMemoryDisplayMode(direction = 1) { - const modes = - getAvailableRuntimeMemoryDisplayModes(); - - if (modes.length <= 1) { - return; - } - - const currentMode = - getRuntimeMemoryDisplayMode(); - - const currentIndex = - modes.indexOf(currentMode); - - const step = direction < 0 ? -1 : 1; - const nextIndex = - (currentIndex + step + modes.length) % modes.length; - - setRuntimeMemoryDisplayMode( - modes[nextIndex] - ); - - renderRuntimeMemorySnapshot({ - availableModes: modes, - }); - } - - function isRuntimeMemoryTitleBackZone(event) { - if (!runtimeMemoryTitle) { - return false; - } - - const rect = runtimeMemoryTitle.getBoundingClientRect(); - const fontSize = parseFloat( - window.getComputedStyle(runtimeMemoryTitle).fontSize - ) || 11; - - // Opening bracket acts as the backward navigation zone for every memory title. - const backZoneWidth = Math.max(12, fontSize * 1.35); - - return (event.clientX - rect.left) <= backZoneWidth; - } - function bindRuntimeMemoryNavigation() { if (initialized) { return; @@ -8698,22 +8619,23 @@ runtimeMemoryPosition.click(); }); - runtimeMemoryTitle?.addEventListener("click", (event) => { - toggleRuntimeMemoryDisplayMode( - isRuntimeMemoryTitleBackZone(event) ? -1 : 1 - ); - }); + runtimeMemoryTabs.forEach((tab) => { + tab.addEventListener("click", () => { + const displayMode = + String(tab.dataset.runtimeMemoryMode || "").trim(); - runtimeMemoryTitle?.addEventListener("keydown", (event) => { - if ( - event.key !== "Enter" - && event.key !== " " - ) { - return; - } + if ( + !RUNTIME_MEMORY_DISPLAY_MODES.includes(displayMode) + || displayMode === getRuntimeMemoryDisplayMode() + ) { + return; + } - event.preventDefault(); - toggleRuntimeMemoryDisplayMode(); + setRuntimeMemoryDisplayMode(displayMode); + renderRuntimeMemorySnapshot({ + availableModes: getAvailableRuntimeMemoryDisplayModes(), + }); + }); }); runtimeDiffToggle?.addEventListener("click", () => { @@ -8799,6 +8721,7 @@ bindRuntimeMemoryLazyScroll(); bindRuntimeMemoryNavigation(); + bindRuntimeMemoryTabsGeometryObserver(); renderRuntimeMemorySnapshot(); renderRuntimeDiffs(); } diff --git a/ui/templates/index.html b/ui/templates/index.html index de390613..4ddad0e8 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,7 +14,7 @@ - + @@ -336,7 +336,7 @@
      + class="memory-scroll flex flex-1 flex-col overflow-y-auto p-4 space-y-4 text-xs text-zinc-300 leading-relaxed">
      @@ -448,24 +448,38 @@
      + class="runtime-memory-header text-[11px] uppercase tracking-widest text-slate-100"> - - [ runtime memory ] - +
      + + + + + +
      - - - 0 - - +
      + + + + 0 + + + +
      @@ -477,6 +491,11 @@
      + +
      @@ -508,13 +527,13 @@ - + - + diff --git a/utils/context/session_actions.py b/utils/context/session_actions.py index 2663b8ea..78c3bd1d 100644 --- a/utils/context/session_actions.py +++ b/utils/context/session_actions.py @@ -266,6 +266,16 @@ def _format_session_action_context_parts( ) continue + if ( + part_text.upper() + == "SAVE_DELAYED_MEMORY: FAILED" + and part_detail + ): + context_parts.append( + f"{part_text}: {part_detail}" + ) + continue + context_detail = str( part.get( "context_detail", diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index bf09e8c2..8407fe7b 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1178,13 +1178,15 @@ def build_delayed_memory_save_rejected_history_text( or "" ).strip() - text = "SAVE_DELAYED_MEMORY - failed" - - if normalized_title: - text = f"{text}: {normalized_title}" + detail = ( + f"{normalized_title} " + if normalized_title + else "" + ) return ( - f"{text} " + "SAVE_DELAYED_MEMORY: failed - " + f"{detail}" "(user did not provided system allowed trigger words for this action)" ) diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 49e7f22f..8d77215a 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -100,7 +100,7 @@ def configured_int( ) MAX_REPEAT_SYMBOLIC_MOTIFS = configured_int( "STREAM_VALIDATOR_MAX_REPEAT_SYMBOLIC_MOTIFS", - 4, + 8, minimum=3, zero_disables=True, ) @@ -109,6 +109,13 @@ def configured_int( 48, minimum=8, ) +# Inline symbol degeneration is deliberately conservative. A finite geometric +# drawing may repeat the same visual pattern for several rows, so line breaks +# are hard boundaries here. Only a long low-period run inside one physical line +# counts as strong evidence of a loop. +INLINE_SYMBOLIC_LOOP_MIN_CHARS = 96 +INLINE_SYMBOLIC_LOOP_MAX_MOTIF_SIZE = 16 +INLINE_SYMBOLIC_LOOP_MIN_REPETITIONS = 8 MAX_SENTENCE_LOOP_SEQUENCE_SIZE = configured_int( "STREAM_VALIDATOR_MAX_SENTENCE_LOOP_SEQUENCE_SIZE", 16, @@ -779,6 +786,82 @@ def validate_l4_fact_ids( # VALIDATE SYMBOLIC / EMOJI MOTIF LOOPS # ----------------------------------------------------- + @staticmethod + def find_inline_symbolic_loop( + line: str, + ) -> tuple[str, str]: + """Return (motif, repeated_tail) for an obvious one-line symbol loop.""" + + line = str(line or "").rstrip("\r") + + runs = [] + current_run = [] + + for char in line: + category = unicodedata.category(char) + + # Layout spacing and lightweight markdown wrappers do not change a + # visual motif, but a physical newline is never present here: the + # caller checks one completed/current line at a time. + if char.isspace() or char in "`*_~": + continue + + if category[:1] in {"P", "S"}: + current_run.append(char) + continue + + if category in {"Cf", "Mn", "Me"}: + continue + + if current_run: + runs.append("".join(current_run)) + current_run = [] + + if current_run: + runs.append("".join(current_run)) + + for run in reversed(runs): + if len(run) < INLINE_SYMBOLIC_LOOP_MIN_CHARS: + continue + + max_motif_size = min( + INLINE_SYMBOLIC_LOOP_MAX_MOTIF_SIZE, + len(run) // INLINE_SYMBOLIC_LOOP_MIN_REPETITIONS, + ) + + for motif_size in range(2, max_motif_size + 1): + motif = run[-motif_size:] + + # A solid bar / divider is common intentional ASCII art. + # The failure we care about has an actual repeating pattern. + if len(set(motif)) < 2: + continue + + repetitions = 0 + offset = len(run) + + while ( + offset >= motif_size + and run[offset - motif_size:offset] == motif + ): + repetitions += 1 + offset -= motif_size + + repeated_length = repetitions * motif_size + + if ( + repetitions < INLINE_SYMBOLIC_LOOP_MIN_REPETITIONS + or repeated_length < INLINE_SYMBOLIC_LOOP_MIN_CHARS + ): + continue + + return ( + motif, + run[len(run) - repeated_length:], + ) + + return "", "" + @staticmethod def normalize_symbolic_motif( line: str, @@ -797,9 +880,10 @@ def normalize_symbolic_motif( return "" # Pure ASCII art commonly repeats structural rows (pipes, - # slashes, underscores, etc.) on purpose. Keep this visual - # guard scoped to non-ASCII symbolic motifs so ASCII layouts - # cannot be mistaken for a reasoning loop. + # slashes, underscores, etc.) on purpose. Keep the cross-line + # motif guard scoped to non-ASCII symbols. Extremely long + # low-period runs inside one line are handled separately by + # ``find_inline_symbolic_loop``. if stripped.isascii(): return "" @@ -868,6 +952,33 @@ def validate_symbolic_motif_loops( complete_lines = lines[:-1] self.symbolic_line_fragment = lines[-1] + # Do not collapse separate rows into one symbol stream. Repeated rows + # are valid structure in ASCII/Unicode art; only an obviously runaway + # low-period sequence inside one physical line is rejected here. + inline_lines = list(complete_lines) + if self.symbolic_line_fragment: + inline_lines.append(self.symbolic_line_fragment) + + for raw_line in inline_lines: + inline_motif, repeated_tail = self.find_inline_symbolic_loop( + raw_line + ) + + if not inline_motif: + continue + + self.last_failure_reason = ( + "Repeated symbolic motif loop detected." + ) + self.last_failure_preview = build_preview( + repeated_tail + ) + self.last_failure_loop_preview = build_loop_preview( + inline_motif + ) + + return False + for raw_line in complete_lines: self.symbolic_line_index += 1 diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 70870694..8efbc355 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -36,7 +36,6 @@ is_active_memory_key, is_delayed_memory_report_id, normalize_jin_color_payload, - normalize_jin_speed_value, refresh_active_memory_runtime_metadata, remove_active_memory_entries, ) @@ -511,16 +510,8 @@ def _clean_restore_metadata(field_name: str) -> list[dict]: context, message_data, ) - stage_session_restore_visual_state( - context, - message_data, - ) else: context.runtime_session_restore_pending_attached_file_ids = [] - context.runtime_session_restore_pending_jin_color = "" - context.runtime_session_restore_pending_jin_size = None - context.runtime_session_restore_pending_jin_position = None - context.runtime_session_restore_pending_jin_speed = None attached_file_ids = [ str(file_id or "").strip() for file_id in message_data.get( @@ -674,34 +665,6 @@ def apply_loaded_delayed_memory_ids( -def stage_session_restore_visual_state( - context, - message_data: dict, -) -> None: - - color = clean_bootstrap_memory( - message_data.get("current_jin_color", ""), - limit=32, - ).strip() - size = message_data.get("current_jin_size") - position = message_data.get("current_jin_position") - speed = normalize_jin_speed_value( - message_data.get("current_jin_speed") - ) - - context.runtime_session_restore_pending_jin_color = color - context.runtime_session_restore_pending_jin_size = ( - dict(size) - if isinstance(size, dict) - else None - ) - context.runtime_session_restore_pending_jin_position = ( - dict(position) - if isinstance(position, dict) - else None - ) - context.runtime_session_restore_pending_jin_speed = speed - def stage_session_restore_attached_file_ids( context, message_data: dict, From 0bc2cd7432b478a15966966fb1a98b1706c86c03 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:04:13 +0300 Subject: [PATCH 109/168] - fix for ascii drift loop --- tests/test_stream_validator.py | 47 ++++++++++++++ utils/stream_validator.py | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index 8d3ba9cd..0cd2317a 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -451,6 +451,53 @@ def test_stream_validator_allows_repeated_ascii_art_rows(): assert validator.last_failure_loop_preview == "" +def test_stream_validator_stops_runaway_ascii_diagonal_drift(): + validator = StreamValidator() + row = "\\ \\" + + for line_index in range( + stream_validator_module.ASCII_DRIFT_LOOP_MIN_LINES + ): + chunk = " " * line_index + row + "\n" + clean, is_valid = validator.filter_chunk(chunk) + + if line_index < stream_validator_module.ASCII_DRIFT_LOOP_MIN_LINES - 1: + assert clean == chunk + assert is_valid + continue + + assert clean == "" + assert not is_valid + + assert validator.last_failure_reason == ( + "Repeated symbolic motif loop detected." + ) + assert validator.last_failure_loop_preview == row + + +def test_stream_validator_allows_finite_ascii_diagonal_drift(): + validator = StreamValidator() + row = "\\ \\" + line_count = stream_validator_module.ASCII_DRIFT_LOOP_MIN_LINES - 4 + art = "".join( + " " * line_index + row + "\n" + for line_index in range(line_count) + ) + + text = collect( + validator, + [ + art[:73], + art[73:211], + art[211:], + ], + ) + + assert text == art + assert validator.last_failure_reason is None + assert validator.last_failure_loop_preview == "" + + def test_stream_validator_allows_bare_two_emoji_lines_even_when_repeated(): validator = StreamValidator() line = "😂 ⚡\n" diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 8d77215a..6fe4dbcd 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -116,6 +116,10 @@ def configured_int( INLINE_SYMBOLIC_LOOP_MIN_CHARS = 96 INLINE_SYMBOLIC_LOOP_MAX_MOTIF_SIZE = 16 INLINE_SYMBOLIC_LOOP_MIN_REPETITIONS = 8 +# Same symbol-only ASCII row drifting one column per newline is a distinct +# runaway shape. Keep this deliberately high so finite diagonals stay valid. +ASCII_DRIFT_LOOP_MIN_LINES = 24 +ASCII_DRIFT_LOOP_MIN_BODY_WIDTH = 12 MAX_SENTENCE_LOOP_SEQUENCE_SIZE = configured_int( "STREAM_VALIDATOR_MAX_SENTENCE_LOOP_SEQUENCE_SIZE", 16, @@ -261,6 +265,7 @@ def __init__( self.symbolic_line_fragment = "" self.symbolic_line_index = 0 self.symbolic_motif_history = [] + self.ascii_drift_history = [] self.validation_marker_buffer = "" self.validation_excluded_block_name = "" @@ -935,6 +940,111 @@ def normalize_symbolic_motif( ) ) + @staticmethod + def get_ascii_drift_candidate( + line: str, + ) -> tuple[int, str] | None: + line = str(line or "").rstrip("\r") + stripped = line.strip(" ") + + if ( + not stripped + or "\t" in line + or not line.isascii() + or len(stripped) < ASCII_DRIFT_LOOP_MIN_BODY_WIDTH + or any(char.isalnum() for char in stripped) + ): + return None + + visible = [ + char + for char in stripped + if not char.isspace() + ] + + if ( + len(visible) < 2 + or any( + not ( + unicodedata.category(char).startswith("P") + or unicodedata.category(char).startswith("S") + ) + for char in visible + ) + ): + return None + + return ( + len(line) - len(line.lstrip(" ")), + stripped, + ) + + def validate_ascii_drift_line( + self, + line: str, + ) -> bool: + candidate = self.get_ascii_drift_candidate( + line + ) + + if candidate is None: + self.ascii_drift_history = [] + return True + + indent, body = candidate + + if ( + self.ascii_drift_history + and self.ascii_drift_history[-1][1] != body + ): + self.ascii_drift_history = [] + + self.ascii_drift_history.append(( + indent, + body, + line.rstrip("\r"), + )) + self.ascii_drift_history = self.ascii_drift_history[ + -ASCII_DRIFT_LOOP_MIN_LINES: + ] + + if len(self.ascii_drift_history) < ASCII_DRIFT_LOOP_MIN_LINES: + return True + + step = ( + self.ascii_drift_history[1][0] + - self.ascii_drift_history[0][0] + ) + + if ( + abs(step) != 1 + or any( + current[0] - previous[0] != step + for previous, current in zip( + self.ascii_drift_history, + self.ascii_drift_history[1:], + ) + ) + ): + return True + + preview = "\n".join( + item[2] + for item in self.ascii_drift_history + ) + + self.last_failure_reason = ( + "Repeated symbolic motif loop detected." + ) + self.last_failure_preview = build_preview( + preview + ) + self.last_failure_loop_preview = build_loop_preview( + body + ) + + return False + def validate_symbolic_motif_loops( self, chunk: str, @@ -980,6 +1090,11 @@ def validate_symbolic_motif_loops( return False for raw_line in complete_lines: + if not self.validate_ascii_drift_line( + raw_line + ): + return False + self.symbolic_line_index += 1 line = raw_line.rstrip("\r") From 939d10287d8ddb9e1f39064ebc4b837f474f2d18 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:02:10 +0300 Subject: [PATCH 110/168] chore: runtime storage v2 - update runtime storage flow - cleanup redundant logic --- AGENTS.md | 6 +- README.md | 9 +- app.py | 22 - contracts/save_active_memory.json | 2 +- docs/JIN_ARCHITECTURE.md | 13 +- docs/JIN_CURRENT_STATE.md | 22 +- docs/JIN_DECISIONS.md | 18 + saved_runtime.example.txt | 7 - tests/test_anonymous_mode_client_contract.py | 5 +- tests/test_archived_session_restore.py | 2 +- ...trap_latest_session_fix_client_contract.py | 24 +- tests/test_browser_runtime_storage_v2.py | 350 ++++ ...l_results_bootstrap_checkpoint_contract.py | 3 +- tests/test_live_session_checkpoint.py | 23 +- ...ssion_bootstrap_lineage_client_contract.py | 32 +- ui/static/js/logger/log-entries.js | 37 +- ui/static/js/logger/logger.js | 12 +- ui/static/js/runtime/runtime-session.js | 297 +--- ui/static/js/runtime/runtime-storage.js | 1412 +++++++---------- ui/static/js/runtime/runtime.js | 8 - ui/static/js/session-restore.js | 84 +- ui/static/js/socket.js | 16 - ui/static/js/socket/input.js | 13 +- ui/static/js/socket/memory.js | 95 -- ui/templates/index.html | 18 +- 25 files changed, 1155 insertions(+), 1375 deletions(-) delete mode 100644 saved_runtime.example.txt create mode 100644 tests/test_browser_runtime_storage_v2.py diff --git a/AGENTS.md b/AGENTS.md index 731e1b27..1baa5f98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,9 @@ A current implementation can still violate a product decision. Do not hide that. - Durable facts belong in L4. Do not reintroduce a durable-L1/L2/L3 memory hierarchy. - Active Memory, Delayed Memory, L4 facts, persistent Files, live L1/runtime memory, and session checkpoints are different systems with different lifetimes. Do not collapse them into one generic memory store. - Session continuity must distinguish a real USER move, a completed turn, an interrupted USER-only turn, an action-only completion, and a blank bootstrap tab. A real USER row can become the newest conversation move without a visible JIN row; a blank tab cannot. -- The common browser checkpoint names the last runtime session that actually moved. Opening a tab may clone inherited L1 under a fresh runtime ID, but must not promote that ID until real user activity or a server-confirmed completed-turn commit. +- Normal browser continuity has exactly two runtime stores: ephemeral `sessionStorage` key `jin.liveRuntimeMemory.v2` for the current page's soft reconnect, and atomic `localStorage` key `jin.sessionCheckpoint.v2` for reload/new-tab bootstrap. Do not add per-session runtime records or freshness scans. +- The common browser checkpoint names the last runtime session that actually moved. Opening a tab may hydrate inherited L1 into the ephemeral live record, but must not promote the fresh runtime ID until real user activity or a server-confirmed completed-turn commit. +- Session CLEAR writes a `state: "cleared"` tombstone. Passive runtime, room, color, bootstrap, reconnect, and retry paths must not replace it; only a successfully emitted new USER move may authorize a later checkpoint write. - Preserve checkpoint lineage: `session_id`, `previous_session_id`, `booted_from_session_id`, nested runtime-snapshot origin, and `conversation_committed_at` have different meanings. Room/avatar field writes must not switch the checkpoint owner. - Browser checkpoint `saved_at` is a causal freshness boundary for runtime/resource archive enrichment, not a generic "last touched" timestamp. Dialogue freshness is compared tail-to-tail independently. A field-local mutation must not advance `saved_at` unless the whole checkpoint is genuinely refreshed. - In predecessor bootstrap, an explicitly present `tool_results: []` is an authoritative cleared value. Do **not** generalize that tombstone rule to `loaded_memory_ids` or `active_memory_records`; their empty browser values still retain archive-fallback semantics. @@ -108,7 +110,7 @@ At minimum, run the checks that apply to the change: - serialize -> reload -> hydrate round trip for restore/state changes; - explicit-empty-vs-missing tests for bootstrap fields, plus checkpoint timestamp/lineage invariance for field-only cleanup; - blank-tab vs completed vs interrupted USER-only vs action-only latest-session selection, separately for normal and anonymous log roots; -- dialogue-tail freshness independent of runtime `saved_at`, and matching-session L1 selection for the common checkpoint; +- dialogue-tail freshness independent of runtime `saved_at`, plus v1-to-v2 exact-owner migration, orphan rejection, write-failure preservation, and cleared-tombstone multi-tab protection; - structured session-action metadata round trip (including JIN_COLOR swatch/hex hover); - JIN_COLOR server-context -> raw runtime log -> common checkpoint -> local early apply -> one server reconciliation round trip, including stale-source replacement and repeated reload; - full-text recent-message context tests beyond the former character cap, including physical-newline escaping and the three-pair limit; diff --git a/README.md b/README.md index 45172038..44f6acaa 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,12 @@ On Windows, the LM Studio launcher can fill unset/default model ids from a loade JIN stores persistent runtime state locally through: -* browser storage for resumable session-facing state, Active Memory, and explicit save/bootstrap data; +* `jin.liveRuntimeMemory.v2` in `sessionStorage` for the current page's soft-reconnect FRAME; +* one atomic `jin.sessionCheckpoint.v2` in `localStorage` for new-tab/reload continuity; * `memory/delayed/*.json` for Delayed Memory reports; * `memory/facts/long_term_facts.json` for the canonical L4 store; * `assets/files/` plus its local index for persistent uploaded files; -* `logs/` for chat and per-turn reasoning logs; -* optional `saved_runtime.txt` runtime/session fallback data. +* `logs/` for chat and per-turn reasoning logs. Model and search traffic goes to the endpoints and providers configured for the runtime. @@ -179,8 +179,7 @@ JIN can inspect available skills, attach the one required for the current task, |-- requirements.txt # Python dependencies |-- package.json # Test and probe commands |-- ARCHITECTURE.md # Runtime ownership, data flow, persistence -|-- LIVE_AVATAR.md # Avatar visual-state contract -`-- saved_runtime.example.txt # Optional runtime/session fallback template +`-- LIVE_AVATAR.md # Avatar visual-state contract ``` ## Setup and Quick Start diff --git a/app.py b/app.py index 36091867..d2619d74 100644 --- a/app.py +++ b/app.py @@ -11,9 +11,7 @@ ) from fastapi.responses import ( - FileResponse, HTMLResponse, - Response, ) from fastapi.staticfiles import ( @@ -281,26 +279,6 @@ async def api_preview_file(file_id: str): return payload -@app.get( - "/saved_runtime.txt", -) -async def saved_runtime_file(): - - saved_runtime_path = Path( - "saved_runtime.txt" - ) - - if not saved_runtime_path.is_file(): - return Response( - status_code=404 - ) - - return FileResponse( - saved_runtime_path, - media_type="text/plain; charset=utf-8", - ) - - # --------------------------------------------------------- # INDEX PAGE # --------------------------------------------------------- diff --git a/contracts/save_active_memory.json b/contracts/save_active_memory.json index f5d9d7f4..18d5a713 100644 --- a/contracts/save_active_memory.json +++ b/contracts/save_active_memory.json @@ -17,7 +17,7 @@ "Avoid duplicating with long term facts, basic rules or another active memories.", "Emit using exactly this schema:", "", - "{\"conditions\":\"\",\"custom_field_name\":\"\"}", + "{\"conditions\":\"Descriptive conditions text\",\"custom_field\":\"Custom field value\"}", "", "conditions is required and must be non-empty." ], diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index 35bed7b7..fa0b4ff1 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -393,7 +393,8 @@ JIN deliberately splits persistence by owner/lifetime. | State | Current storage/owner | | --- | --- | -| live runtime checkpoint / browser-facing session state | browser storage via runtime/session modules | +| live runtime FRAME for soft reconnect | `sessionStorage`: `jin.liveRuntimeMemory.v2` | +| atomic browser session checkpoint | `localStorage`: `jin.sessionCheckpoint.v2` | | Active Memory | browser runtime storage; anonymous mode can isolate it | | Facts Memory candidate buckets | browser storage per facts-memory session ID | | Delayed Memory | `memory/delayed/*.json` | @@ -410,9 +411,13 @@ JIN deliberately splits persistence by owner/lifetime. `ui/static/js/runtime/runtime-session.js` persists live checkpoints and sends `session_bootstrap` data when appropriate. The backend normalizes and hydrates the browser-provided snapshot in `websocket/bootstrap.py`. -The browser uses one common checkpoint to name the last runtime session that actually moved. Merely opening/reloading a tab can clone inherited L1 into a fresh per-session record and record `booted_from_session_id`, but it does not promote that fresh runtime ID to the common conversation checkpoint. A real user send marks session activity immediately; a later server `completed_turn_commit` is the fallback commit signal. `conversation_committed_at` advances only for a completed turn, so it remains distinct from USER-only session movement. +Browser runtime continuity has two intentionally different lifetimes. `jin.liveRuntimeMemory.v2` exists only in the current page's `sessionStorage` and supports soft WebSocket reconnect. The module clears that key on page execution, so reload and new-tab bootstrap cannot inherit a copied live FRAME. `jin.sessionCheckpoint.v2` is the single durable, atomic `localStorage` record containing lineage, runtime memory and update count, runtime snapshot, and `session_snapshot`. -Per-session runtime records are candidates for the checkpoint's L1 payload only when their `session_id` matches the common checkpoint. Nested runtime snapshots preserve their own origin session ID; lineage is not rewritten merely because the containing browser record belongs to the new tab. +The atomic checkpoint names the last runtime session that actually moved. Merely opening/reloading a tab hydrates inherited L1 into the fresh page's ephemeral live record and records its source lineage, but it does not promote the fresh runtime ID. A successfully emitted real USER message marks session activity immediately; a later server `completed_turn_commit` is the fallback commit signal. Retry, bootstrap, reconnect, and passive UI/runtime events do not mark activity. `conversation_committed_at` advances only for a completed turn, so it remains distinct from USER-only session movement. Nested runtime snapshots preserve their own origin session ID. + +Session CLEAR replaces the durable record with a version-2 `state: "cleared"` tombstone and clears the current page's live FRAME plus all legacy runtime keys. Other already-open tabs cannot passively resurrect state. The first later checkpoint is allowed only after a successful new USER send; it carries the clear boundary forward so a still-older tab cannot overwrite the new owner after the tombstone has been replaced. + +One-time migration runs only after normal/anonymous storage isolation is known. A legacy common snapshot owns selection and may join only a matching `latestSavedRuntimeMemory` or the exact per-session runtime key. Without a common snapshot, only the self-contained `latestSavedRuntimeMemory` may migrate. Orphan per-session keys become a cleared tombstone and are never ranked by freshness. Legacy keys are removed only after the v2 write succeeds; failed writes preserve them for retry. Anonymous mode neither reads nor migrates the normal-profile checkpoint. The server sends a normalized `session_snapshot` at visible `message_end` and again at `agent_runtime_end`. The client merges the current room state into that snapshot and persists it before finishing the visible bubble. Completed state and room/avatar state therefore land as one checkpoint rather than racing through separate full writers. @@ -425,7 +430,7 @@ Bootstrap uses two freshness clocks: - dialogue/reasoning/counters compare the browser recent-turn tail to the archive recent-turn tail; - runtime/resource fields still use checkpoint `saved_at` against archive tail time. -This separation prevents a harmless fresh-tab clone or room-state write from pinning dialogue to an older turn. `saved_at` must still represent a whole-checkpoint refresh, not an incidental field mutation. +This separation prevents a harmless fresh-page hydration or room-state write from pinning dialogue to an older turn. `saved_at` must still represent a whole-checkpoint refresh, not an incidental field mutation. Session actions are merged by stable ID when available, otherwise by structured identity, sorted by real `created_at`, and bounded. For an unchanged source session the common checkpoint owns actions at or before `saved_at`; raw logs may append only a newer tail. If raw dialogue proves that another source session has a strictly newer USER move, the stale browser source, its actions, and its color are discarded together. diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index 30a2ea09..a10991e0 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -1,8 +1,8 @@ # JIN Core Engine — Current State / Migration Notes -**Snapshot inspected:** `jin_core(20260826-090339).zip`
      -**Inspection date:** 2026-08-26
      -**Context reference:** current source/tests plus the accumulated 2026-08-23--26 project decisions +**Snapshot inspected:** `jin_core(20260828-060531).zip`
      +**Inspection date:** 2026-08-28
      +**Context reference:** current source/tests plus the accumulated 2026-08-23--28 project decisions This is the document to read before touching transitional code. It lists what is true in the inspected snapshot, what is legacy residue, and where product intent and implementation currently differ. @@ -24,7 +24,9 @@ The runtime is already on the post-L2/L3 architecture in the actual source tree, - normal bootstrap restores a bounded three-USER-move chat tail plus a current-session divider, including interrupted/action-only USER-only rows; - Brain recent-message context keeps the newest three pairs in full, with newline/XML normalization but no per-message character crop; - ordinary Brain turns again include the previous successful reasoning block with explicit middle-crop semantics, while follow-ups keep their dedicated reasoning context; -- common-checkpoint ownership, per-session L1 lineage, completed-turn time, and dialogue freshness are now explicitly separated; +- browser continuity now uses page-ephemeral `jin.liveRuntimeMemory.v2` plus one atomic `jin.sessionCheckpoint.v2`; legacy per-session FRAME selection is migration-only and never freshness-scanned; +- common-checkpoint ownership, L1 origin lineage, completed-turn time, and dialogue freshness are now explicitly separated; +- Session CLEAR is a durable tombstone that blocks passive resurrection across already-open tabs until a new USER message is successfully sent; - JIN color now round-trips through server context, raw runtime events, the common checkpoint, early local room restore, and one server reconciliation without a parallel color store; - the full suite is still not certified green; the 2026-08-26 documentation environment also lacks required `httpx`/`fastapi` dependencies. @@ -264,11 +266,15 @@ Any prompt-order change can alter behavior materially. Do not reorder sections f ### 9.1 Common checkpoint and lineage -The browser common checkpoint is the last session that actually moved, not the most recently opened runtime ID. `applyPersistedSessionBootstrap()` may clone inherited L1 into the current per-session storage record and set `booted_from_session_id`, but it deliberately does not advance the common checkpoint on page load. +The browser common checkpoint is the last session that actually moved, not the most recently opened runtime ID. It is one atomic `localStorage` record at `jin.sessionCheckpoint.v2`. `applyPersistedSessionBootstrap()` hydrates inherited L1 only into `jin.liveRuntimeMemory.v2` in the current page's `sessionStorage`; it does not create a durable per-session record or advance the common checkpoint on page load. The live key is cleared whenever the page module executes, so only soft reconnect inside the same running page can reuse it. A user send marks the session dirty immediately. The next live-checkpoint write can promote that session even if generation is stopped before a visible answer. `completed_turn_commit` is a server fallback and separately advances `conversation_committed_at`; that timestamp is not the definition of every session move. -Per-session L1/runtime candidates are used only when their `session_id` matches the common checkpoint. `runtime_snapshot.session_id` preserves the snapshot's origin rather than being rewritten to the container/current runtime ID. Room/avatar persistence normally rejects cross-session writes and never changes checkpoint lineage. +There is no normal per-session L1 candidate scan. `runtime_snapshot.session_id` preserves the FRAME's origin rather than being rewritten to the current runtime ID. Room/avatar persistence normally rejects cross-session writes and never changes checkpoint lineage. + +Session CLEAR writes `{version: 2, state: "cleared", cleared_at}` rather than merely deleting the key. Passive writers in any already-open tab remain blocked. Only a successfully emitted real USER message marks that page eligible to write the next checkpoint; retry, bootstrap, reconnect, and passive events do not. The first post-clear checkpoint retains a clear barrier so a pre-clear tab cannot overwrite the new owner later. + +Migration is one-time and normal-profile-only. A legacy common snapshot selects the owner; it may join a matching saved-runtime record or only that owner's exact per-session record. Without a common snapshot, only the self-contained saved-runtime record may migrate. Orphan per-session keys create a tombstone instead of restoring. Cleanup happens only after a successful v2 write, so quota/write failures preserve legacy data for retry. The server supplies `session_snapshot` on `message_end` and `agent_runtime_end`. The browser adds current room/avatar state and persists before finishing the visible message bubble, so completed turn data and room state are committed through one common snapshot path. @@ -457,7 +463,7 @@ Command run: python -m unittest discover -s tests ``` -Result in the 2026-08-26 documentation environment: +Historical result in the 2026-08-26 documentation environment: ```text Ran 431 tests in 3.973s @@ -476,6 +482,8 @@ Important implications: - do not use “full suite currently fails” as permission to ignore targeted regressions; - for any patch, run the smallest relevant target set and report exact pass/fail counts. +For the 2026-08-28 browser-storage-v2 increment, the dependency-light targeted storage/bootstrap/anonymous/color/cleanup run executes 26 tests and passes all 26. It includes an executable Node storage round trip covering copied-live clearing, exact-owner migration, orphan rejection, migration write failure, multi-tab CLEAR protection, post-clear USER recovery, and anonymous isolation. + Do not resurrect L2/L3 merely to make stale tests import again. The test suite itself needs a deliberate cleanup/migration task. --- diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index 4b5ada73..fb26603f 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -533,3 +533,21 @@ Memory Attention is deterministic and stateless: it does not call SERVICE, chang **Why:** the useful behavior was retrieval/ranking. The causal homeostat duplicated model work, obscured sampling, and leaked event-wide significance through L1 into durable L4. **Rejected alternatives:** keeping observer-only chemistry; keeping significance as an independent durable score; preserving the metabolic UI without the backend. + +--- + +## D039 — Browser runtime continuity has one atomic checkpoint + +**Status:** Accepted / implemented + +Normal browser continuity uses exactly `jin.liveRuntimeMemory.v2` in `sessionStorage` and `jin.sessionCheckpoint.v2` in `localStorage`. The live record is page-ephemeral and survives only soft WebSocket reconnect. Page execution clears it before bootstrap. Reload and new tabs read the one durable checkpoint, then hydrate only an ephemeral live record; there are no durable per-session FRAME records and no freshness scan. + +The durable checkpoint atomically stores session lineage, save/commit times, runtime memory and update count, runtime snapshot, and session snapshot. Session CLEAR writes a version-2 cleared tombstone. Passive writers cannot replace it; only a successfully emitted new USER move authorizes a later checkpoint, and the clear boundary continues to reject a late pre-clear tab. + +Legacy migration follows ownership rather than freshness: the common snapshot selects the session and may join only a matching saved runtime or its exact per-session record. A self-contained saved runtime is the only fallback without a common snapshot. Orphan per-session records are cleared. Legacy data is deleted only after a successful v2 write, and anonymous mode does not inspect normal-profile state. + +Explicit archived restore keeps dialogue, reasoning, and FRAME under server-archive ownership. A same-ID, fresh local v2 checkpoint may contribute only presentation state. + +**Why:** one atomic owner eliminates torn SAVE pairs, stale-key resurrection, ambiguous newest-record selection, and hidden bootstrap sources while preserving soft reconnect. + +**Rejected alternatives:** split saved-runtime/saved-session keys; durable per-session FRAME keys; newest-timestamp scans; physical deletion without a multi-tab tombstone; automatic `saved_runtime.txt` fallback. diff --git a/saved_runtime.example.txt b/saved_runtime.example.txt deleted file mode 100644 index ff8db867..00000000 --- a/saved_runtime.example.txt +++ /dev/null @@ -1,7 +0,0 @@ -SAVED_RUNTIME = " - -" - -SAVED_SESSION = " - -" \ No newline at end of file diff --git a/tests/test_anonymous_mode_client_contract.py b/tests/test_anonymous_mode_client_contract.py index 190a47b0..d705fb19 100644 --- a/tests/test_anonymous_mode_client_contract.py +++ b/tests/test_anonymous_mode_client_contract.py @@ -76,8 +76,9 @@ def test_anonymous_active_memory_uses_separate_browser_key(self): self.assertIn("jin.activeMemory.anonymous.v1", source) self.assertIn("getActiveMemoryStorageKey", source) self.assertIn("shouldIsolateAnonymousStorage", source) - self.assertIn("readLatestSavedSessionSnapshot", source) - self.assertIn("writeLatestSavedSessionSnapshot", source) + self.assertIn("readSessionCheckpoint", source) + self.assertIn("writeSessionCheckpoint", source) + self.assertIn("shouldIsolateAnonymousStorage()", source) def test_socket_waits_for_detection_and_marks_anonymous_connection(self): source = ( diff --git a/tests/test_archived_session_restore.py b/tests/test_archived_session_restore.py index dc794126..5f7aef40 100644 --- a/tests/test_archived_session_restore.py +++ b/tests/test_archived_session_restore.py @@ -1377,7 +1377,7 @@ def test_delayed_memory_session_id_opens_archived_restore_tab(self): self.assertIn("appendThinkingChunk", restore_script) self.assertIn("replaceLoadedDelayedMemoryReportIds", restore_script) self.assertIn("applyPersistedSessionBootstrap", restore_script) - self.assertIn("readLatestSavedRuntimeMemory", restore_script) + self.assertIn("readSessionCheckpoint", restore_script) self.assertIn("payload.runtime_snapshot", restore_script) self.assertIn("jinArchivedSessionBootstrap", runtime_session) self.assertIn("snapshotRuntimeMemory", runtime_session) diff --git a/tests/test_bootstrap_latest_session_fix_client_contract.py b/tests/test_bootstrap_latest_session_fix_client_contract.py index 1827d233..e181d609 100644 --- a/tests/test_bootstrap_latest_session_fix_client_contract.py +++ b/tests/test_bootstrap_latest_session_fix_client_contract.py @@ -15,21 +15,18 @@ class BootstrapLatestSessionFixClientContractTests(unittest.TestCase): - def test_bootstrap_selects_runtime_record_for_common_checkpoint_session(self): + def test_bootstrap_reads_one_atomic_checkpoint(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") end = source.index("function clearPersistedSessionBootstrap()", start) block = source[start:end] - self.assertIn("const checkpointSessionId =", block) - self.assertIn("const runtimeCandidates = [", block) - self.assertIn("browserLatestSavedRuntimeMemory", block) - self.assertIn("latestCompletedConversationRuntime", block) - self.assertIn("...(", block) - self.assertIn("browserLatestRuntimeMemory", block) - self.assertIn("runtimeCandidates.find(record => (", block) - self.assertIn("=== checkpointSessionId", block) - self.assertIn("if (!runtimeMemory && !conversationCheckpoint)", block) + self.assertIn("const checkpoint =", block) + self.assertIn("readSessionCheckpoint()", block) + self.assertIn("checkpoint.session_id", block) + self.assertIn("checkpoint.runtime_memory", block) + self.assertNotIn("runtimeCandidates", block) + self.assertNotIn("collectOtherLatestRuntimeMemorySnapshots", block) def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") @@ -45,12 +42,9 @@ def test_stale_tab_cannot_rewind_global_bootstrap_pointer(self): self.assertIn(guard, persist) self.assertLess( persist.index(guard), - persist.index("writeLatestSavedRuntimeMemory({"), - ) - self.assertLess( - persist.index(guard), - persist.index("writeLatestSavedSessionSnapshot({"), + persist.index("writeSessionCheckpoint({"), ) + self.assertEqual(persist.count("writeSessionCheckpoint({"), 1) self.assertIn("hasUnsavedSessionActivity = false", persist) diff --git a/tests/test_browser_runtime_storage_v2.py b/tests/test_browser_runtime_storage_v2.py new file mode 100644 index 00000000..6a7f1021 --- /dev/null +++ b/tests/test_browser_runtime_storage_v2.py @@ -0,0 +1,350 @@ +import shutil +import subprocess +import textwrap +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_STORAGE_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-storage.js" +) +RUNTIME_SESSION_JS = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-session.js" +) +SOCKET_JS = ROOT / "ui" / "static" / "js" / "socket.js" +SOCKET_INPUT_JS = ROOT / "ui" / "static" / "js" / "socket" / "input.js" +SESSION_RESTORE_JS = ROOT / "ui" / "static" / "js" / "session-restore.js" + + +@unittest.skipUnless(shutil.which("node"), "node is required") +class BrowserRuntimeStorageV2Tests(unittest.TestCase): + + def test_user_gate_atomic_writer_and_restore_source_contract(self): + runtime_session = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + persist_start = runtime_session.index( + "function persistLiveSessionCheckpoint(data)" + ) + persist_end = runtime_session.index( + "function clearPersistedToolResultsCheckpoint()", + persist_start, + ) + persist = runtime_session[persist_start:persist_end] + self.assertEqual(persist.count("writeSessionCheckpoint({"), 1) + self.assertNotIn("writeLatestSavedRuntimeMemory", persist) + self.assertNotIn("writeLatestSavedSessionSnapshot", persist) + + socket_input = SOCKET_INPUT_JS.read_text(encoding="utf-8") + sent_at = socket_input.index("const sent =") + mark_at = socket_input.index("window.markSessionActivityDirty();", sent_at) + self.assertLess(sent_at, mark_at) + self.assertIn("if (\n sent", socket_input[sent_at:mark_at]) + self.assertNotIn( + "markSessionActivityDirty", + SOCKET_JS.read_text(encoding="utf-8"), + ) + + restore = SESSION_RESTORE_JS.read_text(encoding="utf-8") + start = restore.index("function mergeLatestVisualCheckpoint(") + end = restore.index("function restoreVisualState(", start) + merge = restore[start:end] + self.assertIn("storage.readSessionCheckpoint()", merge) + self.assertIn("checkpointMatchesRestore", merge) + self.assertIn("=== restoreSourceSessionId", merge) + self.assertIn("archive_tail_at", merge) + self.assertNotIn("runtime_memory", merge) + self.assertNotIn("collectOtherLatestRuntimeMemorySnapshots", merge) + + def test_migration_clear_and_multitab_contract(self): + script = textwrap.dedent( + f""" + const assert = require("assert"); + const fs = require("fs"); + const vm = require("vm"); + const source = fs.readFileSync( + {str(RUNTIME_STORAGE_JS)!r}, + "utf8" + ); + + const LIVE = "jin.liveRuntimeMemory.v2"; + const CHECKPOINT = "jin.sessionCheckpoint.v2"; + const LEGACY_SAVE = "jin.latestSavedSessionSnapshot.v1"; + const LEGACY_RUNTIME = "jin.latestSavedRuntimeMemory.v1"; + const legacySessionKey = id => `jin.latestRuntimeMemory.${{id}}.v1`; + let nextId = 0; + + class MemoryStorage {{ + constructor(seed = {{}}) {{ + this.values = new Map( + Object.entries(seed).map(([key, value]) => [ + key, + typeof value === "string" ? value : JSON.stringify(value), + ]) + ); + this.failKey = null; + }} + + get length() {{ + return this.values.size; + }} + + key(index) {{ + return Array.from(this.values.keys())[index] ?? null; + }} + + getItem(key) {{ + return this.values.has(key) ? this.values.get(key) : null; + }} + + setItem(key, value) {{ + if (key === this.failKey) {{ + throw new Error("forced storage failure"); + }} + this.values.set(key, String(value)); + }} + + removeItem(key) {{ + this.values.delete(key); + }} + }} + + function boot(localStorage, sessionSeed = {{}}, anonymous = false) {{ + const sessionStorage = new MemoryStorage(sessionSeed); + const window = {{ + localStorage, + sessionStorage, + crypto: {{ + randomUUID: () => `runtime-${{++nextId}}`, + }}, + JinRuntime: {{ + anonymousMode: {{ + shouldIsolateStorage: () => anonymous, + isEnabled: () => anonymous, + }}, + }}, + }}; + window.window = window; + const context = vm.createContext({{ + window, + console, + Date, + JSON, + String, + Number, + Boolean, + Object, + Array, + Set, + Map, + Math, + RegExp, + Promise, + Uint8Array, + }}); + vm.runInContext(source, context); + return {{ + storage: window.JinRuntime.storage, + sessionStorage, + }}; + }} + + function parse(storage, key) {{ + const value = storage.getItem(key); + return value === null ? null : JSON.parse(value); + }} + + // A copied/reloaded sessionStorage value is never a bootstrap source. + {{ + const local = new MemoryStorage({{ + [CHECKPOINT]: {{ + version: 2, + state: "checkpoint", + session_id: "durable-session", + runtime_memory: "durable frame", + }}, + }}); + const page = boot(local, {{ + [LIVE]: {{runtime_memory: "copied frame"}}, + }}); + assert.strictEqual(page.storage.readLatestRuntimeMemory(), null); + assert.strictEqual( + page.storage.readSessionCheckpoint().runtime_memory, + "durable frame" + ); + page.storage.writeLatestRuntimeMemory({{runtime_memory: "live"}}); + assert.strictEqual( + parse(page.sessionStorage, LIVE).runtime_memory, + "live" + ); + assert.strictEqual(local.getItem(LIVE), null); + }} + + // The common legacy SAVE owns migration; only its exact L1 may join it. + {{ + const local = new MemoryStorage({{ + [LEGACY_SAVE]: {{ + session_id: "session-a", + previous_session_id: "session-prev", + saved_at: "2026-08-28T01:00:00.000Z", + session_snapshot: {{dialogue: ["saved"]}}, + }}, + [LEGACY_RUNTIME]: {{ + session_id: "session-b", + runtime_memory: "wrong owner", + }}, + [legacySessionKey("session-a")]: {{ + session_id: "session-a", + runtime_memory: "matching frame", + runtime_memory_updates: 7, + }}, + }}); + const page = boot(local, {{ + [legacySessionKey("session-z")]: {{ + session_id: "session-z", + runtime_memory: "session-only stale frame", + }}, + }}); + const checkpoint = page.storage.readSessionCheckpoint(); + assert.strictEqual(checkpoint.version, 2); + assert.strictEqual(checkpoint.state, "checkpoint"); + assert.strictEqual(checkpoint.session_id, "session-a"); + assert.strictEqual(checkpoint.runtime_memory, "matching frame"); + assert.strictEqual(checkpoint.runtime_memory_updates, 7); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(checkpoint.session_snapshot)), + {{dialogue: ["saved"]}} + ); + assert.strictEqual(local.getItem(LEGACY_SAVE), null); + assert.strictEqual(local.getItem(LEGACY_RUNTIME), null); + assert.strictEqual(local.getItem(legacySessionKey("session-a")), null); + assert.strictEqual( + page.sessionStorage.getItem(legacySessionKey("session-z")), + null + ); + }} + + // A self-contained saved runtime is the sole no-common fallback. + {{ + const local = new MemoryStorage({{ + [LEGACY_RUNTIME]: {{ + session_id: "standalone-session", + saved_at: "2026-08-28T02:00:00.000Z", + runtime_memory: "standalone frame", + }}, + }}); + const checkpoint = boot(local).storage.readSessionCheckpoint(); + assert.strictEqual(checkpoint.session_id, "standalone-session"); + assert.strictEqual(checkpoint.runtime_memory, "standalone frame"); + }} + + // Orphan per-session records are cleared, never selected by freshness. + {{ + const local = new MemoryStorage({{ + [legacySessionKey("orphan")]: {{ + session_id: "orphan", + saved_at: "2099-01-01T00:00:00.000Z", + runtime_memory: "must not restore", + }}, + }}); + const page = boot(local); + assert.strictEqual(page.storage.readSessionCheckpoint(), null); + assert.strictEqual( + page.storage.readSessionCheckpointRecord().state, + "cleared" + ); + assert.strictEqual(local.getItem(legacySessionKey("orphan")), null); + }} + + // Failed v2 persistence leaves every legacy source available to retry. + {{ + const local = new MemoryStorage({{ + [LEGACY_SAVE]: {{session_id: "retry-session"}}, + [LEGACY_RUNTIME]: {{ + session_id: "retry-session", + runtime_memory: "retry frame", + }}, + }}); + local.failKey = CHECKPOINT; + const page = boot(local); + assert.strictEqual(page.storage.readSessionCheckpoint(), null); + assert.notStrictEqual(local.getItem(LEGACY_SAVE), null); + assert.notStrictEqual(local.getItem(LEGACY_RUNTIME), null); + local.failKey = null; + assert.strictEqual( + page.storage.readSessionCheckpoint().session_id, + "retry-session" + ); + assert.strictEqual(local.getItem(LEGACY_SAVE), null); + }} + + // CLEAR wins over an old tab, including after a new USER move replaces + // the tombstone with a checkpoint carrying the clear barrier. + {{ + const local = new MemoryStorage(); + const oldTab = boot(local); + oldTab.storage.markSessionCheckpointUserActivity(); + assert.strictEqual(oldTab.storage.writeSessionCheckpoint({{ + session_id: "old-session", + runtime_memory: "old frame", + }}), true); + + const clearTab = boot(local); + assert.strictEqual(clearTab.storage.clearSessionCheckpoint(), true); + assert.strictEqual(parse(local, CHECKPOINT).state, "cleared"); + assert.strictEqual(oldTab.storage.writeSessionCheckpoint({{ + session_id: "old-session", + runtime_memory: "late old frame", + }}), false); + + clearTab.storage.markSessionCheckpointUserActivity(); + assert.strictEqual(clearTab.storage.writeSessionCheckpoint({{ + session_id: "new-session", + runtime_memory: "new frame", + }}), true); + assert.ok(parse(local, CHECKPOINT).clear_barrier_at); + assert.strictEqual(oldTab.storage.writeSessionCheckpoint({{ + session_id: "old-session", + runtime_memory: "even later old frame", + }}), false); + assert.strictEqual(parse(local, CHECKPOINT).session_id, "new-session"); + }} + + // Anonymous mode neither migrates nor writes the durable checkpoint. + {{ + const local = new MemoryStorage({{ + [LEGACY_SAVE]: {{session_id: "private-session"}}, + }}); + const page = boot(local, {{}}, true); + assert.strictEqual(page.storage.readSessionCheckpoint(), null); + assert.strictEqual(page.storage.writeSessionCheckpoint({{ + session_id: "private-session", + runtime_memory: "private", + }}), false); + assert.notStrictEqual(local.getItem(LEGACY_SAVE), null); + assert.strictEqual(local.getItem(CHECKPOINT), null); + page.storage.writeLatestRuntimeMemory({{runtime_memory: "ephemeral"}}); + assert.strictEqual( + page.storage.readLatestRuntimeMemory().runtime_memory, + "ephemeral" + ); + }} + """ + ) + + result = subprocess.run( + ["node", "-e", script], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual( + result.returncode, + 0, + msg=f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py index ca280879..85d04182 100644 --- a/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py +++ b/tests/test_clean_tool_results_bootstrap_checkpoint_contract.py @@ -20,13 +20,14 @@ def test_clean_only_mutates_tool_results_in_existing_browser_checkpoint(self): end = source.index("function buildCheckpointRuntimeSnapshot", start) block = source[start:end] - self.assertIn("readLatestSavedSessionSnapshot()", block) + self.assertIn("readSessionCheckpoint()", block) self.assertIn("...previousCheckpoint", block) self.assertIn("...previousSessionSnapshot", block) self.assertIn("tool_results: []", block) self.assertIn("tool_results_cleared_at: new Date().toISOString()", block) self.assertNotIn("saved_at:", block) self.assertNotIn("writeLatestSavedRuntimeMemory", block) + self.assertEqual(block.count("writeSessionCheckpoint({"), 1) def test_clean_runtime_action_does_not_rewrite_full_live_checkpoint(self): source = RUNTIME_ACTIONS_JS.read_text(encoding="utf-8") diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index aef81bbc..6a491611 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -128,23 +128,16 @@ def test_every_runtime_l1_persists_the_live_session_checkpoint(self): self.assertIn("Number(data.updates || 0) <= 0", block) - def test_bootstrap_prefers_continuous_last_saved_runtime(self): + def test_bootstrap_reads_the_atomic_v2_checkpoint(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") end = source.index("function clearPersistedSessionBootstrap", start) block = source[start:end] - saved_runtime_read = block.index("readLatestSavedRuntimeMemory()") - previous_runtime_read = block.index("readLatestPreviousRuntimeMemory") - self.assertLess(saved_runtime_read, previous_runtime_read) - self.assertIn( - "const conversationCheckpoint =", - block, - ) - self.assertIn( - "browserCheckpoint || legacyCompletedCheckpoint", - block, - ) + self.assertIn("readSessionCheckpoint()", block) + self.assertIn("checkpoint.runtime_memory", block) + self.assertIn("checkpoint.session_snapshot", block) + self.assertNotIn("readLatestPreviousRuntimeMemory", block) def test_duplicate_l1_still_refreshes_live_session_checkpoint(self): source = RUNTIME_JS.read_text(encoding="utf-8") @@ -194,7 +187,7 @@ def test_color_has_no_separate_checkpoint_writer(self): end = source.index("function scheduleRoomStatePersist()", start) block = source[start:end] - self.assertEqual(block.count("writeLatestSavedSessionSnapshot"), 2) + self.assertEqual(block.count("writeSessionCheckpoint"), 2) self.assertIn("sessionSnapshot.current_jin_color = avatar.color;", block) self.assertIn("reconcileCurrentColor", block) self.assertNotIn("saved_at:", block) @@ -217,7 +210,9 @@ def test_url_restore_merges_only_checkpoint_presentation_state(self): end = source.index("function restoreVisualState(", start) block = source[start:end] - self.assertIn("const candidates = [];", block) + self.assertIn("storage.readSessionCheckpoint()", block) + self.assertIn("checkpointSessionId !== restoreSessionId", block) + self.assertNotIn("const candidates = [];", block) self.assertIn('"session_actions",', block) self.assertIn('"room_state",', block) self.assertNotIn("Object.assign(merged, selected.snapshot);", block) diff --git a/tests/test_session_bootstrap_lineage_client_contract.py b/tests/test_session_bootstrap_lineage_client_contract.py index 68c6feb9..6fa07b05 100644 --- a/tests/test_session_bootstrap_lineage_client_contract.py +++ b/tests/test_session_bootstrap_lineage_client_contract.py @@ -39,31 +39,28 @@ def test_session_save_uses_current_runtime_id_not_restored_facts_id(self): self.assertIn("getCurrentRuntimeSessionId()", block) self.assertNotIn("getCurrentFactsMemorySessionId()", block) - def test_normal_boot_uses_common_checkpoint_and_only_matching_l1(self): + def test_normal_boot_uses_the_atomic_v2_checkpoint(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") end = source.index("function clearPersistedSessionBootstrap()", start) block = source[start:end] - self.assertIn("readLatestPreviousRuntimeMemory()", block) - self.assertIn("browserLatestRuntimeMemory", block) - self.assertIn( - "browserCheckpoint || legacyCompletedCheckpoint", - block, - ) - self.assertIn("String(record.session_id || \"\").trim()", block) - self.assertIn("=== checkpointSessionId", block) - self.assertIn("!runtimeMemory && !conversationCheckpoint", block) + self.assertIn("readSessionCheckpoint()", block) + self.assertIn("checkpoint.session_id", block) + self.assertIn("checkpoint.runtime_memory", block) + self.assertIn("checkpoint.session_snapshot", block) + self.assertNotIn("collectOtherLatestRuntimeMemorySnapshots", block) - def test_boot_materializes_current_session_as_next_direct_predecessor(self): + def test_boot_hydrates_only_the_ephemeral_live_runtime(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function applyPersistedSessionBootstrap(bootstrap)") end = source.index("function getPersistedSessionBootstrap()", start) block = source[start:end] - self.assertIn("cloneRuntimeMemoryToCurrentSession", block) + self.assertIn("hydrateLiveRuntimeMemoryFromCheckpoint", block) self.assertIn("bootstrap.source_session_id", block) self.assertIn("bootstrap.runtime_memory", block) + self.assertNotIn("writeSessionCheckpoint", block) def test_boot_color_has_one_local_and_one_server_reconciliation_path(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") @@ -128,10 +125,11 @@ def test_live_runtime_snapshot_keeps_direct_predecessor_link(self): self.assertIn("booted_from_session_id", source) self.assertIn("setBootSourceRuntimeSessionId", source) - self.assertIn("readLatestPreviousRuntimeMemory", source) - self.assertIn("collectOtherLatestRuntimeMemorySnapshots", source) - self.assertIn("Opening a tab is not conversation activity", source) - self.assertIn("|| new Date().toISOString()", source) + self.assertIn("hydrateLiveRuntimeMemoryFromCheckpoint", source) + self.assertIn("previous_session_id", source) + self.assertIn("jin.liveRuntimeMemory.v2", source) + self.assertIn("jin.sessionCheckpoint.v2", source) + self.assertNotIn("collectOtherLatestRuntimeMemorySnapshots", source) def test_persisted_runtime_snapshot_keeps_its_origin_session_id(self): storage_source = RUNTIME_STORAGE_JS.read_text(encoding="utf-8") @@ -139,7 +137,7 @@ def test_persisted_runtime_snapshot_keeps_its_origin_session_id(self): "function buildPersistedRuntimeSnapshot(" ) storage_end = storage_source.index( - "function cloneRuntimeMemoryToCurrentSession(", + "function hydrateLiveRuntimeMemoryFromCheckpoint(", storage_start, ) storage_block = storage_source[storage_start:storage_end] diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index ea673ea9..b98e5b4b 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -2753,9 +2753,6 @@ function appendLog( } else if (normalizedTag.includes("SESSION")) { logKind = "session"; - } else if (normalizedTag.includes("LATEST SNAPSHOTS")) { - logKind = - "session"; } else if (normalizedTag.includes("ACTIVE_MEMORY")) { logKind = "active-memory"; @@ -2903,21 +2900,6 @@ function appendLog( ); } - if (tag.includes("LATEST SNAPSHOTS")) { - tagClass = - "text-cyan-300 font-bold"; - - logDiv.classList.add( - "font-mono", - "text-[12px]", - "bg-cyan-500/5", - "p-2", - "rounded", - "border", - "border-cyan-500/10", - ); - } - if (tag.includes("ACTIVE_MEMORY")) { tagClass = "text-zinc-300 font-bold"; @@ -3118,9 +3100,6 @@ function appendLog( const isSession = tag.includes("SESSION"); - const isLatestSnapshots = - tag.includes("LATEST SNAPSHOTS"); - const isActiveMemory = tag.includes("ACTIVE_MEMORY"); @@ -3200,14 +3179,14 @@ function appendLog( ? "inline-flex items-center rounded border border-zinc-600/40 px-2 py-1 text-[10px] uppercase tracking-wider text-zinc-300 hover:bg-zinc-700/40 transition" : isSummarizer ? "mt-2 inline-flex items-center rounded border border-blue-500/20 px-2 py-1 text-[10px] uppercase tracking-wider text-blue-300 hover:bg-blue-500/10 transition" - : isSession || isLatestSnapshots + : isSession ? "inline-flex items-center rounded border border-cyan-500/20 px-2 py-1 text-[10px] uppercase tracking-wider text-cyan-300 hover:bg-cyan-500/10 transition" : "mt-2 inline-flex items-center rounded border border-red-500/20 px-2 py-1 text-[10px] uppercase tracking-wider text-red-300 hover:bg-red-500/10 transition"; traceButton.textContent = isModelOutput ? "payload" - : isSession || isLatestSnapshots || isActiveMemory || isFactsMemory + : isSession || isActiveMemory || isFactsMemory ? "show" : isSummarizer ? "payload" @@ -3233,9 +3212,7 @@ function appendLog( : prettifyTraceDetails(normalized.details), getTraceTitle( normalized.details, - isLatestSnapshots - ? "Latest snapshots" - : isSession + isSession ? "Session bootstrap" : tag.includes("ACTIVE_MEMORY") ? "Active memory payload" @@ -3264,7 +3241,6 @@ function appendLog( if ( isSession - || isLatestSnapshots || isActiveMemory || isFactsMemory ) { @@ -3285,12 +3261,7 @@ function appendLog( clearButton.addEventListener( "click", function () { - if ( - isLatestSnapshots - && window.clearOtherLatestRuntimeMemorySnapshots - ) { - window.clearOtherLatestRuntimeMemorySnapshots(); - } else if (isActiveMemory) { + if (isActiveMemory) { if ( window.JinRuntime && window.JinRuntime.runtime diff --git a/ui/static/js/logger/logger.js b/ui/static/js/logger/logger.js index b12517f4..4906bd38 100644 --- a/ui/static/js/logger/logger.js +++ b/ui/static/js/logger/logger.js @@ -4149,8 +4149,8 @@ function persistRoomStateNow(options = {}) { if ( !storage - || typeof storage.readLatestSavedSessionSnapshot !== "function" - || typeof storage.writeLatestSavedSessionSnapshot !== "function" + || typeof storage.readSessionCheckpoint !== "function" + || typeof storage.writeSessionCheckpoint !== "function" || ( typeof storage.shouldIsolateAnonymousStorage === "function" && storage.shouldIsolateAnonymousStorage() @@ -4160,7 +4160,7 @@ function persistRoomStateNow(options = {}) { } const checkpoint = - storage.readLatestSavedSessionSnapshot(); + storage.readSessionCheckpoint(); if ( !checkpoint @@ -4231,7 +4231,7 @@ function persistRoomStateNow(options = {}) { }; } - const written = storage.writeLatestSavedSessionSnapshot({ + const written = storage.writeSessionCheckpoint({ ...checkpoint, session_snapshot: sessionSnapshot, }); @@ -4272,13 +4272,13 @@ function getStoredRoomState() { if ( !storage - || typeof storage.readLatestSavedSessionSnapshot !== "function" + || typeof storage.readSessionCheckpoint !== "function" ) { return null; } const checkpoint = - storage.readLatestSavedSessionSnapshot(); + storage.readSessionCheckpoint(); if ( !checkpoint diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 39087eaa..681e2a64 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -54,23 +54,15 @@ } = memoryModel; const { - keys: runtimeStorageKeys, - removeBrowserMemory, readLatestRuntimeMemory, writeLatestRuntimeMemory, - writeLatestSavedSessionSnapshot, - readLatestSavedSessionSnapshot, - writeLatestSavedRuntimeMemory, - readLatestSavedRuntimeMemory, + writeSessionCheckpoint, + readSessionCheckpoint, + clearSessionCheckpoint, + markSessionCheckpointUserActivity, buildPersistedRuntimeSnapshot, setBootSourceRuntimeSessionId, - cloneRuntimeMemoryToCurrentSession, - readLatestPreviousRuntimeMemory, - readLatestCompletedConversationRuntimeMemory, - collectOtherLatestRuntimeMemorySnapshots, - clearOtherLatestRuntimeMemorySnapshots, - getSavedRuntimeMemoryFallback, - getCurrentLatestRuntimeMemoryStorageKey, + hydrateLiveRuntimeMemoryFromCheckpoint, getCurrentRuntimeSessionId, activateFactsMemorySession, shouldIsolateAnonymousStorage, @@ -79,7 +71,6 @@ let pendingBootstrapRuntimeMemorySnapshot = null; let lastStableRuntimeMemorySnapshot = null; - let persistedSessionBootstrapCleared = false; let hasUnsavedSessionActivity = false; function getCurrentSavedSessionId() { @@ -373,7 +364,7 @@ const savedAt = new Date().toISOString(); const previousCheckpoint = - readLatestSavedSessionSnapshot(); + readSessionCheckpoint(); const sameSession = Boolean( previousCheckpoint && typeof previousCheckpoint === "object" @@ -459,29 +450,22 @@ }); } - writeLatestSavedRuntimeMemory({ - ...currentRuntime, - version: currentRuntime.version || 1, - session_id: currentSessionId, - previous_session_id: previousSessionId, - saved_at: savedAt, - conversation_committed_at: conversationCommittedAt, - session_snapshot: sessionSnapshot, - runtime_snapshot: - buildCheckpointRuntimeSnapshot( - currentRuntime.runtime_snapshot - ), - }); - const checkpointWritten = - writeLatestSavedSessionSnapshot({ - version: 1, + writeSessionCheckpoint({ + version: 2, + state: "checkpoint", session_id: currentSessionId, previous_session_id: previousSessionId, saved_at: savedAt, conversation_committed_at: conversationCommittedAt, - loaded_memory_ids: - sessionSnapshot.loaded_memory_ids, + runtime_memory: + currentRuntime.runtime_memory, + runtime_memory_updates: + Number(currentRuntime.runtime_memory_updates || 0), + runtime_snapshot: + buildCheckpointRuntimeSnapshot( + currentRuntime.runtime_snapshot + ), session_snapshot: sessionSnapshot, }); @@ -506,7 +490,7 @@ } const previousCheckpoint = - readLatestSavedSessionSnapshot(); + readSessionCheckpoint(); if ( !previousCheckpoint @@ -529,7 +513,7 @@ // checkpoint timestamp/lineage verbatim: advancing saved_at here makes // the browser checkpoint look newer than the raw chat-log tail, which // suppresses archive enrichment for dialogue/reasoning/actions/files. - writeLatestSavedSessionSnapshot({ + writeSessionCheckpoint({ ...previousCheckpoint, session_snapshot: { ...previousSessionSnapshot, @@ -1003,7 +987,7 @@ const restoredRuntimeSnapshot = { ...data.snapshot, index: 0, - display_source: "saved_runtime_checkpoint", + display_source: "session_checkpoint", restored_from_checkpoint: true, runtime_memory_updates: Number( data.updates @@ -1114,7 +1098,7 @@ || (data && data.previous_session_id) || "browser_restore", index: 0, - display_source: "saved_runtime_checkpoint", + display_source: "session_checkpoint", raw_memory: runtimeMemory, lines: sourceSnapshotMatches @@ -1209,25 +1193,28 @@ bootstrap && bootstrap.source_session_id && String(bootstrap.runtime_memory || "").trim() - && cloneRuntimeMemoryToCurrentSession + && hydrateLiveRuntimeMemoryFromCheckpoint ) { - // Materialize the inherited L1 immediately under this fresh runtime - // session id. Its new saved_at makes this session the direct - // predecessor for the next boot, while booted_from_session_id keeps - // the one-hop lineage explicit. - cloneRuntimeMemoryToCurrentSession({ - version: 1, + // Materialize inherited L1 only in this page's ephemeral live cache. + // Opening a tab does not create another durable per-session record and + // does not advance the common conversation checkpoint. + hydrateLiveRuntimeMemoryFromCheckpoint({ + version: 2, session_id: bootstrap.source_session_id, + previous_session_id: + bootstrap.previous_session_id || null, + saved_at: + String(bootstrap.saved_at || "").trim(), + conversation_committed_at: + String( + bootstrap.conversation_committed_at || "" + ).trim(), runtime_memory: bootstrap.runtime_memory, runtime_memory_updates: bootstrap.runtime_memory_updates || 0, runtime_snapshot: bootstrap.runtime_snapshot || null, }); - - // Do not advance the common conversation checkpoint on page load. - // A real user move may advance it later even if generation is stopped - // before JIN finishes. } if ( @@ -1297,159 +1284,24 @@ }; } - const browserCheckpoint = - readLatestSavedSessionSnapshot(); - const browserLatestSavedRuntimeMemory = - readLatestSavedRuntimeMemory(); - const browserLatestRuntimeMemory = - readLatestPreviousRuntimeMemory - ? readLatestPreviousRuntimeMemory() - : null; - const latestCompletedConversationRuntime = - readLatestCompletedConversationRuntimeMemory - ? readLatestCompletedConversationRuntimeMemory() - : null; - - const isRuntimeRecord = value => Boolean( - value - && typeof value === "object" - && !Array.isArray(value) - && String(value.runtime_memory || "").trim() - ); - const legacyCompletedCheckpoint = - !browserCheckpoint - && isRuntimeRecord(latestCompletedConversationRuntime) - ? { - version: 1, - session_id: - latestCompletedConversationRuntime.session_id, - previous_session_id: - latestCompletedConversationRuntime.previous_session_id - || latestCompletedConversationRuntime.booted_from_session_id - || null, - saved_at: - latestCompletedConversationRuntime.saved_at - || latestCompletedConversationRuntime.conversation_committed_at - || "", - conversation_committed_at: - latestCompletedConversationRuntime.conversation_committed_at - || "", - loaded_memory_ids: - ( - latestCompletedConversationRuntime.session_snapshot - && Array.isArray( - latestCompletedConversationRuntime.session_snapshot.loaded_memory_ids - ) - ) - ? latestCompletedConversationRuntime.session_snapshot.loaded_memory_ids - : [], - session_snapshot: - ( - latestCompletedConversationRuntime.session_snapshot - && typeof latestCompletedConversationRuntime.session_snapshot === "object" - && !Array.isArray( - latestCompletedConversationRuntime.session_snapshot - ) - ) - ? { - ...latestCompletedConversationRuntime.session_snapshot, - } - : {}, - } - : null; - - // The common checkpoint names the last session that actually moved. - // Per-session records only provide its matching L1 payload, or a legacy - // fallback when no common checkpoint exists at all. - const conversationCheckpoint = - browserCheckpoint || legacyCompletedCheckpoint; + const checkpoint = + readSessionCheckpoint(); - const checkpointSessionId = - String( - conversationCheckpoint - && conversationCheckpoint.session_id - || "" - ).trim(); - const otherRuntimeRecords = - collectOtherLatestRuntimeMemorySnapshots - ? collectOtherLatestRuntimeMemorySnapshots() - : []; - const runtimeCandidates = [ - browserLatestSavedRuntimeMemory, - latestCompletedConversationRuntime, - ...( - Array.isArray(otherRuntimeRecords) - ? otherRuntimeRecords - : [] - ), - browserLatestRuntimeMemory, - ].filter(isRuntimeRecord); - - let runtimeMemory = checkpointSessionId - ? runtimeCandidates.find(record => ( - String(record.session_id || "").trim() - === checkpointSessionId - )) || null - : runtimeCandidates[0] || null; - - if (!runtimeMemory && !conversationCheckpoint) { - const savedRuntimeFallback = - getSavedRuntimeMemoryFallback(); - - if (savedRuntimeFallback) { - runtimeMemory = - savedRuntimeFallback.latest_saved_runtime_memory - || savedRuntimeFallback.runtime_memory - || null; - } - } - - const runtimeText = - String( - runtimeMemory && runtimeMemory.runtime_memory - || "" - ).trim(); - - if (!conversationCheckpoint && !runtimeText) { + if (!checkpoint) { return null; } - let sourceSessionId = - String( - checkpointSessionId - || ( - runtimeMemory - && runtimeMemory.session_id - ) - || ( - runtimeMemory - && runtimeMemory.runtime_snapshot - && runtimeMemory.runtime_snapshot.session_id - ) - || "" - ).trim(); - - const checkpoint = conversationCheckpoint; - - if (!sourceSessionId && checkpoint) { - sourceSessionId = - String(checkpoint.session_id || "").trim(); - } + const sourceSessionId = + String(checkpoint.session_id || "").trim(); const previousSessionId = String( - ( - checkpoint - && checkpoint.previous_session_id - ) - || (runtimeMemory && runtimeMemory.previous_session_id) - || (runtimeMemory && runtimeMemory.booted_from_session_id) + checkpoint.previous_session_id || "" ).trim(); const sessionSnapshot = ( - checkpoint - && checkpoint.session_snapshot + checkpoint.session_snapshot && typeof checkpoint.session_snapshot === "object" && !Array.isArray(checkpoint.session_snapshot) ) @@ -1467,16 +1319,15 @@ ); } + const runtimeText = + String(checkpoint.runtime_memory || "").trim(); const runtimeDisplaySnapshot = runtimeText ? buildRuntimeMemoryDisplaySnapshot({ runtime_memory: runtimeText, runtime_memory_updates: - Number( - runtimeMemory.runtime_memory_updates - || 0 - ), + Number(checkpoint.runtime_memory_updates || 0), runtime_snapshot: - runtimeMemory.runtime_snapshot || null, + checkpoint.runtime_snapshot || null, source_session_id: sourceSessionId || null, previous_session_id: previousSessionId || null, }) @@ -1487,42 +1338,30 @@ type: "session_bootstrap", source_session_id: sourceSessionId || null, previous_session_id: previousSessionId || null, - saved_at: + conversation_committed_at: String( - (checkpoint && checkpoint.saved_at) - || (runtimeMemory && runtimeMemory.saved_at) - || "" + checkpoint.conversation_committed_at || "" ).trim(), + saved_at: + String(checkpoint.saved_at || "").trim(), loaded_memory_ids: - Array.from(new Set([ - ...( - checkpoint - && Array.isArray(checkpoint.loaded_memory_ids) - ? checkpoint.loaded_memory_ids - : [] - ), - ...( - Array.isArray(sessionSnapshot.loaded_memory_ids) - ? sessionSnapshot.loaded_memory_ids - : [] - ), - ])) + Array.from(new Set( + Array.isArray(sessionSnapshot.loaded_memory_ids) + ? sessionSnapshot.loaded_memory_ids + : [] + )) .map(item => String(item || "").trim()) .filter(Boolean), runtime_memory: runtimeText, runtime_memory_updates: - Number( - runtimeMemory && runtimeMemory.runtime_memory_updates - || 0 - ), + Number(checkpoint.runtime_memory_updates || 0), runtime_snapshot: - runtimeMemory && runtimeMemory.runtime_snapshot || null, + checkpoint.runtime_snapshot || null, runtime_display_snapshot: runtimeDisplaySnapshot, }; } function clearPersistedSessionBootstrap() { - persistedSessionBootstrapCleared = true; hasUnsavedSessionActivity = false; if ( @@ -1534,19 +1373,11 @@ return; } - removeBrowserMemory( - runtimeStorageKeys.latestSavedSessionSnapshotStorageKey - ); - removeBrowserMemory( - runtimeStorageKeys.latestSavedRuntimeMemoryStorageKey - ); - removeBrowserMemory( - getCurrentLatestRuntimeMemoryStorageKey() - ); + clearSessionCheckpoint(); } function markSessionActivityDirty() { - persistedSessionBootstrapCleared = false; + markSessionCheckpointUserActivity(); hasUnsavedSessionActivity = true; } @@ -1565,16 +1396,6 @@ window.applyPersistedSessionBootstrap = applyPersistedSessionBootstrap; window.getPersistedSessionBootstrap = getPersistedSessionBootstrap; window.clearPersistedSessionBootstrap = clearPersistedSessionBootstrap; - window.getCurrentLatestRuntimeMemoryStorageKey = function () { - return getCurrentLatestRuntimeMemoryStorageKey(); - }; - window.getOtherLatestRuntimeMemorySnapshots = function () { - return collectOtherLatestRuntimeMemorySnapshots(); - }; - window.clearOtherLatestRuntimeMemorySnapshots = function () { - return clearOtherLatestRuntimeMemorySnapshots(); - }; window.markSessionActivityDirty = markSessionActivityDirty; - window.markSessionBootstrapActive = markSessionActivityDirty; } }()); diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index f3473950..4bb0bc9f 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -2,11 +2,17 @@ window.JinRuntime = window.JinRuntime || {}; - const latestSavedSessionSnapshotStorageKey = + const liveRuntimeMemoryStorageKey = + "jin.liveRuntimeMemory.v2"; + + const sessionCheckpointStorageKey = + "jin.sessionCheckpoint.v2"; + + const legacyLatestSavedSessionSnapshotStorageKey = "jin.latestSavedSessionSnapshot.v1"; // One-time compatibility read for checkpoints created before L3 removal. - const legacyLatestSavedSessionSnapshotStorageKey = + const legacyL3SavedSessionSnapshotStorageKey = "jin.latestSavedSessionMemory.v1"; const retiredSavedSessionHistoryStorageKey = @@ -15,10 +21,10 @@ const runtimeSessionIdSessionStorageKey = "jin.runtimeSessionId.v1"; - const latestRuntimeMemoryStorageKeyPrefix = + const legacyLatestRuntimeMemoryStorageKeyPrefix = "jin.latestRuntimeMemory"; - const latestRuntimeMemoryStorageKeyVersion = + const legacyLatestRuntimeMemoryStorageKeyVersion = "v1"; const latestSavedRuntimeMemoryStorageKey = @@ -42,13 +48,9 @@ const factsMemoryStorageKeyVersion = "v1"; - const savedRuntimeFallbackPath = - "/saved_runtime.txt"; - - let clonedRuntimeSessionId = null; let bootSourceRuntimeSessionId = null; - let savedRuntimeFileFallback = null; - let savedRuntimeFileFallbackLoaded = false; + let sessionCheckpointMigrationAttempted = false; + let sessionCheckpointUserActivityAt = 0; function normalizeFactsMemoryStatus( value @@ -163,8 +165,6 @@ const newRuntimeSessionId = generateRuntimeSessionId(); - clonedRuntimeSessionId = storedSessionId; - window.sessionStorage.setItem( runtimeSessionIdSessionStorageKey, newRuntimeSessionId @@ -198,14 +198,17 @@ let factsMemorySessionId = runtimeSessionId; - let latestRuntimeMemoryStorageKey = - getLatestRuntimeMemoryStorageKey( - runtimeSessionId - ); - window.jinRuntimeSessionId = runtimeSessionId; + // sessionStorage can be copied into a new tab and survives reload. The live + // FRAME is valid only inside this already-running page, so discard any copied + // or reloaded value before bootstrap. Soft WebSocket reconnect does not + // re-execute this module and keeps using the value written afterwards. + removeSessionMemory( + liveRuntimeMemoryStorageKey + ); + function getRuntimeSessionId() { return runtimeSessionId; @@ -242,33 +245,26 @@ } - function getLatestRuntimeMemoryStorageKey( + function getLegacyLatestRuntimeMemoryStorageKey( runtimeSessionId ) { - return `${latestRuntimeMemoryStorageKeyPrefix}` + return `${legacyLatestRuntimeMemoryStorageKeyPrefix}` + `.${runtimeSessionId}` - + `.${latestRuntimeMemoryStorageKeyVersion}`; - - } - - - function getCurrentLatestRuntimeMemoryStorageKey() { - - return latestRuntimeMemoryStorageKey; + + `.${legacyLatestRuntimeMemoryStorageKeyVersion}`; } - function isLatestRuntimeMemoryKey( + function isLegacyLatestRuntimeMemoryKey( key ) { const prefix = - `${latestRuntimeMemoryStorageKeyPrefix}.`; + `${legacyLatestRuntimeMemoryStorageKeyPrefix}.`; const suffix = - `.${latestRuntimeMemoryStorageKeyVersion}`; + `.${legacyLatestRuntimeMemoryStorageKeyVersion}`; return ( typeof key === "string" @@ -280,32 +276,6 @@ } - function getSessionIdFromLatestRuntimeMemoryKey( - key - ) { - - const prefix = - `${latestRuntimeMemoryStorageKeyPrefix}.`; - - const suffix = - `.${latestRuntimeMemoryStorageKeyVersion}`; - - if ( - typeof key !== "string" - || !key.startsWith(prefix) - || !key.endsWith(suffix) - ) { - return ""; - } - - return key.slice( - prefix.length, - key.length - suffix.length - ); - - } - - function setRuntimeSessionId( nextRuntimeSessionId ) { @@ -320,11 +290,6 @@ runtimeSessionId = normalizedRuntimeSessionId; window.jinRuntimeSessionId = runtimeSessionId; - latestRuntimeMemoryStorageKey = - getLatestRuntimeMemoryStorageKey( - runtimeSessionId - ); - try { window.sessionStorage.setItem( runtimeSessionIdSessionStorageKey, @@ -519,32 +484,6 @@ } - function readRuntimeSessionMemory( - key - ) { - - return shouldIsolateAnonymousStorage() - ? readSessionMemory(key) - : readBrowserMemory(key); - - } - - - function writeRuntimeSessionMemory( - key, - value - ) { - - if (shouldIsolateAnonymousStorage()) { - writeSessionMemory(key, value); - return; - } - - writeBrowserMemory(key, value); - - } - - // The old multi-checkpoint L3 history has no runtime meaning anymore. // Do not mutate the normal browser profile while anonymous detection is // pending or anonymous isolation is active. @@ -624,8 +563,8 @@ function readLatestRuntimeMemory() { return sanitizeRuntimeMemoryRecord( - readRuntimeSessionMemory( - latestRuntimeMemoryStorageKey + readSessionMemory( + liveRuntimeMemoryStorageKey ) ); @@ -638,8 +577,8 @@ const previousValue = sanitizeRuntimeMemoryRecord( - readRuntimeSessionMemory( - latestRuntimeMemoryStorageKey + readSessionMemory( + liveRuntimeMemoryStorageKey ) ); @@ -735,15 +674,15 @@ }; } - writeRuntimeSessionMemory( - latestRuntimeMemoryStorageKey, + writeSessionMemory( + liveRuntimeMemoryStorageKey, normalizedValue ); } - function normalizeSavedSessionSnapshot( + function normalizeLegacySavedSessionSnapshot( value ) { @@ -758,19 +697,13 @@ return { version: Number(value.version || 1), session_id: - String(value.session_id || runtimeSessionId || "").trim(), + String(value.session_id || "").trim(), previous_session_id: String(value.previous_session_id || "").trim() || null, saved_at: String(value.saved_at || "").trim(), conversation_committed_at: String(value.conversation_committed_at || "").trim(), - loaded_memory_ids: - Array.isArray(value.loaded_memory_ids) - ? value.loaded_memory_ids - .map(item => String(item || "").trim()) - .filter(Boolean) - : [], session_snapshot: ( value.session_snapshot @@ -786,148 +719,603 @@ } - function readLatestSavedSessionSnapshot() { + function normalizeSessionCheckpointRecord( + value + ) { - if (shouldIsolateAnonymousStorage()) { + if ( + !value + || typeof value !== "object" + || Array.isArray(value) + ) { return null; } - const current = - normalizeSavedSessionSnapshot( - readBrowserMemory( - latestSavedSessionSnapshotStorageKey - ) - ); - - if (current) { - return current; + if (String(value.state || "").trim() === "cleared") { + return { + version: 2, + state: "cleared", + cleared_at: + String(value.cleared_at || "").trim(), + }; } - const legacy = - normalizeSavedSessionSnapshot( - readBrowserMemory( - legacyLatestSavedSessionSnapshotStorageKey - ) - ); + const sessionId = + String(value.session_id || "").trim(); - if (!legacy) { + if (!sessionId) { return null; } - writeBrowserMemory( - latestSavedSessionSnapshotStorageKey, - legacy - ); - removeBrowserMemory( - legacyLatestSavedSessionSnapshotStorageKey - ); - - return legacy; + return sanitizeRuntimeMemoryRecord({ + version: 2, + state: "checkpoint", + session_id: sessionId, + previous_session_id: + String(value.previous_session_id || "").trim() || null, + saved_at: + String(value.saved_at || "").trim(), + conversation_committed_at: + String(value.conversation_committed_at || "").trim(), + clear_barrier_at: + String(value.clear_barrier_at || "").trim(), + runtime_memory: + String(value.runtime_memory || "").trim(), + runtime_memory_updates: + Number(value.runtime_memory_updates || 0), + runtime_snapshot: + ( + value.runtime_snapshot + && typeof value.runtime_snapshot === "object" + && !Array.isArray(value.runtime_snapshot) + ) + ? { + ...value.runtime_snapshot, + } + : null, + session_snapshot: + ( + value.session_snapshot + && typeof value.session_snapshot === "object" + && !Array.isArray(value.session_snapshot) + ) + ? { + ...value.session_snapshot, + } + : {}, + }); } - function writeLatestSavedSessionSnapshot( - value + function collectLegacyLatestRuntimeMemoryKeys( + storageArea ) { - if (shouldIsolateAnonymousStorage()) { - return false; + const keys = []; + + try { + for (let index = 0; index < storageArea.length; index += 1) { + const key = storageArea.key(index); + + if (isLegacyLatestRuntimeMemoryKey(key)) { + keys.push(key); + } + } + } catch (error) { + return []; } - const written = writeBrowserMemory( - latestSavedSessionSnapshotStorageKey, - normalizeSavedSessionSnapshot(value) - ); - if (!written) { - return false; + return keys; + + } + + + function clearLegacyRuntimeStorage() { + + [ + legacyLatestSavedSessionSnapshotStorageKey, + legacyL3SavedSessionSnapshotStorageKey, + retiredSavedSessionHistoryStorageKey, + latestSavedRuntimeMemoryStorageKey, + ].forEach(removeBrowserMemory); + + try { + collectLegacyLatestRuntimeMemoryKeys( + window.localStorage + ).forEach(removeBrowserMemory); + } catch (error) { + // Legacy cleanup is best-effort after the v2 checkpoint is safe. } - removeBrowserMemory( - legacyLatestSavedSessionSnapshotStorageKey - ); - return true; + try { + collectLegacyLatestRuntimeMemoryKeys( + window.sessionStorage + ).forEach(removeSessionMemory); + } catch (error) { + // Legacy cleanup is best-effort after the v2 checkpoint is safe. + } } - function readLatestSavedRuntimeMemory() { + function readLegacyRuntimeForSession( + sessionId + ) { - if (shouldIsolateAnonymousStorage()) { + const normalizedSessionId = + String(sessionId || "").trim(); + + if (!normalizedSessionId) { return null; } return sanitizeRuntimeMemoryRecord( readBrowserMemory( - latestSavedRuntimeMemoryStorageKey + getLegacyLatestRuntimeMemoryStorageKey( + normalizedSessionId + ) ) ); } - function normalizeActiveMemoryRecords(value) { + function buildMigratedSessionCheckpoint() { - const source = - Array.isArray(value) - ? value - : String(value || "").split(/\r?\n/); + const legacyCheckpoint = + normalizeLegacySavedSessionSnapshot( + readBrowserMemory( + legacyLatestSavedSessionSnapshotStorageKey + ) + ) + || normalizeLegacySavedSessionSnapshot( + readBrowserMemory( + legacyL3SavedSessionSnapshotStorageKey + ) + ); + const latestSavedRuntime = + sanitizeRuntimeMemoryRecord( + readBrowserMemory( + latestSavedRuntimeMemoryStorageKey + ) + ); + const checkpointSessionId = + String( + legacyCheckpoint + && legacyCheckpoint.session_id + || "" + ).trim(); + const latestSavedRuntimeSessionId = + String( + latestSavedRuntime + && latestSavedRuntime.session_id + || "" + ).trim(); + const sourceSessionId = + checkpointSessionId + || latestSavedRuntimeSessionId; - const records = []; - const seen = new Set(); + if (!sourceSessionId) { + return null; + } - source.forEach(function (record) { - const text = String(record || "").trim(); + let runtimeRecord = null; - if (!/^active_memory(?:_\d+)?\s*:/i.test(text)) { - return; - } + if ( + latestSavedRuntime + && latestSavedRuntimeSessionId === sourceSessionId + ) { + runtimeRecord = latestSavedRuntime; + } else if (checkpointSessionId) { + // This is the only legacy per-session lookup allowed. The common SAVE + // remains the owner; orphan records never choose a session by freshness. + runtimeRecord = + readLegacyRuntimeForSession( + checkpointSessionId + ); + } - if (seen.has(text)) { - return; - } + const runtimeSessionSnapshot = + ( + runtimeRecord + && runtimeRecord.session_snapshot + && typeof runtimeRecord.session_snapshot === "object" + && !Array.isArray(runtimeRecord.session_snapshot) + ) + ? runtimeRecord.session_snapshot + : {}; + const checkpointSessionSnapshot = + ( + legacyCheckpoint + && legacyCheckpoint.session_snapshot + && typeof legacyCheckpoint.session_snapshot === "object" + && !Array.isArray(legacyCheckpoint.session_snapshot) + ) + ? legacyCheckpoint.session_snapshot + : {}; - seen.add(text); - records.push(text); + return normalizeSessionCheckpointRecord({ + version: 2, + state: "checkpoint", + session_id: sourceSessionId, + previous_session_id: + String( + legacyCheckpoint + && legacyCheckpoint.previous_session_id + || runtimeRecord + && ( + runtimeRecord.previous_session_id + || runtimeRecord.booted_from_session_id + ) + || "" + ).trim() || null, + saved_at: + String( + legacyCheckpoint + && legacyCheckpoint.saved_at + || runtimeRecord + && runtimeRecord.saved_at + || "" + ).trim(), + conversation_committed_at: + String( + legacyCheckpoint + && legacyCheckpoint.conversation_committed_at + || runtimeRecord + && runtimeRecord.conversation_committed_at + || "" + ).trim(), + runtime_memory: + String( + runtimeRecord + && runtimeRecord.runtime_memory + || "" + ).trim(), + runtime_memory_updates: + Number( + runtimeRecord + && runtimeRecord.runtime_memory_updates + || 0 + ), + runtime_snapshot: + runtimeRecord + && runtimeRecord.runtime_snapshot + || null, + session_snapshot: { + ...runtimeSessionSnapshot, + ...checkpointSessionSnapshot, + }, }); - return records; - } - function readActiveMemoryRecords() { + function ensureSessionCheckpointMigration() { - return normalizeActiveMemoryRecords( - readBrowserMemory( - getActiveMemoryStorageKey() - ) - ); + if (shouldIsolateAnonymousStorage()) { + return null; + } - } + const current = + normalizeSessionCheckpointRecord( + readBrowserMemory( + sessionCheckpointStorageKey + ) + ); + if (current) { + if (!sessionCheckpointMigrationAttempted) { + clearLegacyRuntimeStorage(); + } + sessionCheckpointMigrationAttempted = true; + return current; + } - function writeActiveMemoryRecords( - records - ) { + if (sessionCheckpointMigrationAttempted) { + return null; + } - writeBrowserMemory( - getActiveMemoryStorageKey(), - normalizeActiveMemoryRecords(records) - ); + sessionCheckpointMigrationAttempted = true; - } + const migrated = + buildMigratedSessionCheckpoint(); + if (migrated) { + if ( + writeBrowserMemory( + sessionCheckpointStorageKey, + migrated + ) + ) { + clearLegacyRuntimeStorage(); + return migrated; + } - function clearActiveMemoryRecords() { + sessionCheckpointMigrationAttempted = false; + return null; + } - removeBrowserMemory( - getActiveMemoryStorageKey() - ); + const orphanLegacyKeys = [ + ...collectLegacyLatestRuntimeMemoryKeys( + window.localStorage + ), + ...collectLegacyLatestRuntimeMemoryKeys( + window.sessionStorage + ), + ]; - return []; + if (orphanLegacyKeys.length) { + const cleared = { + version: 2, + state: "cleared", + cleared_at: new Date().toISOString(), + }; - } + if ( + writeBrowserMemory( + sessionCheckpointStorageKey, + cleared + ) + ) { + clearLegacyRuntimeStorage(); + return cleared; + } + + sessionCheckpointMigrationAttempted = false; + } + + return null; + + } + + + function readSessionCheckpointRecord() { + + if (shouldIsolateAnonymousStorage()) { + return null; + } + + return ensureSessionCheckpointMigration(); + + } + + + function readSessionCheckpoint() { + + const checkpoint = + readSessionCheckpointRecord(); + + return ( + checkpoint + && checkpoint.state === "checkpoint" + ) + ? checkpoint + : null; + + } + + + function markSessionCheckpointUserActivity() { + + const checkpoint = + shouldIsolateAnonymousStorage() + ? null + : normalizeSessionCheckpointRecord( + readBrowserMemory( + sessionCheckpointStorageKey + ) + ); + const clearedAt = + checkpoint + && checkpoint.state === "cleared" + ? Date.parse( + String(checkpoint.cleared_at || "").trim() + ) + : 0; + + sessionCheckpointUserActivityAt = + Math.max( + Date.now(), + sessionCheckpointUserActivityAt + 1, + Number.isFinite(clearedAt) + ? clearedAt + 1 + : 1 + ); + + return sessionCheckpointUserActivityAt; + + } + + + function canOverwriteClearedCheckpoint( + checkpoint + ) { + + if ( + !checkpoint + || checkpoint.state !== "cleared" + ) { + return true; + } + + const clearedAt = + Date.parse( + String(checkpoint.cleared_at || "").trim() + ); + + return sessionCheckpointUserActivityAt > ( + Number.isFinite(clearedAt) + ? clearedAt + : 0 + ); + + } + + + function writeSessionCheckpoint( + value + ) { + + if (shouldIsolateAnonymousStorage()) { + return false; + } + + const existing = + readSessionCheckpointRecord(); + + if (!canOverwriteClearedCheckpoint(existing)) { + return false; + } + + const normalized = + normalizeSessionCheckpointRecord(value); + + if ( + !normalized + || normalized.state !== "checkpoint" + ) { + return false; + } + + const clearBarrierAt = + existing + && existing.state === "cleared" + ? String(existing.cleared_at || "").trim() + : String( + existing + && existing.clear_barrier_at + || "" + ).trim(); + const clearBarrierTimestamp = + Date.parse(clearBarrierAt); + const changesCheckpointOwner = Boolean( + existing + && existing.state === "checkpoint" + && String(existing.session_id || "").trim() + !== String(normalized.session_id || "").trim() + ); + + if ( + changesCheckpointOwner + && Number.isFinite(clearBarrierTimestamp) + && sessionCheckpointUserActivityAt <= clearBarrierTimestamp + ) { + return false; + } + + if (clearBarrierAt) { + normalized.clear_barrier_at = clearBarrierAt; + } + + return writeBrowserMemory( + sessionCheckpointStorageKey, + normalized + ); + + } + + + function clearLiveRuntimeMemory() { + + removeSessionMemory( + liveRuntimeMemoryStorageKey + ); + + } + + + function clearSessionCheckpoint() { + + if (shouldIsolateAnonymousStorage()) { + return false; + } + + sessionCheckpointUserActivityAt = 0; + + const written = writeBrowserMemory( + sessionCheckpointStorageKey, + { + version: 2, + state: "cleared", + cleared_at: new Date().toISOString(), + } + ); + + if (!written) { + return false; + } + + clearLiveRuntimeMemory(); + clearLegacyRuntimeStorage(); + + return true; + + } + + + function normalizeActiveMemoryRecords(value) { + + const source = + Array.isArray(value) + ? value + : String(value || "").split(/\r?\n/); + + const records = []; + const seen = new Set(); + + source.forEach(function (record) { + const text = String(record || "").trim(); + + if (!/^active_memory(?:_\d+)?\s*:/i.test(text)) { + return; + } + + if (seen.has(text)) { + return; + } + + seen.add(text); + records.push(text); + }); + + return records; + + } + + + function readActiveMemoryRecords() { + + return normalizeActiveMemoryRecords( + readBrowserMemory( + getActiveMemoryStorageKey() + ) + ); + + } + + + function writeActiveMemoryRecords( + records + ) { + + writeBrowserMemory( + getActiveMemoryStorageKey(), + normalizeActiveMemoryRecords(records) + ); + + } + + + function clearActiveMemoryRecords() { + + removeBrowserMemory( + getActiveMemoryStorageKey() + ); + + return []; + + } function appendActiveMemoryRecords( @@ -1995,65 +2383,6 @@ } - function writeLatestSavedRuntimeMemory( - value - ) { - - if (shouldIsolateAnonymousStorage()) { - return; - } - - value = sanitizeRuntimeMemoryRecord(value); - - const normalizedValue = - ( - value - && typeof value === "object" - && !Array.isArray(value) - ) - ? { - ...value, - session_id: - String(value.session_id || runtimeSessionId || "").trim(), - previous_session_id: - String( - value.previous_session_id - || value.booted_from_session_id - || bootSourceRuntimeSessionId - || "" - ).trim() || null, - } - : value; - - if ( - normalizedValue - && normalizedValue.runtime_snapshot - && typeof normalizedValue.runtime_snapshot === "object" - ) { - const snapshotSessionId = - String( - normalizedValue.runtime_snapshot.session_id - || "" - ).trim() - || normalizedValue.session_id - || runtimeSessionId; - - normalizedValue.runtime_snapshot = { - ...normalizedValue.runtime_snapshot, - session_id: snapshotSessionId, - previous_session_id: - normalizedValue.previous_session_id, - }; - } - - writeBrowserMemory( - latestSavedRuntimeMemoryStorageKey, - normalizedValue - ); - - } - - function setBootSourceRuntimeSessionId( sourceRuntimeSessionId ) { @@ -2096,579 +2425,64 @@ } - function cloneRuntimeMemoryToCurrentSession( - runtimeMemory + function hydrateLiveRuntimeMemoryFromCheckpoint( + checkpoint ) { - if (shouldIsolateAnonymousStorage()) { - return; - } - if ( - !runtimeMemory - || typeof runtimeMemory !== "object" - || readBrowserMemory(latestRuntimeMemoryStorageKey) + !checkpoint + || typeof checkpoint !== "object" + || Array.isArray(checkpoint) + || !String(checkpoint.runtime_memory || "").trim() ) { - return; + return false; } + const sourceSessionId = + String(checkpoint.session_id || "").trim(); + setBootSourceRuntimeSessionId( - runtimeMemory.session_id + sourceSessionId ); writeLatestRuntimeMemory({ - version: - runtimeMemory.version || 1, - // Opening a tab is not conversation activity. Keep the predecessor's - // causal timestamp until a real user move is checkpointed; otherwise an - // untouched tab can outrank the session the user actually used. + version: 2, saved_at: - String(runtimeMemory.saved_at || "").trim() - || new Date().toISOString(), + String(checkpoint.saved_at || "").trim(), runtime_memory: - runtimeMemory.runtime_memory || "", + checkpoint.runtime_memory || "", runtime_memory_updates: - runtimeMemory.runtime_memory_updates || 0, + checkpoint.runtime_memory_updates || 0, runtime_snapshot: buildPersistedRuntimeSnapshot( - runtimeMemory.runtime_snapshot + checkpoint.runtime_snapshot ), cloned_from_session_id: - runtimeMemory.session_id || null, + sourceSessionId || null, previous_session_id: - runtimeMemory.session_id || null, + sourceSessionId || null, + conversation_committed_at: + String( + checkpoint.conversation_committed_at || "" + ).trim(), }); - } - - - function cloneRuntimeMemoryFromSessionId( - sourceRuntimeSessionId - ) { - - const normalizedSourceRuntimeSessionId = - String(sourceRuntimeSessionId || "").trim(); - - if (!normalizedSourceRuntimeSessionId) { - return; - } - - const sourceRuntimeMemory = - readBrowserMemory( - getLatestRuntimeMemoryStorageKey( - normalizedSourceRuntimeSessionId - ) - ); - - cloneRuntimeMemoryToCurrentSession( - sourceRuntimeMemory - ); - - } - - - function cloneBootRuntimeMemoryIfNeeded() { - - if (!clonedRuntimeSessionId) { - return; - } - - // Do not copy live latestRuntimeMemory across a page reload. That cache is - // only safe for in-page WebSocket reconnects. Saved session restore uses - // latest saved checkpoint/latestSavedRuntimeMemory instead. - clonedRuntimeSessionId = null; - - } - - - function collectOtherLatestRuntimeMemorySnapshots() { - - if (shouldIsolateAnonymousStorage()) { - return []; - } - - const snapshots = []; - - try { - for ( - let index = window.localStorage.length - 1; - index >= 0; - index -= 1 - ) { - const key = - window.localStorage.key(index); - - if ( - !isLatestRuntimeMemoryKey(key) - || key === latestRuntimeMemoryStorageKey - ) { - continue; - } - - const keySessionId = - getSessionIdFromLatestRuntimeMemoryKey( - key - ); - - if (keySessionId === runtimeSessionId) { - continue; - } - - const value = - sanitizeRuntimeMemoryRecord( - readBrowserMemory( - key - ) - ); - - snapshots.push({ - key, - key_session_id: keySessionId, - session_id: - ( - value - && value.session_id - ) - || keySessionId - || null, - saved_at: - ( - value - && value.saved_at - ) - || null, - conversation_committed_at: - ( - value - && value.conversation_committed_at - ) - || null, - session_snapshot: - ( - value - && value.session_snapshot - && typeof value.session_snapshot === "object" - && !Array.isArray(value.session_snapshot) - ) - ? { - ...value.session_snapshot, - } - : null, - runtime_memory_updates: - ( - value - && value.runtime_memory_updates - ) - || 0, - runtime_memory: - ( - value - && value.runtime_memory - ) - || "", - runtime_snapshot: - ( - value - && value.runtime_snapshot - && typeof value.runtime_snapshot === "object" - ) - ? value.runtime_snapshot - : null, - booted_from_session_id: - ( - value - && value.booted_from_session_id - ) - || null, - previous_session_id: - ( - value - && ( - value.previous_session_id - || value.booted_from_session_id - ) - ) - || null, - }); - } - } catch (error) { - return []; - } - - return snapshots.sort( - function ( - left, - right, - ) { - return String( - right.saved_at || "" - ).localeCompare( - String(left.saved_at || "") - ); - } - ); - - } - - - function readLatestPreviousRuntimeMemory() { - - if (shouldIsolateAnonymousStorage()) { - return null; - } - - const snapshots = - collectOtherLatestRuntimeMemorySnapshots(); - - if (!snapshots.length) { - return null; - } - - const latest = snapshots[0]; - const value = readBrowserMemory( - latest.key - ); - - if ( - !value - || typeof value !== "object" - || Array.isArray(value) - ) { - return null; - } - - const sourceSessionId = - String( - value.session_id - || latest.key_session_id - || "" - ).trim(); - - if (!sourceSessionId) { - return null; - } - - return { - ...value, - session_id: sourceSessionId, - storage_key: latest.key, - }; - - } - - - function readLatestCompletedConversationRuntimeMemory() { - - if (shouldIsolateAnonymousStorage()) { - return null; - } - - const snapshots = - collectOtherLatestRuntimeMemorySnapshots() - .filter(snapshot => ( - snapshot - && String( - snapshot.conversation_committed_at - || "" - ).trim() - && String( - snapshot.runtime_memory - || "" - ).trim() - )) - .sort((left, right) => ( - String( - right.conversation_committed_at - || "" - ).localeCompare( - String( - left.conversation_committed_at - || "" - ) - ) - )); - - if (!snapshots.length) { - return null; - } - - const latest = snapshots[0]; - const value = readBrowserMemory( - latest.key - ); - - if ( - !value - || typeof value !== "object" - || Array.isArray(value) - ) { - return null; - } - - const sourceSessionId = - String( - value.session_id - || latest.key_session_id - || "" - ).trim(); - - if (!sourceSessionId) { - return null; - } - - return { - ...value, - session_id: sourceSessionId, - storage_key: latest.key, - }; - - } - - - function clearOtherLatestRuntimeMemorySnapshots() { - - if (shouldIsolateAnonymousStorage()) { - return { - cleared: 0, - }; - } - - const snapshots = - collectOtherLatestRuntimeMemorySnapshots(); - - try { - snapshots.forEach( - function ( - snapshot - ) { - if ( - snapshot - && snapshot.key - && snapshot.key !== latestRuntimeMemoryStorageKey - ) { - window.localStorage.removeItem( - snapshot.key - ); - } - } - ); - } catch (error) { - // Browser memory cleanup is helpful, not required for chat. - } - - return { - cleared: snapshots.length, - keys: snapshots.map( - function ( - snapshot - ) { - return snapshot.key; - } - ), - }; - - } - - - function extractSavedRuntimeConstant( - source, - name - ) { - - const normalizedSource = - String(source || "").replace( - /\r\n/g, - "\n" - ); - - const markerIndex = - normalizedSource.indexOf( - name - ); - - if (markerIndex < 0) { - return ""; - } - - const assignmentIndex = - normalizedSource.indexOf( - "=", - markerIndex + name.length - ); - - if (assignmentIndex < 0) { - return ""; - } - - const afterAssignment = - normalizedSource.slice( - assignmentIndex + 1 - ); - - const openingMatch = - afterAssignment.match( - /["'`]/ - ); - - if (!openingMatch) { - return ""; - } - - const quote = - openingMatch[0]; - - const valueStart = - assignmentIndex + 1 + openingMatch.index + 1; - - const closingIndex = - normalizedSource.indexOf( - `\n${quote}`, - valueStart - ); - - if (closingIndex < 0) { - return ""; - } - - return normalizedSource.slice( - valueStart, - closingIndex - ).trim(); - - } - - - function parseSavedRuntimeText( - source - ) { - - const runtimeMemory = - extractSavedRuntimeConstant( - source, - "SAVED_RUNTIME" - ); - - if (!runtimeMemory) { - return null; - } - - return { - runtime_memory: runtimeMemory, - source: "saved_runtime_txt", - }; - - } - - - function buildSavedRuntimeFallback( - memory - ) { - - if (!memory) { - return null; - } - - const runtimeMemory = - ( - memory.runtime_memory - && String(memory.runtime_memory).trim() - ) - || ""; - - if (!runtimeMemory) { - return null; - } - - const source = - memory.source || "saved_runtime_txt"; - const savedAt = - new Date().toISOString(); - - return { - source, - latest_saved_runtime_memory: { - version: 1, - saved_at: savedAt, - runtime_memory: runtimeMemory, - runtime_memory_updates: 1, - runtime_snapshot: null, - }, - runtime_memory: { - version: 1, - saved_at: savedAt, - runtime_memory: runtimeMemory, - runtime_memory_updates: 1, - runtime_snapshot: null, - }, - }; - - } - - - function getSavedRuntimeMemoryFallback() { - - return buildSavedRuntimeFallback( - savedRuntimeFileFallback - ); - - } - - - async function loadSavedRuntimeMemoryFallback() { - - if (savedRuntimeFileFallbackLoaded) { - return savedRuntimeFileFallback; - } - - savedRuntimeFileFallbackLoaded = true; - - if ( - !window.fetch - || !savedRuntimeFallbackPath - ) { - return null; - } - - try { - const response = - await window.fetch( - savedRuntimeFallbackPath, - { - cache: "no-store", - } - ); - - if (!response.ok) { - return null; - } - - savedRuntimeFileFallback = - parseSavedRuntimeText( - await response.text() - ); - } catch (error) { - savedRuntimeFileFallback = null; - } - - return savedRuntimeFileFallback; + return true; } const storage = { keys: { - latestSavedSessionSnapshotStorageKey, + liveRuntimeMemoryStorageKey, + sessionCheckpointStorageKey, runtimeSessionIdSessionStorageKey, - latestRuntimeMemoryStorageKeyPrefix, - latestRuntimeMemoryStorageKeyVersion, - latestSavedRuntimeMemoryStorageKey, activeMemoryStorageKey, anonymousActiveMemoryStorageKey, delayedMemoryReportsStorageKey, anonymousDelayedMemoryReportsStorageKey, factsMemoryStorageKeyPrefix, factsMemoryStorageKeyVersion, - savedRuntimeFallbackPath, }, getRuntimeSessionId, getCurrentRuntimeSessionId, @@ -2676,10 +2490,6 @@ setCurrentFactsMemorySessionId, setRuntimeSessionId, generateRuntimeSessionId, - getLatestRuntimeMemoryStorageKey, - getCurrentLatestRuntimeMemoryStorageKey, - isLatestRuntimeMemoryKey, - getSessionIdFromLatestRuntimeMemoryKey, readBrowserMemory, writeBrowserMemory, removeBrowserMemory, @@ -2692,10 +2502,13 @@ getDelayedMemoryReportsStorageKey, readLatestRuntimeMemory, writeLatestRuntimeMemory, - readLatestSavedSessionSnapshot, - writeLatestSavedSessionSnapshot, - readLatestSavedRuntimeMemory, - writeLatestSavedRuntimeMemory, + readSessionCheckpoint, + readSessionCheckpointRecord, + writeSessionCheckpoint, + clearSessionCheckpoint, + clearLiveRuntimeMemory, + markSessionCheckpointUserActivity, + ensureSessionCheckpointMigration, normalizeActiveMemoryRecords, readActiveMemoryRecords, writeActiveMemoryRecords, @@ -2724,22 +2537,9 @@ mergeDelayedMemoryReports, setBootSourceRuntimeSessionId, buildPersistedRuntimeSnapshot, - cloneRuntimeMemoryToCurrentSession, - cloneRuntimeMemoryFromSessionId, - cloneBootRuntimeMemoryIfNeeded, - collectOtherLatestRuntimeMemorySnapshots, - readLatestPreviousRuntimeMemory, - readLatestCompletedConversationRuntimeMemory, - clearOtherLatestRuntimeMemorySnapshots, - extractSavedRuntimeConstant, - parseSavedRuntimeText, - buildSavedRuntimeFallback, - getSavedRuntimeMemoryFallback, - loadSavedRuntimeMemoryFallback, + hydrateLiveRuntimeMemoryFromCheckpoint, }; window.JinRuntime.storage = storage; - window.jinSavedRuntimeFallbackReady = - loadSavedRuntimeMemoryFallback(); }()); diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index f544e6e0..eaf8a37a 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -164,17 +164,9 @@ const { } = memoryModel; const { - keys: runtimeStorageKeys, - removeBrowserMemory, readLatestRuntimeMemory, writeLatestRuntimeMemory, - readLatestSavedRuntimeMemory, - writeLatestSavedRuntimeMemory, buildPersistedRuntimeSnapshot, - cloneBootRuntimeMemoryIfNeeded, - collectOtherLatestRuntimeMemorySnapshots, - clearOtherLatestRuntimeMemorySnapshots, - getSavedRuntimeMemoryFallback, readActiveMemoryRecords, writeActiveMemoryRecords, clearActiveMemoryRecords, diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index cb49d8d5..26a20562 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -394,7 +394,7 @@ if ( !storage - || typeof storage.readLatestSavedSessionSnapshot + || typeof storage.readSessionCheckpoint !== "function" ) { return merged; @@ -407,75 +407,37 @@ || "" ).trim(); const checkpoint = - storage.readLatestSavedSessionSnapshot(); - const runtimeRecords = []; - const latestSavedRuntime = - typeof storage.readLatestSavedRuntimeMemory === "function" - ? storage.readLatestSavedRuntimeMemory() - : null; - - if (latestSavedRuntime) { - runtimeRecords.push(latestSavedRuntime); - } - if ( - typeof storage.collectOtherLatestRuntimeMemorySnapshots - === "function" - ) { - const collected = - storage.collectOtherLatestRuntimeMemorySnapshots(); - if (Array.isArray(collected)) { - runtimeRecords.push(...collected); - } - } + storage.readSessionCheckpoint(); const timestamp = value => { const parsed = Date.parse(String(value || "").trim()); return Number.isFinite(parsed) ? parsed : 0; }; - const candidates = []; - const addCandidate = (record, snapshot) => { - if ( - !record - || String( - record.session_id - || record.key_session_id - || "" - ).trim() !== restoreSourceSessionId - || !snapshot - || typeof snapshot !== "object" - || Array.isArray(snapshot) - ) { - return; - } - - candidates.push({ - record, - snapshot, - savedAt: Math.max( - timestamp(record.conversation_committed_at), - timestamp(record.saved_at), + const snapshot = + checkpoint + && checkpoint.session_snapshot; + const checkpointMatchesRestore = Boolean( + checkpoint + && String(checkpoint.session_id || "").trim() + === restoreSourceSessionId + && snapshot + && typeof snapshot === "object" + && !Array.isArray(snapshot) + ); + const checkpointSavedAt = checkpointMatchesRestore + ? Math.max( + timestamp(checkpoint.conversation_committed_at), + timestamp(checkpoint.saved_at), timestamp( snapshot.room_state && snapshot.room_state.saved_at ) - ), - }); - }; - - addCandidate( - checkpoint, - checkpoint && checkpoint.session_snapshot - ); - runtimeRecords.forEach(record => { - addCandidate(record, record.session_snapshot); - }); - candidates.sort((left, right) => right.savedAt - left.savedAt); - - const selected = candidates[0]; + ) + : 0; const archiveTailAt = timestamp(merged.archive_tail_at); if ( - !selected - || (archiveTailAt && selected.savedAt < archiveTailAt) + !checkpointMatchesRestore + || (archiveTailAt && checkpointSavedAt < archiveTailAt) ) { return merged; } @@ -496,11 +458,11 @@ ].forEach(fieldName => { if ( Object.prototype.hasOwnProperty.call( - selected.snapshot, + snapshot, fieldName ) ) { - merged[fieldName] = selected.snapshot[fieldName]; + merged[fieldName] = snapshot[fieldName]; } }); diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index b05b9afd..ac011a0a 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -224,10 +224,6 @@ window.requestJinLastResponseRetry = function () { return false; } - if (window.markSessionActivityDirty) { - window.markSessionActivityDirty(); - } - setGenerationState( true ); @@ -742,14 +738,6 @@ async function handleSocketOpen() { } } - if (window.jinSavedRuntimeFallbackReady) { - try { - await window.jinSavedRuntimeFallbackReady; - } catch (error) { - // File fallback is optional. Browser memory still works. - } - } - if ( !ws || ws.readyState !== WebSocket.OPEN @@ -1010,10 +998,6 @@ async function initializeSocketClient() { } } - if (typeof logOtherLatestRuntimeMemorySnapshots === "function") { - logOtherLatestRuntimeMemorySnapshots(); - } - if (typeof logActiveMemoryRecords === "function") { logActiveMemoryRecords(); } diff --git a/ui/static/js/socket/input.js b/ui/static/js/socket/input.js index 4134dcb9..abaa67e8 100644 --- a/ui/static/js/socket/input.js +++ b/ui/static/js/socket/input.js @@ -336,10 +336,6 @@ chatForm.addEventListener( ); } - if (window.markSessionActivityDirty) { - window.markSessionActivityDirty(); - } - setGenerationState( true ); @@ -421,7 +417,14 @@ chatForm.addEventListener( const sent = sendSocketMessage(payload); - void sent; + if ( + sent + && window.markSessionActivityDirty + ) { + // Only a successfully emitted real USER move may replace a cleared + // checkpoint tombstone. Retry/bootstrap/reconnect paths do not call this. + window.markSessionActivityDirty(); + } if (window.jinFreezeUserIdleTimerAtSeconds) { window.jinFreezeUserIdleTimerAtSeconds( diff --git a/ui/static/js/socket/memory.js b/ui/static/js/socket/memory.js index 8970f787..86ace792 100644 --- a/ui/static/js/socket/memory.js +++ b/ui/static/js/socket/memory.js @@ -1,4 +1,3 @@ -let latestRuntimeSnapshotsLogged = false; let activeMemoryRecordsLogged = false; let factsMemoryRecordsLogged = false; @@ -32,100 +31,6 @@ const MEMORY_GLOW_STAGES = { }, }; -function buildLatestRuntimeSnapshotsDetails( - snapshots -) { - - const lines = [ - "current_runtime_session_id: " - + String(window.jinRuntimeSessionId || websocketClientId), - "", - "current_key: " - + String( - window.getCurrentLatestRuntimeMemoryStorageKey - ? window.getCurrentLatestRuntimeMemoryStorageKey() - : "" - ), - ]; - - snapshots.forEach( - function ( - snapshot, - index, - ) { - const runtimeMemory = - String(snapshot.runtime_memory || "") - .replace(/\\n/g, "\n") - .replace( - /;\s+(?=[a-z][a-z0-9_]*\s*:)/g, - "\n" - ) - .split(/\r?\n+/) - .map(function (line) { - return line.trim(); - }) - .filter(Boolean); - - lines.push( - "", - `[ snapshot ${index + 1} ]`, - "", - `key: ${snapshot.key || ""}`, - "", - `key_session_id: ${snapshot.key_session_id || ""}`, - "", - `session_id: ${snapshot.session_id || ""}`, - "", - `saved_at: ${snapshot.saved_at || ""}`, - "", - `runtime_memory_updates: ${snapshot.runtime_memory_updates || 0}` - ); - - if (runtimeMemory.length) { - lines.push( - "", - "runtime_memory:", - "", - runtimeMemory.join("\n\n") - ); - } - } - ); - - return lines.join("\n"); - -} - -function logOtherLatestRuntimeMemorySnapshots() { - - if ( - latestRuntimeSnapshotsLogged - || !window.getOtherLatestRuntimeMemorySnapshots - ) { - return; - } - - const snapshots = - window.getOtherLatestRuntimeMemorySnapshots(); - - if (!snapshots.length) { - return; - } - - latestRuntimeSnapshotsLogged = true; - - appendLog( - "[LATEST SNAPSHOTS]", - `${snapshots.length} stale latest runtime snapshot` - + `${snapshots.length === 1 ? "" : "s"} found.`, - buildLatestRuntimeSnapshotsDetails( - snapshots - ) - ); - -} - - function getFactsMemoryRecordsForStartupLog() { const storage = diff --git a/ui/templates/index.html b/ui/templates/index.html index 4ddad0e8..c99e969b 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -520,38 +520,38 @@ - + - + - + - + - + - - - + + + - + From bc9728b0cd7bac54644879190e2ee5c1b61d6020 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:16:45 +0300 Subject: [PATCH 111/168] - rollback session bootstrap instruction --- rules/runtime.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rules/runtime.py b/rules/runtime.py index 1a4ee804..4e98e219 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -34,14 +34,14 @@ SESSION_RESTORE_REASONING_COUNT = 2 SESSION_RESTORE_REASONING_CHAR_LIMIT = 2000 SESSION_RESTORE_MESSAGE = ( - "\n" - "Current session was bootstrapped in a browser tab!\n" - "RESTORED_SESSION_DIALOG is the authoritative newest visible conversation state.\n" - "RUNTIME_MEMORY is background state and may predate the last restored dialogue turn; never treat its keys as a newer user message.\n" - "Continue from the latest USER entry in RESTORED_SESSION_DIALOG and answer in that user's language.\n" - "Think deep and fresh, extract the conversation's tone and resume it fluently.\n" - "Respond briefly and naturally without summarizing or re-introducing the restored exchange unless the user asks.\n" - "" + "\n" + "This is session boostrap system message!\n" + "User opened runtime in a new tab!\n" + "You must think and derive current time and pending tasks before continue!\n" + "Analyze PREVIOUS_CHAT_MESSAGES and answer in language of user message.\n" + "Think deep and fresh, extract all vibe and re-enable the archived conversation as fluently as possible.\n" + "Respond briefly and naturally, quickly acknowledge your presence and make it easy for the user to continue from exactly where we left off and move on.\n" + "" ) RUNTIME_ACTIONS_RULES = "" RUNTIME_ACTIONS_RULES_ = ( From d344a0a20ac28f27d500a06b6d8bd987934fb0b8 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:32:04 +0300 Subject: [PATCH 112/168] chore: ui - fix for L-T fact delete lazy load - fix for note citation - fix for L-T tooltip visibility on scroll - fix for soft recconnect race frame writing - update context arrange, runtime fields and remove message counters --- agent/nodes/brain.py | 20 ++- rules/brain_context_builder.py | 93 ++++++++----- runtime/L1_memory.py | 42 +++++- runtime/L1_memory_utils.py | 35 +++++ runtime/deep_web_search.py | 8 +- runtime/memory_common.py | 16 ++- runtime/runtime_context.py | 22 ++- runtime/stream.py | 12 ++ tests/test_attached_files_client_contract.py | 2 +- tests/test_brain_asset_flow.py | 6 +- tests/test_brain_prompt_memory.py | 128 +++++++++++++++-- tests/test_brain_runtime_actions.py | 34 +++-- ..._chat_reasoning_spacing_client_contract.py | 8 +- tests/test_current_context_window.py | 4 +- tests/test_l1_memory.py | 5 + tests/test_l1_reconnect_resume.py | 62 +++++++++ tests/test_l4_hover_card_client_contract.py | 24 +++- tests/test_live_session_checkpoint.py | 21 +++ ..._memory_citation_layers_client_contract.py | 15 +- ...t_reconnect_persistence_client_contract.py | 28 ++++ ...untime_memory_lazy_rows_client_contract.py | 16 ++- tests/test_websocket_pending_usage.py | 48 +++++++ ui/static/css/chat.css | 130 +++++++----------- ui/static/css/runtime-memory.css | 10 +- ui/static/js/runtime/runtime-memory-view.js | 79 ++++++++++- ui/static/js/runtime/runtime-session.js | 40 ++++++ ui/static/js/session-restore.js | 4 + ui/static/js/think-citations.js | 5 +- ui/templates/index.html | 6 +- utils/context/runtime_state.py | 59 ++++++-- utils/current_context_window.py | 1 + utils/session_restore.py | 1 + websocket/bootstrap.py | 110 +++++++++++++++ websocket/messages.py | 3 + 34 files changed, 919 insertions(+), 178 deletions(-) diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index a805cfb6..1b10f531 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -891,12 +891,19 @@ def rename_runtime_memory_for_followup( system_prompt or "" ) - opening_tag_prefix = "" closing_index = prompt.find( closing_tag, opening_end_index + 1, diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index ef6a853b..d9d22c25 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -166,6 +166,7 @@ def _append_visible_session_state( format_session_state, ) from utils.context.runtime_state import ( + get_current_session_user_message_count, get_visible_assistant_message_count, get_visible_turn_count, ) @@ -178,7 +179,9 @@ def _append_visible_session_state( turn_number=get_visible_turn_count( context ), - user_message_count=getattr(context, "user_message_count", 0), + user_message_count=get_current_session_user_message_count( + context + ), assistant_message_count=get_visible_assistant_message_count( context ), @@ -296,6 +299,7 @@ def _append_L1_runtime_memory( *, user_input: str = "", commit_active_memory_refresh: bool = False, + previous_chat_messages_context: str = "", ) -> None: from runtime.L1_memory_utils import ( @@ -441,6 +445,11 @@ def _append_L1_runtime_memory( "" ) + if previous_chat_messages_context: + parts.append( + previous_chat_messages_context + ) + if runtime_memory.strip(): snapshots = getattr( context, @@ -486,6 +495,32 @@ def _append_L1_runtime_memory( ) ) + frame_memory_index = int( + getattr( + context, + "runtime_memory_display_index_offset", + 0, + ) + or 0 + ) + if isinstance(latest_snapshot, dict): + try: + frame_memory_index += int( + latest_snapshot.get( + "index", + len(snapshots) - 1, + ) + or 0 + ) + except (TypeError, ValueError): + frame_memory_index += max( + len(snapshots) - 1, + 0, + ) + + frame_memory_tag = ( + f"FRAME_MEMORY_{max(frame_memory_index, 0)}" + ) runtime_memory_attrs = [ f'ts="{escape(snapshot_timestamp)}"' ] @@ -495,9 +530,9 @@ def _append_L1_runtime_memory( ) parts.append( - f'\n' + f'<{frame_memory_tag} {" ".join(runtime_memory_attrs)}>\n' f"{indent_xml(escape(canonicalize_runtime_memory_text(runtime_memory)))}\n" - "" + f"" ) @@ -1155,6 +1190,14 @@ def build_brain_context( False, ) ) + restored_session_dialog = str( + getattr( + context, + "runtime_restored_session_dialog", + "", + ) + or "" + ).strip() current_runtime_settings_context = ( build_current_runtime_settings_context() @@ -1304,12 +1347,25 @@ def build_brain_context( context, ) - # L1 memory block: includes active memory records and live runtime memory. + previous_chat_messages_context = ( + build_previous_chat_messages_context( + context + ) + if ( + include_previous_chat_messages + and not restored_session_dialog + ) + else "" + ) + + # Keep the recent visible dialogue directly above the FRAME snapshot so + # those two views of the live conversation stay adjacent in the prompt. _append_L1_runtime_memory( runtime_context_parts, context, user_input=user_input, commit_active_memory_refresh=commit_active_memory_refresh, + previous_chat_messages_context=previous_chat_messages_context, ) # Visible session state block: records visible turn and message counters. @@ -1362,37 +1418,12 @@ def build_brain_context( ) ) - # Archived-session checkout gets one exact-dialogue priming turn before - # falling back to the normal rolling recent-chat window. This preserves - # the original conversational trajectory without permanently bloating - # every subsequent prompt. - restored_session_dialog = str( - getattr( - context, - "runtime_restored_session_dialog", - "", - ) - or "" - ).strip() - + # Archived-session checkout still gets its one-shot exact dialogue dump. + # Normal rolling chat already lives beside FRAME_MEMORY above. if restored_session_dialog: prompt_parts.append( restored_session_dialog ) - else: - # Previous chat messages block: gives the brain the recent visible dialogue. - previous_chat_messages_context = ( - build_previous_chat_messages_context( - context - ) - if include_previous_chat_messages - else "" - ) - - if previous_chat_messages_context: - prompt_parts.append( - previous_chat_messages_context - ) # Previous reasoning block: a restore tick receives a one-shot raw dump of # the latest archived reasonings (newest first). Ordinary turns keep the diff --git a/runtime/L1_memory.py b/runtime/L1_memory.py index b3c74d74..94af4e09 100644 --- a/runtime/L1_memory.py +++ b/runtime/L1_memory.py @@ -44,14 +44,17 @@ from runtime.L1_memory_utils import ( build_empty_assistant_message, build_interrupted_assistant_message, + build_last_jin_response_fallback, build_runtime_response_feedback_value, build_runtime_memory_batch_user_prompt, build_runtime_memory_snapshot, build_runtime_memory_user_prompt, enforce_runtime_turn_fields, + find_runtime_memory_entry_value, get_strength_zones, normalize_compound_runtime_memory_lines, parse_runtime_memory_lines, + quote_runtime_user_message_value, remove_runtime_memory_placeholder_lines, remove_runtime_response_feedback_text, remove_runtime_user_idle_lines, @@ -1110,7 +1113,44 @@ def resume_runtime_memory_pending_update( # Keep the checkpoint through a successful server commit until the # browser proves it persisted a newer L1 snapshot. That closes the # crash window between commit and websocket delivery/localStorage. - if current_updates > base_updates: + pending_turn_already_committed = False + + if len(pending_turns) == 1: + pending_turn = pending_turns[0] + current_memory = str( + getattr( + context, + "runtime_memory", + "", + ) + or "" + ) + current_user_message = find_runtime_memory_entry_value( + current_memory, + "user_message", + ) + current_jin_response = find_runtime_memory_entry_value( + current_memory, + "last_jin_response", + ) + pending_turn_already_committed = bool( + current_user_message + == quote_runtime_user_message_value( + pending_turn.get("user_message", "") + ) + and current_jin_response + and build_last_jin_response_fallback( + current_jin_response + ) + == build_last_jin_response_fallback( + pending_turn.get("assistant_message", "") + ) + ) + + if ( + current_updates > base_updates + or pending_turn_already_committed + ): context.runtime_memory_pending_turns = [] clear_pending_l1_update( context diff --git a/runtime/L1_memory_utils.py b/runtime/L1_memory_utils.py index 05c4d6ae..d0228797 100644 --- a/runtime/L1_memory_utils.py +++ b/runtime/L1_memory_utils.py @@ -3260,6 +3260,25 @@ def build_runtime_memory_snapshot( "assistant_message_count", 0, ), + # Keep the causal L1 revision inside the snapshot itself. The browser + # prefers its newest in-memory snapshot during a soft reconnect; if + # this field is missing it falls back to zero and the backend can + # replay an already committed crash-recovery journal after restart. + "runtime_memory_updates": getattr( + context, + "runtime_memory_updates", + 0, + ), + "current_session_user_message_count": getattr( + context, + "current_session_user_message_count", + 0, + ), + "current_session_assistant_message_count": getattr( + context, + "current_session_assistant_message_count", + 0, + ), "created_at": format_runtime_memory_lifecycle_timestamp( snapshot_time ), @@ -3510,6 +3529,22 @@ def _dict_list(value, *, limit: int | None = None) -> list[dict]: ) or 0 ), + "current_session_user_message_count": int( + getattr( + context, + "current_session_user_message_count", + 0, + ) + or 0 + ), + "current_session_assistant_message_count": int( + getattr( + context, + "current_session_assistant_message_count", + 0, + ) + or 0 + ), "runtime_memory_updates": int( getattr( context, diff --git a/runtime/deep_web_search.py b/runtime/deep_web_search.py index 2bae571c..17c4c746 100644 --- a/runtime/deep_web_search.py +++ b/runtime/deep_web_search.py @@ -352,14 +352,18 @@ def build_compact_runtime_snapshot( ) selected_blocks = [] - for tag in ( + for tag_pattern in ( + r"FRAME_MEMORY_\d+", "RUNTIME_MEMORY", "ACTIVE_MEMORY", "DELAYED_MEMORY", "LONG_TERM_MEMORY", ): match = re.search( - rf"<{tag}(?:\s[^>]*)?>.*?", + ( + rf"<(?P{tag_pattern})(?:\s[^>]*)?>" + rf".*?" + ), system_prompt, flags=re.IGNORECASE | re.DOTALL, ) diff --git a/runtime/memory_common.py b/runtime/memory_common.py index dd2db181..a9a0e87b 100644 --- a/runtime/memory_common.py +++ b/runtime/memory_common.py @@ -96,8 +96,12 @@ def build_runtime_summarizer_trusted_context( user_message_count = ( getattr( context, - "user_message_count", - None, + "current_session_user_message_count", + getattr( + context, + "user_message_count", + None, + ), ) if context is not None else None @@ -105,8 +109,12 @@ def build_runtime_summarizer_trusted_context( assistant_message_count = ( getattr( context, - "assistant_message_count", - None, + "current_session_assistant_message_count", + getattr( + context, + "assistant_message_count", + None, + ), ) if context is not None else None diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index d51b8c8a..cc081adf 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -258,6 +258,11 @@ class RuntimeContext: runtime_memory_updates: int = 0 + # Offset that maps the server snapshot index to the FRAME number shown in + # the right-panel UI. Fresh sessions start at 0; restored baselines can + # start at 1, and soft reconnects can resume at any visible FRAME number. + runtime_memory_display_index_offset: int = 0 + runtime_pattern_counter: int = 0 runtime_repeated_input_count: int = 0 @@ -292,6 +297,13 @@ class RuntimeContext: assistant_message_count: int = 0 + # CURRENT_SESSION_STATE is scoped to this live runtime session. The + # counters above remain monotonic across predecessor bootstrap because + # Active Memory lifecycle metadata uses that message scale. + current_session_user_message_count: int = 0 + + current_session_assistant_message_count: int = 0 + runtime_memory_pending_turns: list[dict] = field( default_factory=list ) @@ -558,8 +570,7 @@ class ContextContract: compressed_history: str = "" system_state: str = "ACTIVE" current_session_id: str = "" - service_model_uid: str = "" - brain_model_uid: str = "" + current_model_uid: str = "" current_context_window: str = "" jin_color: str = DEFAULT_JIN_COLOR jin_size_context: str = "" @@ -590,11 +601,8 @@ def build_runtime_fields(self) -> str: if self.current_session_id: fields["CURRENT_SESSION_ID"] = self.current_session_id - if self.service_model_uid: - fields["SERVICE_MODEL_UID"] = self.service_model_uid - - if self.brain_model_uid: - fields["BRAIN_MODEL_UID"] = self.brain_model_uid + if self.current_model_uid: + fields["CURRENT_MODEL_UID"] = self.current_model_uid if self.current_context_window: fields["CURRENT_CONTEXT_WINDOW"] = ( diff --git a/runtime/stream.py b/runtime/stream.py index b27debfe..22e3c0f3 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -789,6 +789,18 @@ def build_message_end_checkpoint_payload(self) -> dict: ) + 1 ) + session_snapshot[ + "current_session_assistant_message_count" + ] = ( + int( + session_snapshot.get( + "current_session_assistant_message_count", + 0, + ) + or 0 + ) + + 1 + ) session_snapshot["turn_number"] = ( int( session_snapshot.get( diff --git a/tests/test_attached_files_client_contract.py b/tests/test_attached_files_client_contract.py index 8836bfee..b7cd681d 100644 --- a/tests/test_attached_files_client_contract.py +++ b/tests/test_attached_files_client_contract.py @@ -84,7 +84,7 @@ def test_files_panel_shows_file_count_and_reuses_100px_image_hover_preview(): assert 'String(record.id || "")' in runtime_view assert "hoverPreviewMaxPx: 100" in runtime_view assert "window.bindJinAttachmentHoverPreview" in attachments - assert "/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1&delayed-fact-paste=2&numeric-fact-order=1&l4-fact-age=1&hold-delete-1&modal-title-editor=1&active-citations=4&runtime-memory-title=2&lazy-rows=5&hover-title=1&row-payload=1&l4-citation-gate=1&delayed-pin-link-isolation=1&delayed-highlight-contract=1&active-memory-activity-order=1&l4-two-line-layout=1&l4-value-fill=1&l4-priority-bubble=1&delayed-id-citations=1" in index + assert "/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1&delayed-fact-paste=2&numeric-fact-order=1&l4-fact-age=1&hold-delete-1&modal-title-editor=1&active-citations=4&runtime-memory-title=2&lazy-rows=6&hover-title=1&row-payload=1&l4-citation-gate=1&delayed-pin-link-isolation=1&delayed-highlight-contract=1&active-memory-activity-order=1&l4-two-line-layout=1&l4-value-fill=1&l4-priority-bubble=1&delayed-id-citations=1" in index assert "/static/js/chat-attachments.js?v=attached-files-3-attachment-modal-id-1&hold-delete-1&runtime-action-preview-2" in index diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index cece1670..63ab1303 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -658,7 +658,7 @@ async def test_followup_renames_runtime_memory_block(self): prompt = BrainNode.build_followup_system_prompt( ( "\nactive memory\n\n\n" - '\nactive_topic: test\n\n\n' + '\nactive_topic: test\n\n\n' "\npattern\n" ), "continue the task", @@ -670,11 +670,11 @@ async def test_followup_renames_runtime_memory_block(self): prompt, ) self.assertNotIn( - "", + "", prompt, ) self.assertIn( diff --git a/tests/test_brain_prompt_memory.py b/tests/test_brain_prompt_memory.py index 4daf3be6..1979d851 100644 --- a/tests/test_brain_prompt_memory.py +++ b/tests/test_brain_prompt_memory.py @@ -54,7 +54,7 @@ def test_first_brain_prompt_includes_default_runtime_memory(self): ) self.assertIn( - "' ), prompt, ) + def test_frame_memory_number_matches_ui_and_chat_sits_directly_above_it(self): + + context = RuntimeContext( + websocket=object(), + emitter=object(), + logger=object(), + clients={}, + ) + context.runtime_memory = "topic: numbered frame" + context.runtime_memory_display_index_offset = 1 + context.runtime_memory_snapshots = [ + { + "index": 4, + "timestamp": "2026-08-28T18:41:32+03:00", + "session_id": "frame-session", + } + ] + context.runtime_recent_turns = [ + { + "user": "latest user message", + "jin": "latest jin message", + } + ] + + prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + ) + + self.assertIn( + ( + '' + ), + prompt, + ) + self.assertIn( + ( + "\n" + '' + ), + prompt, + ) + def test_brain_prompt_places_user_idle_in_runtime_memory(self): context = RuntimeContext( @@ -116,7 +165,7 @@ def test_brain_prompt_places_user_idle_in_runtime_memory(self): prompt, ) self.assertIn( - ""), - prompt.index(""), @@ -1154,6 +1203,52 @@ def test_brain_prompt_does_not_count_runtime_actions_as_messages(self): session_state, ) + def test_current_session_state_uses_session_local_counters(self): + + context = RuntimeContext( + websocket=object(), + emitter=object(), + logger=object(), + clients={}, + ) + context.user_message_count = 699 + context.assistant_message_count = 699 + context.turn_number = 699 + context.current_session_user_message_count = 2 + context.current_session_assistant_message_count = 2 + + prompt = build_brain_context( + context=context, + runtime_actions={ + "CAN_WEB_SEARCH": False, + }, + ) + + session_state = prompt.split( + "", + 1, + )[1].split( + "", + 1, + )[0] + + self.assertIn( + "User messages count: 2", + session_state, + ) + self.assertIn( + "JIN messages count: 2", + session_state, + ) + self.assertIn( + "Total messages count: 4", + session_state, + ) + self.assertNotIn( + "699", + session_state, + ) + def test_brain_prompt_anchors_short_feedback_to_last_jin_response(self): context = SimpleNamespace( @@ -1173,7 +1268,7 @@ def test_brain_prompt_anchors_short_feedback_to_last_jin_response(self): ) self.assertIn( - "", 1, )[0] - runtime_memory = prompt.split( - "", + 1, + )[0] + ) + runtime_memory = frame_memory_suffix.split( ">", 1, )[1].split( - "", + f"", 1, )[0] @@ -1256,7 +1362,7 @@ def test_brain_prompt_keeps_user_feedback_out_of_runtime_state(self): ) self.assertLess( prompt.index(""), - prompt.index(""), - prompt.index("", + 1, + )[0] + ) + runtime_memory_block = frame_memory_suffix.split( ">", 1, )[1].split( - "", + f"", 1, )[0] assert_not_contains_text( @@ -3768,10 +3779,17 @@ def test_prompt_uses_passed_agent_runtime_actions(self): self.assertNotIn("", prompt) + current_model_uid = ( + config.SERVICE_MODEL_UID + if config.USE_SERVICE_AS_BRAIN + else config.BRAIN_MODEL_UID + ) self.assertIn( - f"{config.SERVICE_MODEL_UID}", + f"{current_model_uid}", prompt, ) + self.assertNotIn("", prompt) + self.assertNotIn("", prompt) self.assertNotIn("", prompt) self.assertNotIn("", prompt) self.assertNotIn("", prompt) diff --git a/tests/test_chat_reasoning_spacing_client_contract.py b/tests/test_chat_reasoning_spacing_client_contract.py index 2191f713..fbd5dc6f 100644 --- a/tests/test_chat_reasoning_spacing_client_contract.py +++ b/tests/test_chat_reasoning_spacing_client_contract.py @@ -108,9 +108,14 @@ def test_collapsed_reasoning_is_upright_roomier_and_softly_faded(self): self.assertIn("font-style: normal;", css) self.assertIn("max-height: calc(1lh + 32px);", css) self.assertIn( - "body:not(.theme-win95) .jin-think-content.is-collapsed::after", + "body:not(.theme-win95) .jin-think-wrapper:has(> .jin-think-content.is-collapsed)::after", css, ) + self.assertNotIn( + ".jin-think-content.is-collapsed::after", + css, + ) + self.assertIn(".jin-think-wrapper {\n position: relative;", css) self.assertIn("height: 12px;", css) self.assertIn("rgba(12, 12, 14, 0.22)", css) @@ -119,6 +124,7 @@ def test_cache_versions_are_bumped_for_reasoning_spacing_assets(self): self.assertIn("/static/css/base.css?v=stream-avatar-1", source) self.assertIn("/static/css/chat.css?v=stream-avatar-1", source) + self.assertIn("reasoning-fade-anchor=1", source) self.assertIn("/static/js/chat.js?v=stream-avatar-2", source) self.assertIn( "/static/js/panel-inactivity.js?v=jin-size-1", diff --git a/tests/test_current_context_window.py b/tests/test_current_context_window.py index 9d125db1..f2588677 100644 --- a/tests/test_current_context_window.py +++ b/tests/test_current_context_window.py @@ -38,7 +38,7 @@ class CurrentContextWindowTests( unittest.IsolatedAsyncioTestCase ): - async def test_prepare_inserts_detected_context_window_under_brain_model(self): + async def test_prepare_inserts_detected_context_window_under_current_model(self): context = SimpleNamespace( runtime_action_events=[], @@ -74,7 +74,7 @@ async def test_prepare_inserts_detected_context_window_under_brain_model(self): ) self.assertLess( prepared.system_prompt.index( - "" + "" ), prepared.system_prompt.index( "" diff --git a/tests/test_l1_memory.py b/tests/test_l1_memory.py index 828b98dc..4f6c51be 100644 --- a/tests/test_l1_memory.py +++ b/tests/test_l1_memory.py @@ -233,6 +233,7 @@ def test_runtime_memory_snapshot_persists_session_counters(self): context.runtime_turn_counter = 19 context.user_message_count = 15 context.assistant_message_count = 14 + context.runtime_memory_updates = 28 snapshot = build_runtime_memory_snapshot( context, @@ -255,6 +256,10 @@ def test_runtime_memory_snapshot_persists_session_counters(self): snapshot["assistant_message_count"], 14, ) + self.assertEqual( + snapshot["runtime_memory_updates"], + 28, + ) self.assertEqual( snapshot["raw_memory"], "topic: reconnect counters", diff --git a/tests/test_l1_reconnect_resume.py b/tests/test_l1_reconnect_resume.py index 33120ea0..e7d497df 100644 --- a/tests/test_l1_reconnect_resume.py +++ b/tests/test_l1_reconnect_resume.py @@ -293,5 +293,67 @@ async def test_newer_browser_snapshot_discards_stale_pending_checkpoint(self): ) + async def test_matching_browser_memory_discards_stale_pending_checkpoint(self): + with tempfile.TemporaryDirectory() as directory: + pending_dir = Path(directory) + + with patch.object( + l1_pending, + "PENDING_L1_DIR", + pending_dir, + ): + source_context = self.build_context( + service_client=FakeServiceClient("Unused."), + runtime_memory_updates=27, + ) + source_context.runtime_memory_pending_turns = [ + { + "user_message": "Which film is this?", + "assistant_message": "One Point O.", + }, + ] + source_context.runtime_memory_pending_base_updates = 27 + + self.assertTrue( + l1_pending.persist_pending_l1_update( + source_context + ) + ) + + restarted_service = FakeServiceClient("Must not run.") + restarted_context = self.build_context( + service_client=restarted_service, + runtime_memory_updates=0, + ) + restarted_context.runtime_memory = ( + 'user_message: "Which film is this?"\n' + "last_jin_response: One Point O." + ) + restarted_context.runtime_memory_stable = ( + restarted_context.runtime_memory + ) + + self.assertTrue( + l1_pending.restore_pending_l1_update( + restarted_context + ) + ) + + resumed_task = l1_memory.resume_runtime_memory_pending_update( + restarted_context + ) + + self.assertIsNone(resumed_task) + self.assertEqual(restarted_service.calls, []) + self.assertEqual( + restarted_context.runtime_memory_pending_turns, + [], + ) + self.assertEqual( + list(pending_dir.glob("*.l1_pending.json")), + [], + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_l4_hover_card_client_contract.py b/tests/test_l4_hover_card_client_contract.py index f71fc91f..0e7c241f 100644 --- a/tests/test_l4_hover_card_client_contract.py +++ b/tests/test_l4_hover_card_client_contract.py @@ -62,10 +62,32 @@ def test_hover_card_sits_left_of_panel_and_does_not_capture_pointer(self): self.assertIn(".runtime-memory-l4-hover-card::before", css) self.assertNotIn(".runtime-memory-l4-hover-icon", css) + def test_scroll_retargets_hover_card_under_stationary_pointer(self): + source = MEMORY_VIEW.read_text(encoding="utf-8") + + self.assertIn( + "const longTermMemoryHoverRows = new WeakMap();", + source, + ) + self.assertIn( + '".runtime-memory-line:hover"', + source, + ) + self.assertIn( + "scheduleLongTermMemoryHoverCardScrollSync();", + source, + ) + self.assertNotIn( + 'memoryScroll.addEventListener("scroll", () => {\n' + ' hideLongTermMemoryHoverCard();', + source, + ) + def test_hover_card_assets_are_cache_busted(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertEqual(source.count("l4-hover-card=2"), 2) + self.assertEqual(source.count("l4-hover-card=2"), 1) + self.assertEqual(source.count("l4-hover-card=3"), 1) if __name__ == "__main__": diff --git a/tests/test_live_session_checkpoint.py b/tests/test_live_session_checkpoint.py index 6a491611..67bc8b66 100644 --- a/tests/test_live_session_checkpoint.py +++ b/tests/test_live_session_checkpoint.py @@ -68,6 +68,8 @@ def test_runtime_checkpoint_contains_live_session_state_without_l3(self): turn_number=31, user_message_count=9, assistant_message_count=8, + current_session_user_message_count=2, + current_session_assistant_message_count=1, runtime_memory_updates=6, runtime_loaded_delayed_memory_ids=["dm-1", "dm-2"], runtime_attached_file_ids=["file-1"], @@ -83,6 +85,14 @@ def test_runtime_checkpoint_contains_live_session_state_without_l3(self): checkpoint = build_runtime_session_checkpoint(context) self.assertEqual(checkpoint["session_id"], "session-current") + self.assertEqual( + checkpoint["current_session_user_message_count"], + 2, + ) + self.assertEqual( + checkpoint["current_session_assistant_message_count"], + 1, + ) self.assertEqual( checkpoint["recent_turns"], context.runtime_recent_turns[-3:], @@ -128,6 +138,17 @@ def test_every_runtime_l1_persists_the_live_session_checkpoint(self): self.assertIn("Number(data.updates || 0) <= 0", block) + def test_live_checkpoint_keeps_current_session_counters(self): + source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") + start = source.index("function normalizeLiveSessionSnapshot(") + end = source.index("function persistLiveSessionCheckpoint(data)", start) + block = source[start:end] + + self.assertIn("current_session_user_message_count", block) + self.assertIn("current_session_assistant_message_count", block) + self.assertIn("runtimeSnapshot.current_session_user_message_count", block) + self.assertIn("runtimeSnapshot.current_session_assistant_message_count", block) + def test_bootstrap_reads_the_atomic_v2_checkpoint(self): source = RUNTIME_SESSION_JS.read_text(encoding="utf-8") start = source.index("function getPersistedSessionBootstrap()") diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index 9e1ea863..dac90116 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -33,6 +33,18 @@ def test_reasoning_citations_cover_runtime_active_and_l4_layers(self): self.assertIn('match.sourceType === "active"', worker) self.assertIn('match.sourceType === "l4"', worker) + def test_note_key_is_not_an_exact_citation_alias_in_any_memory_layer(self): + source = THINK_CITATIONS_JS.read_text(encoding="utf-8") + + self.assertIn( + '&& key.toLocaleLowerCase() !== "note"', + source, + ) + self.assertNotIn( + 'includeKeyAlias: key.toLocaleLowerCase() !== "note",', + source, + ) + def test_active_and_l4_reasoning_citation_colors_are_layer_specific(self): css = CHAT_CSS.read_text(encoding="utf-8") @@ -139,10 +151,11 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/runtime-memory.css?v=", source) self.assertIn("/static/css/theme-win95.css?v=", source) self.assertIn("/static/js/runtime/runtime-memory-view.js?v=context-card-chevronless-1", source) - self.assertIn("lazy-rows=5", source) + self.assertIn("lazy-rows=6", source) self.assertIn("l4-priority-bubble=1", source) self.assertIn("/static/js/runtime/runtime-avatar.js?v=", source) self.assertIn("/static/js/think-citations.js?v=think-citations-8", source) + self.assertIn("note-key-skip=1", source) self.assertIn("/static/js/chat.js?v=stream-avatar-2-reference-ids-1", source) diff --git a/tests/test_reconnect_persistence_client_contract.py b/tests/test_reconnect_persistence_client_contract.py index 36f4faf7..ab69b8a3 100644 --- a/tests/test_reconnect_persistence_client_contract.py +++ b/tests/test_reconnect_persistence_client_contract.py @@ -13,6 +13,7 @@ ) INDEX_HTML = ROOT / "ui" / "templates" / "index.html" WEBSOCKET_INIT = ROOT / "websocket" / "__init__.py" +L1_MEMORY_UTILS = ROOT / "runtime" / "L1_memory_utils.py" class ReconnectPersistenceClientContractTests(unittest.TestCase): @@ -63,6 +64,33 @@ def test_backend_restart_is_not_treated_as_in_memory_soft_resume(self): source.replace("\r\n", "\n"), ) + def test_soft_reconnect_snapshot_keeps_l1_revision(self): + + source = L1_MEMORY_UTILS.read_text( + encoding="utf-8" + ) + start = source.index( + "def build_runtime_memory_snapshot(" + ) + end = source.index( + "def build_runtime_session_checkpoint(", + start, + ) + block = source[start:end] + + self.assertIn( + '"runtime_memory_updates": getattr(', + block, + ) + + session_source = RUNTIME_SESSION_JS.read_text( + encoding="utf-8" + ) + self.assertIn( + "snapshot.runtime_memory_updates", + session_source, + ) + def test_runtime_session_cache_version_is_bumped(self): source = INDEX_HTML.read_text( diff --git a/tests/test_runtime_memory_lazy_rows_client_contract.py b/tests/test_runtime_memory_lazy_rows_client_contract.py index 1c741737..46eb95ff 100644 --- a/tests/test_runtime_memory_lazy_rows_client_contract.py +++ b/tests/test_runtime_memory_lazy_rows_client_contract.py @@ -88,10 +88,24 @@ def test_native_scroll_path_is_not_debounced_after_downward_progress(self): scroll_handler, ) + def test_l4_rerender_preserves_materialized_depth_and_scroll_position(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + start = source.index("function renderLongTermMemoryFacts()") + end = source.index("function renderActiveMemoryRecords()", start) + block = source[start:end] + + self.assertIn("const preservedRenderedCount =", block) + self.assertIn("runtimeMemoryLazyRenderedCount;", block) + self.assertIn("const preservedScrollTop =", block) + self.assertIn("initialBatchSize: renderBatchSize", block) + self.assertIn("memoryScroll.scrollTop = Math.min(", block) + self.assertIn("runtimeMemoryLastScrollTop =", block) + + def test_memory_view_cache_key_is_bumped_for_l4_lazy_optimization(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("lazy-rows=5", source) + self.assertIn("lazy-rows=6", source) self.assertIn("l4-priority-bubble=1", source) diff --git a/tests/test_websocket_pending_usage.py b/tests/test_websocket_pending_usage.py index 94dd51a1..9ad29ae0 100644 --- a/tests/test_websocket_pending_usage.py +++ b/tests/test_websocket_pending_usage.py @@ -531,6 +531,8 @@ async def test_session_bootstrap_normalizes_restored_snapshot_index(self): session_memory_source="", runtime_l3_session_memory="", runtime_session_memory_updates=0, + current_session_user_message_count=0, + current_session_assistant_message_count=0, ) restored = apply_session_bootstrap( @@ -544,6 +546,8 @@ async def test_session_bootstrap_normalizes_restored_snapshot_index(self): "turn_number": 14, "user_message_count": 15, "assistant_message_count": 14, + "current_session_user_message_count": 15, + "current_session_assistant_message_count": 14, "raw_memory": "topic: restored runtime state", }, }, @@ -576,6 +580,26 @@ async def test_session_bootstrap_normalizes_restored_snapshot_index(self): context.assistant_message_count, 14, ) + self.assertEqual( + context.current_session_user_message_count, + 0, + ) + self.assertEqual( + context.current_session_assistant_message_count, + 0, + ) + self.assertEqual( + context.runtime_memory_snapshots[0][ + "current_session_user_message_count" + ], + 0, + ) + self.assertEqual( + context.runtime_memory_snapshots[0][ + "current_session_assistant_message_count" + ], + 0, + ) self.assertEqual( context.runtime_memory_snapshots[0]["turn_number"], 14, @@ -597,6 +621,8 @@ async def test_runtime_resume_restores_persisted_session_and_turn_counter(self): turn_number=3, user_message_count=3, assistant_message_count=3, + current_session_user_message_count=2, + current_session_assistant_message_count=2, session_memory="", runtime_l3_session_memory="", runtime_session_memory_updates=0, @@ -622,6 +648,8 @@ async def test_runtime_resume_restores_persisted_session_and_turn_counter(self): "runtime_turn_counter": 17, "user_message_count": 11, "assistant_message_count": 10, + "current_session_user_message_count": 7, + "current_session_assistant_message_count": 6, }, "session_memory": "decision: keep reconnect persistence", "session_memory_source": "browser_soft_reconnect", @@ -643,6 +671,26 @@ async def test_runtime_resume_restores_persisted_session_and_turn_counter(self): context.turn_number, 11, ) + self.assertEqual( + context.current_session_user_message_count, + 7, + ) + self.assertEqual( + context.current_session_assistant_message_count, + 6, + ) + self.assertEqual( + context.runtime_memory_snapshots[0][ + "current_session_user_message_count" + ], + 7, + ) + self.assertEqual( + context.runtime_memory_snapshots[0][ + "current_session_assistant_message_count" + ], + 6, + ) self.assertEqual( context.runtime_loaded_delayed_memory_ids, [ diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index d76d0602..36cdf6ba 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -22,7 +22,7 @@ --jin-chat-line-soft: rgba(103, 232, 249, 0.14); /* Базовое внешнее свечение обычного JIN-бабла. */ - --jin-chat-glow: rgba(45, 212, 191, 0.12); + --jin-chat-glow: rgba(45, 212, 191, 0.08); /* Основной цвет текста JIN до переопределений ниже. */ --jin-chat-text: rgba(154, 242, 251, 0.95); @@ -31,10 +31,10 @@ --jin-chat-muted: rgba(203, 213, 225, 0.62); /* Базовая масса обычной JIN-плашки: последнее число = плотность фона. */ - --jin-chat-panel: rgba(8, 17, 22, 0.76); + --jin-chat-panel: rgba(8, 17, 22, 0.82); /* Базовая масса USER-плашки: главный регулятор прозрачности user-бабла. */ - --jin-chat-panel-user: rgba(14, 21, 31, 0.76); + --jin-chat-panel-user: rgba(14, 21, 31, 0.80); /* Базовая масса think-плашки: чем меньше alpha, тем больше она "в воздухе". */ --jin-chat-panel-think: rgba(12, 12, 14, 0.44); @@ -322,48 +322,35 @@ } .jin-chat-bubble-user { - /* Цвет текста USER. Alpha делает текст тише/ярче. */ - color: rgba(226, 235, 246, 0.76); + /* Цвет текста USER. Чуть спокойнее и суше, чтобы плашка не светилась лишнего. */ + color: rgba(223, 231, 240, 0.72); /* Радиус USER-бабла. Сейчас как у аватарки. */ border-radius: 4px; /* Серо-голубая рамка USER-бабла, в тон US-аватарке. */ - border-color: rgba(148, 163, 184, 0.26); - - /* USER-фон: - 1) верхний blue-блик, - 2) правый мягкий blue-свет, - 3) нижняя slate-глубина, - 4) стеклянная вертикальная плёнка, - 5) основная масса через --jin-chat-panel-user. */ + border-color: rgba(148, 163, 184, 0.22); + + /* USER-фон оставляем объёмным, но заметно тише: без агрессивных холодных пятен. */ background: - radial-gradient(circle at 18% 0%, rgba(147, 197, 253, 0.052), transparent 44%), - radial-gradient(circle at 88% 20%, rgba(96, 165, 250, 0.034), transparent 42%), - radial-gradient(circle at 64% 100%, rgba(148, 163, 184, 0.026), transparent 48%), - linear-gradient(180deg, rgba(219, 234, 254, 0.034), rgba(15, 23, 42, 0.022)), + linear-gradient(180deg, rgba(219, 234, 254, 0.026), rgba(15, 23, 42, 0.018)), + linear-gradient(90deg, rgba(148, 163, 184, 0.020), rgba(148, 163, 184, 0.008)), var(--jin-chat-panel-user); /* Размытие USER-бабла. Уменьшай, если плашка слишком мутная. */ /*backdrop-filter: blur(9.75px);*/ - /* USER-тени: - 1) физическая тень, - 2) синее внешнее свечение, - 3) широкая бледная blue-аура, - 4-6) внутренние блики и объём. */ + /* USER-тени приглушены, чтобы фон читался ровнее и без лишнего сияния. */ box-shadow: - 0 0 0 1px rgba(148, 163, 184, 0.050), - 0 12px 32px rgba(0, 0, 0, 0.23), - 0 0 28px rgba(96, 165, 250, 0.062), - 0 0 42px rgba(147, 197, 253, 0.030), - inset 0 1px 0 rgba(219, 234, 254, 0.050), - inset 0 0 24px rgba(96, 165, 250, 0.018), - inset 0 0 28px rgba(147, 197, 253, 0.012); + 0 0 0 1px rgba(148, 163, 184, 0.040), + 0 12px 30px rgba(0, 0, 0, 0.21), + 0 0 18px rgba(96, 165, 250, 0.036), + inset 0 1px 0 rgba(219, 234, 254, 0.040), + inset 0 0 18px rgba(96, 165, 250, 0.010); /* Очень слабое свечение текста USER. */ text-shadow: - 0 0 10px rgba(96, 165, 250, 0.030); + 0 0 8px rgba(96, 165, 250, 0.016); } .jin-chat-bubble-brain { @@ -570,6 +557,8 @@ } .jin-think-wrapper { + position: relative; + /* Ширина think-блока совпадает с шириной длинных баблов. */ max-width: 720px; @@ -651,12 +640,14 @@ inset 0 -18px 18px rgba(12, 12, 14, 0.42); } -body:not(.theme-win95) .jin-think-content.is-collapsed::after { +/* Keep the fade outside the scrolled content: collapsed reasoning scrolls to + its latest line, so a content-owned pseudo-element would move with text. */ +body:not(.theme-win95) .jin-think-wrapper:has(> .jin-think-content.is-collapsed)::after { content: ""; position: absolute; - right: 0; - bottom: 0; - left: 0; + right: 1px; + bottom: 1px; + left: 1px; height: 12px; border-radius: 0 0 9px 9px; background: linear-gradient( @@ -779,65 +770,47 @@ body:not(.theme-win95) .jin-think-content.is-collapsed::after { } .jin-chat-bubble-service { - /* Финальный JIN-фон: - 1) верхний cyan-блик, - 2) правый teal-свет, - 3) нижняя слабая indigo-глубина, - 4) стеклянная вертикальная плёнка, - 5) основная масса JIN-бабла. */ + /* Финальный JIN-фон делаем ровнее: убираем круглые радиальные пятна, + из-за которых на широком/растущем бабле появлялась светлая половина. */ background: - radial-gradient(circle at 18% 0%, rgba(103, 232, 249, 0.050), transparent 44%), - radial-gradient(circle at 86% 18%, rgba(45, 212, 191, 0.040), transparent 42%), - radial-gradient(circle at 64% 100%, rgba(129, 140, 248, 0.024), transparent 48%), - linear-gradient(180deg, rgba(204, 251, 241, 0.030), rgba(10, 18, 22, 0.022)), - rgba(10, 18, 22, 0.48); - - /* Финальный цвет текста JIN. */ - color: rgba(222, 238, 235, 0.69); - - /* Финальное JIN-свечение: - 1) физическая тень, - 2) teal-аура, - 3) cyan-аура, - 4) слабая indigo-глубина, - 5-7) внутренний свет и объём. */ + linear-gradient(180deg, rgba(204, 251, 241, 0.026), rgba(10, 18, 22, 0.018)), + linear-gradient(90deg, rgba(45, 212, 191, 0.014), rgba(103, 232, 249, 0.008)), + rgba(10, 18, 22, 0.58); + + /* Финальный цвет текста JIN: чуть спокойнее, но без потери читабельности. */ + color: rgba(220, 235, 232, 0.66); + + /* Финальное JIN-свечение приглушено, чтобы бабл выглядел одним ровным слоем. */ box-shadow: - 0 0 0 1px rgba(103, 232, 249, 0.060), - 0 14px 42px rgba(0, 0, 0, 0.24), - 0 0 32px rgba(45, 212, 191, 0.070), - 0 0 44px rgba(103, 232, 249, 0.035), - 0 0 34px rgba(129, 140, 248, 0.018), - inset 0 1px 0 rgba(204, 251, 241, 0.045), - inset 0 0 26px rgba(45, 212, 191, 0.020), - inset 0 0 30px rgba(103, 232, 249, 0.014); + 0 0 0 1px rgba(103, 232, 249, 0.050), + 0 14px 34px rgba(0, 0, 0, 0.22), + 0 0 18px rgba(45, 212, 191, 0.040), + inset 0 1px 0 rgba(204, 251, 241, 0.034), + inset 0 0 16px rgba(45, 212, 191, 0.010); } .jin-chat-bubble-service .jin-chat-pre { /* Цвет текста внутри JIN-бабла дублируется здесь для надёжности. */ - color: rgba(222, 238, 235, 0.69); + color: rgba(220, 235, 232, 0.66); - /* Свечение текста JIN: teal + cyan. */ + /* Свечение текста JIN почти убрано: нужен спокойный, ровный текст без дымки. */ text-shadow: - 0 0 10px rgba(45, 212, 191, 0.040), - 0 0 14px rgba(103, 232, 249, 0.024); + 0 0 8px rgba(45, 212, 191, 0.018); } .jin-chat-bubble-service { border-color: color-mix( in srgb, - rgba(103, 232, 249, 0.20) calc(var(--jin-bubble-invert-off) * 100%), + rgba(103, 232, 249, 0.16) calc(var(--jin-bubble-invert-off) * 100%), rgba(255, 255, 255, 0.62) calc(var(--JIN_BUBBLE_INVERT) * 100%) ); box-shadow: inset 0 0 0 9999px rgba(246, 249, 247, calc(var(--JIN_BUBBLE_INVERT) * 0.86)), - 0 0 0 1px rgba(103, 232, 249, 0.060), - 0 14px 42px rgba(0, 0, 0, 0.24), - 0 0 32px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.070)), - 0 0 44px rgba(103, 232, 249, calc(var(--jin-bubble-invert-off) * 0.035)), - 0 0 34px rgba(129, 140, 248, calc(var(--jin-bubble-invert-off) * 0.018)), - inset 0 1px 0 rgba(204, 251, 241, 0.045), - inset 0 0 26px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.020)), - inset 0 0 30px rgba(103, 232, 249, calc(var(--jin-bubble-invert-off) * 0.014)); + 0 0 0 1px rgba(103, 232, 249, 0.050), + 0 14px 34px rgba(0, 0, 0, 0.22), + 0 0 18px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.040)), + inset 0 1px 0 rgba(204, 251, 241, 0.034), + inset 0 0 16px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.010)); } .jin-chat-bubble-brain { @@ -858,12 +831,11 @@ body:not(.theme-win95) .jin-think-content.is-collapsed::after { .jin-chat-bubble-service .jin-chat-pre { color: color-mix( in srgb, - rgba(222, 238, 235, 0.69) calc(var(--jin-bubble-invert-off) * 100%), + rgba(220, 235, 232, 0.66) calc(var(--jin-bubble-invert-off) * 100%), rgba(39, 45, 48, 0.92) calc(var(--JIN_BUBBLE_INVERT) * 100%) ); text-shadow: - 0 0 10px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.040)), - 0 0 14px rgba(103, 232, 249, calc(var(--jin-bubble-invert-off) * 0.024)); + 0 0 8px rgba(45, 212, 191, calc(var(--jin-bubble-invert-off) * 0.018)); } .jin-chat-bubble-brain .jin-chat-pre { diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 3a04f9ba..3548a2ea 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -12,9 +12,13 @@ display: flex; min-height: 22px; align-items: center; - max-width: 100%; overflow: hidden; - margin-left: -0.35rem; + + width: auto; + margin-left: -8px; + margin-right: -8px; + + max-width: none; } .runtime-memory-tab { @@ -37,6 +41,7 @@ } .runtime-memory-tab:last-child { + flex: 1 0 auto; border-right: 0; } @@ -83,6 +88,7 @@ .runtime-memory-navigation[data-active-mode="runtime"] .runtime-memory-navigation-controls { padding-left: 0; + transform: translateX(-2px); } .runtime-memory-navigation-controls { diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 3df0c34f..627b6831 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -2447,8 +2447,10 @@ const runtimeMemoryHoverTitleSources = new WeakMap(); const runtimeMemoryHoverTitleBoundNodes = new WeakSet(); + const longTermMemoryHoverRows = new WeakMap(); let longTermMemoryHoverCard = null; let longTermMemoryHoverCardAnchor = null; + let longTermMemoryHoverSyncFrame = null; function appendLongTermMemoryHoverMetadataRow( container, @@ -2634,6 +2636,7 @@ return; } + longTermMemoryHoverRows.set(row, line); row.removeAttribute("title"); row.addEventListener("mouseenter", () => { showLongTermMemoryHoverCard(row, line); @@ -2643,6 +2646,51 @@ }); } + function syncLongTermMemoryHoverCardAfterScroll() { + longTermMemoryHoverSyncFrame = null; + + if (!runtimeMemoryText || !runtimeMemoryText.isConnected) { + hideLongTermMemoryHoverCard(); + return; + } + + const hoveredRow = runtimeMemoryText.querySelector( + ".runtime-memory-line:hover" + ); + const line = hoveredRow + ? longTermMemoryHoverRows.get(hoveredRow) + : null; + + if (!hoveredRow || !line) { + hideLongTermMemoryHoverCard(); + return; + } + + if ( + longTermMemoryHoverCardAnchor === hoveredRow + && longTermMemoryHoverCard + && longTermMemoryHoverCard.isConnected + ) { + positionLongTermMemoryHoverCard( + longTermMemoryHoverCard, + hoveredRow + ); + return; + } + + showLongTermMemoryHoverCard(hoveredRow, line); + } + + function scheduleLongTermMemoryHoverCardScrollSync() { + if (longTermMemoryHoverSyncFrame !== null) { + return; + } + + longTermMemoryHoverSyncFrame = window.requestAnimationFrame( + syncLongTermMemoryHoverCardAfterScroll + ); + } + window.addEventListener("resize", () => { hideLongTermMemoryHoverCard(); syncRuntimeMemoryNavigationGeometry(); @@ -2650,7 +2698,7 @@ if (memoryScroll) { memoryScroll.addEventListener("scroll", () => { - hideLongTermMemoryHoverCard(); + scheduleLongTermMemoryHoverCardScrollSync(); }, { passive: true }); } @@ -8324,6 +8372,19 @@ const initialBatchSize = priorityRecords.length + LONG_TERM_MEMORY_LAZY_BATCH_SIZE; + const preservedRenderedCount = + runtimeMemoryLazyRenderedCount; + const preservedScrollTop = + memoryScroll + ? Math.max(0, memoryScroll.scrollTop) + : 0; + const renderBatchSize = Math.min( + orderedRecords.length, + Math.max( + initialBatchSize, + preservedRenderedCount + ) + ); if (runtimeMemoryText) { runtimeMemoryText.innerHTML = ""; @@ -8344,7 +8405,7 @@ { applyFlash: false, interactiveLongTermMemory: true, - initialBatchSize, + initialBatchSize: renderBatchSize, buildLine: fact => buildLongTermMemoryLine( fact, contextLoadedFactIds, @@ -8352,6 +8413,20 @@ ), } ); + + if (memoryScroll && preservedScrollTop > 0) { + const maxScrollTop = Math.max( + 0, + memoryScroll.scrollHeight - memoryScroll.clientHeight + ); + + memoryScroll.scrollTop = Math.min( + preservedScrollTop, + maxScrollTop + ); + runtimeMemoryLastScrollTop = + Math.max(0, memoryScroll.scrollTop); + } } } diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 681e2a64..042bf25a 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -227,6 +227,40 @@ || runtimeSnapshot.assistant_message_count || 0 ), + current_session_user_message_count: + Number( + Object.prototype.hasOwnProperty.call( + source, + "current_session_user_message_count" + ) + ? source.current_session_user_message_count + : ( + data + && Object.prototype.hasOwnProperty.call( + data, + "current_session_user_message_count" + ) + ? data.current_session_user_message_count + : (runtimeSnapshot.current_session_user_message_count || 0) + ) + ), + current_session_assistant_message_count: + Number( + Object.prototype.hasOwnProperty.call( + source, + "current_session_assistant_message_count" + ) + ? source.current_session_assistant_message_count + : ( + data + && Object.prototype.hasOwnProperty.call( + data, + "current_session_assistant_message_count" + ) + ? data.current_session_assistant_message_count + : (runtimeSnapshot.current_session_assistant_message_count || 0) + ) + ), current_jin_color: String( source.current_jin_color @@ -728,6 +762,11 @@ return { type: "runtime_resume", + frame_memory_index: Math.max( + 0, + Number(history.index || 0) + + Number(history.displayIndexOffset || 0) + ), source_session_id: sourceSessionId || null, previous_session_id: previousSessionId || null, runtime_memory: runtimeText, @@ -1355,6 +1394,7 @@ runtime_memory: runtimeText, runtime_memory_updates: Number(checkpoint.runtime_memory_updates || 0), + frame_memory_index: runtimeText ? 1 : 0, runtime_snapshot: checkpoint.runtime_snapshot || null, runtime_display_snapshot: runtimeDisplaySnapshot, diff --git a/ui/static/js/session-restore.js b/ui/static/js/session-restore.js index 26a20562..fc27b545 100644 --- a/ui/static/js/session-restore.js +++ b/ui/static/js/session-restore.js @@ -664,6 +664,10 @@ payload.runtime_memory_updates || 0 ), + frame_memory_index: + String(payload.runtime_memory || "").trim() + ? 1 + : 0, runtime_snapshot: payload.runtime_snapshot && typeof payload.runtime_snapshot === "object" diff --git a/ui/static/js/think-citations.js b/ui/static/js/think-citations.js index 28a34c15..1390462c 100644 --- a/ui/static/js/think-citations.js +++ b/ui/static/js/think-citations.js @@ -1113,7 +1113,10 @@ }; add(options.id, base, true); - if (options.includeKeyAlias !== false) { + if ( + options.includeKeyAlias !== false + && key.toLocaleLowerCase() !== "note" + ) { add(key, base, true); } if (activeMemoryId) { diff --git a/ui/templates/index.html b/ui/templates/index.html index c99e969b..1f71870d 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -11,7 +11,7 @@ - + @@ -527,7 +527,7 @@ - + @@ -539,7 +539,7 @@ - + diff --git a/utils/context/runtime_state.py b/utils/context/runtime_state.py index 0fed6a3c..cbcea143 100644 --- a/utils/context/runtime_state.py +++ b/utils/context/runtime_state.py @@ -266,11 +266,10 @@ def build_runtime_xml( ) or "" ).strip(), - service_model_uid=settings.SERVICE_MODEL_UID, - brain_model_uid=( - settings.BRAIN_MODEL_UID - if not settings.USE_SERVICE_AS_BRAIN - else "" + current_model_uid=( + settings.SERVICE_MODEL_UID + if settings.USE_SERVICE_AS_BRAIN + else settings.BRAIN_MODEL_UID ), current_context_window=getattr( context, @@ -317,29 +316,62 @@ def build_runtime_xml( ) -def get_visible_assistant_message_count( +def get_current_session_user_message_count( context=None, ) -> int: if context is None: return 0 - assistant_message_count = int( + return int( getattr( context, - "assistant_message_count", - 0, + "current_session_user_message_count", + getattr( + context, + "user_message_count", + 0, + ), ) or 0 ) - user_message_count = int( + + +def get_current_session_assistant_message_count( + context=None, +) -> int: + + if context is None: + return 0 + + return int( getattr( context, - "user_message_count", - 0, + "current_session_assistant_message_count", + getattr( + context, + "assistant_message_count", + 0, + ), ) or 0 ) + + +def get_visible_assistant_message_count( + context=None, +) -> int: + + assistant_message_count = ( + get_current_session_assistant_message_count( + context + ) + ) + user_message_count = ( + get_current_session_user_message_count( + context + ) + ) pending_response_count = ( 1 if user_message_count > assistant_message_count @@ -356,9 +388,6 @@ def get_visible_turn_count( context=None, ) -> int: - if context is None: - return 0 - turn_number = int( getattr( context, diff --git a/utils/current_context_window.py b/utils/current_context_window.py index 3f8d233d..08a041c3 100644 --- a/utils/current_context_window.py +++ b/utils/current_context_window.py @@ -181,6 +181,7 @@ def ensure_current_context_window_field( return prompt for tag in ( + "CURRENT_MODEL_UID", "BRAIN_MODEL_UID", "SERVICE_MODEL_UID", "CURRENT_SESSION_ID", diff --git a/utils/session_restore.py b/utils/session_restore.py index 6cfdca5a..9d2ae3ed 100644 --- a/utils/session_restore.py +++ b/utils/session_restore.py @@ -1125,6 +1125,7 @@ def _parse_trusted_values(context_text: str) -> dict: for name in ( "RUNTIME_MODE", + "CURRENT_MODEL_UID", "SERVICE_MODEL_UID", "BRAIN_MODEL_UID", "CURRENT_CONTEXT_WINDOW", diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 8efbc355..e80cf345 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -1979,6 +1979,50 @@ def hydrate_runtime_counters_from_bootstrap_metadata( ) +def hydrate_current_session_counters_from_resume_metadata( + context, + message_data: dict, +): + + if not isinstance( + message_data, + dict, + ): + return + + runtime_snapshot = message_data.get( + "runtime_snapshot", + {}, + ) + snapshot_data = ( + runtime_snapshot + if isinstance( + runtime_snapshot, + dict, + ) + else {} + ) + + for field_name in ( + "current_session_user_message_count", + "current_session_assistant_message_count", + ): + floor = parse_bootstrap_counter( + message_data.get( + field_name, + snapshot_data.get( + field_name, + 0, + ), + ) + ) + _raise_runtime_counter_floor( + context, + field_name, + floor, + ) + + def hydrate_runtime_counters_from_active_memory( context, runtime_memory: str, @@ -2149,6 +2193,14 @@ def apply_runtime_resume( runtime_memory, ) + # runtime_resume reconnects the same live session. A normal + # session_bootstrap deliberately does not hydrate these counters, so each + # fresh runtime session starts CURRENT_SESSION_STATE from zero. + hydrate_current_session_counters_from_resume_metadata( + context, + message_data, + ) + current_updates = parse_bootstrap_counter( getattr( context, @@ -2216,6 +2268,35 @@ def apply_runtime_resume( restored_snapshot_session_id ) + restored_snapshot[ + "current_session_user_message_count" + ] = int( + getattr( + context, + "current_session_user_message_count", + 0, + ) + or 0 + ) + restored_snapshot[ + "current_session_assistant_message_count" + ] = int( + getattr( + context, + "current_session_assistant_message_count", + 0, + ) + or 0 + ) + + context.runtime_memory_display_index_offset = parse_bootstrap_counter( + message_data.get( + "frame_memory_index", + runtime_snapshot.get("index", 0) + if isinstance(runtime_snapshot, dict) + else 0, + ) + ) context.runtime_memory_snapshots = [ restored_snapshot ] @@ -3050,6 +3131,35 @@ def apply_session_bootstrap( restored_snapshot_session_id ) + # The FRAME may come from the predecessor, but visible message counts + # belong to the new live session and must not inherit that history. + restored_snapshot[ + "current_session_user_message_count" + ] = int( + getattr( + context, + "current_session_user_message_count", + 0, + ) + or 0 + ) + restored_snapshot[ + "current_session_assistant_message_count" + ] = int( + getattr( + context, + "current_session_assistant_message_count", + 0, + ) + or 0 + ) + + context.runtime_memory_display_index_offset = parse_bootstrap_counter( + message_data.get( + "frame_memory_index", + 1, + ) + ) context.runtime_memory_snapshots.append( restored_snapshot ) diff --git a/websocket/messages.py b/websocket/messages.py index f60a6fc8..f12858d1 100644 --- a/websocket/messages.py +++ b/websocket/messages.py @@ -1190,6 +1190,7 @@ async def process_message( if not is_user_retry: context.user_message_count += 1 + context.current_session_user_message_count += 1 # Tag auto-load is driven only by the text the user typed. # Attachment text still stays in ``user_text`` and reaches JIN as # context, but it must never behave like a tag command. @@ -1349,6 +1350,8 @@ async def process_message( ) if not is_action_guard_retry and not is_user_retry: context.assistant_message_count += 1 + if not is_session_restore_resume: + context.current_session_assistant_message_count += 1 context.turn_number += 1 completed_session_snapshot = ( From 03bd2002fea102992c9463c5936f8cb5e0dec4fb Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:56:15 +0300 Subject: [PATCH 113/168] chore: brain mode - move to brain-first single model flow by default - update documentation --- AGENTS.md | 5 +- ARCHITECTURE.md | 280 --------- README.md | 59 +- app.py | 147 +++-- app_settings.py | 11 +- clients/brain_client.py | 245 +------- clients/registry.py | 53 +- clients/service_client.py | 25 +- config.example.py | 20 +- config_loader.py | 137 ++++- docs/JIN_ARCHITECTURE.md | 57 +- docs/JIN_CURRENT_STATE.md | 168 +++--- docs/JIN_DECISIONS.md | 27 +- launch_jin.ps1 | 73 ++- rules/__init__.py | 4 - rules/brain_context_builder.py | 15 - runtime/L1_memory.py | 12 +- runtime/L4_memory.py | 8 +- runtime/deep_web_search.py | 1 + runtime/fact_check.py | 2 + runtime/memory_common.py | 12 +- runtime/state.py | 39 +- tests/test_dom_lifecycle_client_contract.py | 67 +++ ui/static/css/chat.css | 20 +- ui/static/css/theme-win95.css | 58 +- ui/static/js/chat.js | 4 +- ui/static/js/logger/trace-modal.js | 27 + ui/static/js/runtime/runtime-memory-view.js | 58 ++ ui/static/js/runtime/runtime-panel.js | 603 ++++---------------- ui/static/js/socket.js | 5 - ui/static/js/socket/input.js | 5 - ui/static/js/status.js | 67 ++- ui/templates/index.html | 77 +-- utils/brain_client_utils.py | 29 +- utils/context/runtime_state.py | 4 +- utils/python_skill_asset_utils.py | 5 +- websocket/logger.py | 9 - websocket/messages.py | 21 +- 38 files changed, 920 insertions(+), 1539 deletions(-) delete mode 100644 ARCHITECTURE.md create mode 100644 tests/test_dom_lifecycle_client_contract.py diff --git a/AGENTS.md b/AGENTS.md index 1baa5f98..bf1697e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ This file is the mandatory entry point for any coding agent working in this repo 4. Read `docs/JIN_CURRENT_STATE.md` for transitional formats, stale legacy, known conflicts, and test status. 5. Inspect the current implementation end-to-end before editing it. -Do not treat `README.md`, the old root `ARCHITECTURE.md`, old tests, comments, filenames, or search indexes as stronger evidence than the current source tree plus these documents. If sources conflict, report the conflict instead of silently choosing one. +Do not treat README prose, historical docs/tests/comments, filenames, or search indexes as stronger evidence than the current source tree plus these documents. If sources conflict, report the conflict instead of silently choosing one. ## Source precedence @@ -27,6 +27,9 @@ A current implementation can still violate a product decision. Do not hide that. - JIN Core Engine is a model-agnostic cognitive runtime, not a chatbot skin and not a framework tied to one LLM. - The normal model path is direct: user turn -> `AgentRuntime` -> `BrainNode`. Do not add a pre-Brain routing framework without an explicit task. +- Brain is the only foreground response route. `BrainNode` must resolve the canonical `brain` runtime/client; Service must never become a foreground response mode through `SERVICE_CONFIGURED`, model availability, or a legacy flag. +- Service is a logical background role. If no dedicated `SERVICE_API_BASE` is configured, `clients/registry.py` intentionally aliases the Service client to the Brain client; a dedicated Service endpoint changes only background execution. +- `USE_SERVICE_AS_BRAIN` is legacy config input only. `config_loader.py` may migrate it once and then removes the attribute from normalized config; launcher detection exists only to preserve old local configs during startup. Archived `SERVICE` roles/`RUNTIME_MODE=SERVICE` and the logger's old Service-output presentation are reader compatibility, not live routing. - `RuntimeContext` is the in-process live state hub for a runtime session. Do not create parallel sources of truth for state it already owns. - L2 and L3 are **removed architectural layers**. Do not restore them from old README/tests/indexes. Any surviving L2/L3 names must be classified as compatibility, stale tests/docs, UI residue, or dead legacy before touching them. - Durable facts belong in L4. Do not reintroduce a durable-L1/L2/L3 memory hierarchy. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 0187e363..00000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,280 +0,0 @@ -# JIN Core Engine Architecture - -This document maps the runtime boundaries, state ownership, data flow, persistence, and extension points in JIN Core Engine. - -`README.md` covers product behavior, setup, configuration, and the public feature surface. `LIVE_AVATAR.md` covers avatar rendering and visual-state rules. - ---- - -## 1. Runtime Structure - -JIN is built around one live runtime state shared by the WebSocket session, model roles, memory engines, runtime actions, persistence, and browser projections. - -The main execution path is: - -```text -Browser UI ↔ WebSocket ↔ RuntimeContext → AgentRuntime → Brain - ↕ ↕ - Service Action Engine - ↕ ↕ - Memory Files / Skills / Search -``` - -`RuntimeContext` in `runtime/runtime_context.py` is the in-process state hub for an active runtime. It carries model clients, active streams, current memory, delayed reports, active memory, L4 state, attachments, action history, tool results, telemetry, session counters, recovery state, and background-task references. - -The surrounding modules have narrow ownership: - -| Area | Main responsibility | -|---|---| -| `websocket/` | connection lifecycle, bootstrap, queueing, user-turn orchestration | -| `agent/` | direct Brain execution and turn state | -| `runtime/` | live state, streams, memory engines, telemetry, recovery | -| `contracts/` | runtime-action definitions and Brain-facing action rules | -| `utils/actions/` | action payload handling and state mutations | -| `utils/context/` | Brain context serialization | -| `clients/` | OpenAI-compatible model and search clients | -| `ui/` | browser projections of runtime state | - ---- - -## 2. Connection and Session Lifecycle - -The browser uses `/ws/chat` as the live runtime channel. `websocket/bootstrap.py` creates a new `RuntimeContext` or reconnects the socket to resumable state, then hydrates memory, delayed reports, files, counters, and browser-provided session data. - -Each context owns a normal `asyncio.Queue` for pending requests. User messages and internal continuation ticks run serially through that queue, which keeps turn order stable while generation, actions, and memory updates overlap in time. - -A socket and a logical runtime have separate identities. Reconnect can attach a fresh transport to an existing context. Explicit session bootstrap can restore saved L1/L3 state and related runtime metadata. - -Core files: `websocket/__init__.py`, `websocket/bootstrap.py`, `websocket/tasks.py`, `runtime/registry.py`. - ---- - -## 3. User Turn - -`process_message()` in `websocket/messages.py` coordinates a normal turn. - -1. Hydrate attachment ids against the persistent file store. -2. Apply browser runtime signals such as Active Memory, idle time, counters, feedback, and avatar state. -3. Match Delayed Memory tags against text typed by the user. -4. Create the turn and action-sequence ids. -5. Build `AgentState` and run `AgentRuntime`. -6. Stream reasoning, visible content, runtime-action state, and telemetry to the browser. -7. Record the completed turn and action history. -8. Schedule the Service-model L1/L2 update. - -A later user request waits for a pending memory update before entering Brain execution. This keeps the next prompt aligned with the latest accepted turn. - -Interrupted turns use the interrupted-memory path in `runtime/L1_memory.py`, so partial work can still update live state with separate lifecycle metadata. - ---- - -## 4. Agent and Model Boundary - -`AgentRuntime` passes each user turn directly to `BrainNode`. Brain is the visible reasoning and response role. Service handles background memory work and supporting inference. - -Roles are resolved separately from physical model endpoints. `USE_SERVICE_AS_BRAIN` can assign the Service endpoint to the Brain role while the rest of the runtime continues to address it as Brain. - -Model access goes through the OpenAI-compatible clients in `clients/` and `runtime/client.py`. Provider metadata can supply the loaded context window for live budgeting and telemetry. Stream-level output protection remains inside `RuntimeStream`, where it can interrupt repetition or malformed generation during generation rather than after the Brain has finished. - -Core files: `agent/runtime.py`, `agent/nodes/brain.py`, `clients/registry.py`, `utils/brain_client_utils.py`. - ---- - -## 5. Brain Context - -`rules/brain_context_builder.py` assembles the Brain system context from current runtime state. - -The builder combines the state relevant to the current tick: L1, L2, L3, L4, Active Memory, loaded Delayed Memory, attachments, skills, tool results, TODO state, feedback, session counters, recent visible turns, selected previous reasoning, and enabled runtime-action rules. - -The assembled prompt is a transient projection. Canonical state remains in `RuntimeContext`, browser stores, and filesystem stores. - -Action follow-ups use a dedicated context path in `agent/nodes/brain.py`. The follow-up carries the original sequence request, trusted action results, current runtime state, preserved attachments, and selected reasoning edges. The sequence stays under the original turn id and action-sequence id. - -`BRAIN_MAX_FOLLOWUPS` bounds continuation ticks. At the ceiling, the runtime switches to a final response tick with action execution disabled. - ---- - -## 6. Stream and Runtime Actions - -`RuntimeStream` in `runtime/stream.py` separates three output channels from Brain generation: reasoning, visible answer text, and private runtime-action markers. - -Action markers are matched against contracts from `contracts/*.json`. `contracts/rules_assembler.py` exposes enabled actions to the Brain, and `utils/actions/dispatcher.py` routes accepted calls to action-specific handlers. - -The action path is: - -```text -Brain marker → contract → payload normalization → guard → handler - → runtime mutation / external result → action event - → trusted tool result → optional Brain follow-up -``` - -Behavior guards in `runtime/action_guard.py` can require user intent or confirmation before a state-changing action proceeds. Confirmation state belongs to the runtime and survives the continuation flow. - -Actions share sequence history through `runtime_action_events`, `runtime_session_action_history`, and tool-result state on `RuntimeContext`. UI action bubbles and logger entries consume the same event stream. - ---- - -## 7. Memory State - -The memory systems use separate stores and update schedules because they feed different parts of the runtime. - -| Layer | Runtime role | Update path | -|---|---|---| -| **L1** | current task and interaction state | Service update after a turn | -| **L2** | repeated interaction patterns | derived from L1 diff history | -| **L3** | compact session handoff | `SAVE_SESSION` flow | -| **Facts Memory** | durable candidates collected from runtime state | L1/facts synchronization | -| **L4** | persistent long-term facts | idle extraction, merge, explicit edits | -| **Delayed Memory** | larger contextual reports loaded on demand | actions, UI, tag triggers | -| **Active Memory** | unresolved commitments and conditions | create/resolve actions and browser state | - -### L1 and L2 - -`runtime/L1_memory.py` maintains the current compact runtime memory, versioned snapshots, diffs, pending-turn batches, and response-feedback integration. `runtime/L2_memory.py` consumes L1 change history to update slower pattern memory. - -### L3 - -`runtime/L3_memory.py` creates the saved session handoff. `SAVE_SESSION` completes its memory work before the related Brain follow-up continues, so that continuation sees the saved state produced by the same sequence. - -### Facts Memory and L4 - -`runtime/L4_memory.py` runs L4 as a staged pipeline. Facts Memory supplies candidates, the idle extraction phase selects durable material, and the merge phase reconciles candidates with the current long-term store. Explicit L4 edits are tracked so automatic merge work preserves edits from the active turn. - -L4 records keep source lineage and links to Delayed Memory. Merge, delete, and restore operations update those references through the reconciliation helpers in `runtime/L4_memory.py`. - -Delayed reports can move ordinary L4 facts behind report content through `facts_ids`. `anchor_fact_ids` keeps selected facts directly exposed. Prompt assembly, the long-term panel, and avatar state use the same archive/anchor classification. - -### Delayed Memory - -Delayed reports are persistent JSON objects with content, tags, fact links, attachment links, and load metadata. `utils/delayed_memory_file_store.py` owns normalization and file persistence; `websocket/bootstrap.py` hydrates them into runtime state. - -The Brain saves a report with a JSON payload inside the `SAVE_DELAYED_MEMORY` marker: - -```text - -{ - "title": "", - "summary": "", - "tags": [], - "body": "", - "anchor_fact_ids": [], - "facts_ids": [], - "attachments_ids": [] -} - -``` - -The marker body must be valid JSON. `tags` and every `*_ids` field are arrays; `title`, `summary`, and `body` are strings. The runtime still accepts the pre-JSON field payload when reading old data, but it is no longer part of the advertised action contract. - -Reports enter context through runtime actions, UI state, pin state, or user-text tag matches. `attachments_ids`, `facts_ids`, and `anchor_fact_ids` connect reports to the file and L4 stores. - -### Active Memory - -Active Memory records hold unresolved runtime commitments. They are carried into the Brain context near L1 and keep independent ids and resolve state. Browser bootstrap and runtime actions synchronize the active records with `RuntimeContext`. - ---- - -## 8. Files, Skills, and External Work - -Persistent files are managed by `utils/attached_files_store.py`. Each file has a stable id, metadata record, pin state, and filesystem entry under `assets/files/`. Store reconciliation removes references to missing physical files and keeps pin state consistent. - -A file can be present directly through pin/attach state or indirectly through a loaded Delayed Memory report. The same id is used by prompt assembly, report links, file actions, memory panels, and Live Avatar state. - -Skills are context resources under `assets/skills/`. Runtime actions load and unload skill instructions for a sequence. Python skill execution is limited to `.py` files inside the selected skill directory and uses bounded execution controls. - -Search actions produce trusted tool results. `WEB_SEARCH` uses the configured search provider. `runtime/deep_web_search.py` runs the bounded multi-query Service workflow used by `DEEP_WEB_SEARCH`. - ---- - -## 9. Persistence and Reconciliation - -JIN splits persistence by ownership. - -Browser storage keeps session-facing state used for resume and direct UI interaction. Server-side files keep durable reports, long-term facts, uploaded files, and logs. - -| State | Storage | -|---|---| -| Delayed Memory | `memory/delayed/*.json` | -| L4 long-term facts | `memory/facts/long_term_facts.json` | -| persistent files | `assets/files/` plus file index | -| chat/reasoning logs | `logs/` | -| session-facing runtime state | browser storage | - -Bootstrap and mutation paths reconcile ids across stores. File deletion clears file references, L4 merge/delete/restore remaps report references, and delayed-report synchronization filters stale links. This keeps cross-layer ids usable after independent edits from the UI or runtime actions. - -Core files: `ui/static/js/runtime/runtime-storage.js`, `utils/delayed_memory_file_store.py`, `utils/long_term_facts_file_store.py`, `utils/attached_files_store.py`, `websocket/bootstrap.py`. - ---- - -## 10. Recovery and Limits - -Generation state is tracked on `RuntimeContext` and `RuntimeStream`. User abort cancels the active task, closes stream state, records aborted actions, and can schedule the interrupted L1 update. - -Stream validation detects repetition and malformed generation states. Context/output-limit recovery can start a continuation tick with preserved reasoning edges and the current sequence state. - -Runtime loops have explicit ceilings: Brain follow-ups, Deep Web Search calls, skill execution, action retries, and related worker paths all use configured or hard limits. - -Core files: `runtime/stream.py`, `utils/stream_validator.py`, `utils/runtime_action_abort.py`, `agent/nodes/brain.py`, `runtime/deep_web_search.py`. - ---- - -## 11. UI Synchronization - -The browser receives typed WebSocket events and updates the relevant projection of runtime state. Memory panels, context view, logger, action bubbles, attachment plaques, reasoning references, and Live Avatar share ids from the runtime objects they represent. - -Typical synchronization paths are: - -| Runtime change | Browser projections | -|---|---| -| L1 snapshot | runtime panel, context state, Live Avatar | -| L4 update | long-term panel, delayed links, Live Avatar | -| delayed load/pin | report state, context state, linked facts/files, Live Avatar | -| file pin/delete | attachment state, report links, context state, Live Avatar | -| runtime action | action bubble, logger, sequence history | -| reasoning/reference match | text highlight, matching memory/file/avatar item | - -The Live Avatar uses the same memory and file identities as the panels. Its rendering rules and visual priorities live in `LIVE_AVATAR.md`. - -UI state-only updates prefer in-place synchronization where stable identity matters. Full rerenders are used when the represented object set changes. - ---- - -## 12. Change Rules - -These rules describe the boundaries used by the current codebase when adding behavior. - -**Persistent state gets one canonical owner.** New persistent data needs a store, stable identity, bootstrap path, mutation path, and reconciliation rules. - -**Prompt sections come from canonical state.** Context builders serialize current runtime state for a Brain tick; mutations happen through the owning subsystem. - -**Runtime actions cross the contract boundary.** A new action needs a contract, payload handling, guard semantics where required, dispatcher integration, result/follow-up behavior, and tests. - -**Cross-entity ids are maintained during mutation.** Merge, delete, restore, pin, load, and detach operations update linked reports, facts, files, and UI projections. - -**Internal continuations keep sequence identity.** Tool results, attachments, reasoning recovery, and follow-up limits stay attached to the original user sequence. - -**UI primitives map to semantic states.** New backend behavior reuses existing loaded, pinned, active, archived, linked, referenced, and hover states when those states describe it accurately. A new visual primitive is added when it represents a new persistent distinction in runtime state. - -**Long-running paths are bounded.** Agent continuations, workers, retries, and executable skills keep explicit ceilings. - ---- - -## 13. Tests - -The suite combines Python runtime tests with browser/client contract tests. Runtime tests cover agent routing, memory engines, actions, search, recovery, persistence, and token budgeting. `*_client_contract.py` tests protect the state and event shapes shared by Python and JavaScript. - -For architecture changes, the useful test boundary is the full state path: canonical store → runtime mutation → emitted event → browser contract. Changes that alter cross-layer ids also need merge/delete/restore or load/unload coverage. - -The current test commands and model-dependent probes are documented in `README.md`. - ---- - -## 14. Main Extension Points - -For a new runtime action, start in `contracts/`, then connect the handler in `utils/actions/`, add any guard or follow-up semantics, and cover the emitted event/state mutation in tests. - -For a new memory behavior, start from its owner and lifetime: L1/L2/L3/L4, Facts Memory, Delayed Memory, or Active Memory. Add persistence and reconciliation only for state that survives the active context. - -For a new browser projection, consume the existing semantic ids and runtime events from `ui/static/js/runtime/`. Avatar-specific rendering stays in `runtime-avatar.js` and `runtime-avatar.css`; the visual contract stays in `LIVE_AVATAR.md`. - -For a new model-side context source, add serialization in `utils/context/` or `rules/brain_context_builder.py` and keep the underlying state in its owning runtime/store module. diff --git a/README.md b/README.md index 44f6acaa..709bc2c2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **JIN Core Engine** is an experimental cognitive runtime for OpenAI-compatible models with **visible memory, session continuity, and model-driven actions.** -JIN is designed for long-running interaction. It carries model state forward, exposes the context shaping the current response, lets the model act on that state, and restores explicitly saved sessions through a compact session handoff. +JIN is designed for long-running interaction. It carries model state forward, exposes the context shaping the current response, lets the model act on that state, and can restore archived sessions through the dedicated bootstrap/restore path. The main chat stays visually simple while memory layers, reasoning, context pressure, runtime actions, persistent files, and action history remain accessible in collapsible panels. @@ -23,7 +23,7 @@ The JIN workspace combines the chat stream, draggable/collapsible runtime panels

      Live Avatar visualizes JIN's runtime state in real time.

      -

      Inner orbits react to live L1 memory changes, while outer signal rings track Delayed Memory, L4 facts, Active Memory, and persistent files.

      +

      Inner orbits react to live FRAME/runtime-memory changes, while outer signal rings track Delayed Memory, L4 facts, Active Memory, and persistent files.

      The avatar is interactive: reasoning references light up matching runtime signals, and hovering linked signals reveals the related memory and files.

      During reasoning, the avatar shifts into a dedicated motion state. Runtime actions can also change its size and tint the workspace, giving the model a small visual language beyond text.

      @@ -35,37 +35,36 @@ The JIN workspace combines the chat stream, draggable/collapsible runtime panels ## Memory Architecture -JIN organizes memory by cognitive purpose across four layers and dedicated memory systems. +JIN no longer uses the old four-layer L1/L2/L3/L4 hierarchy. The current user-facing memory panel has five stable views: **FRAME**, **ACTIVE**, **DELAYED**, **L-T**, and **FILES**. -### The Four-Layer Memory Model +### FRAME / Live Runtime Memory -* **L1 — Live Facts:** The current topic, request, task state, decisions, feedback, and unresolved points needed for the next turn. -* **L2 — Patterns:** Repeated inputs, stalled loops, and short-term interaction patterns that may require a strategy change. -* **L3 — Session Digest:** A compact handoff created by an explicit session save. It preserves the state used to re-enter an archived conversation. Soft reconnect is a separate transport-resume path. -* **L4 — Long-Term Facts:** Stable facts, preferences, project decisions, constraints, and environment details that should remain available across sessions. - -Every accepted L1 update is stored as a versioned snapshot. The runtime-memory timeline can be stepped through visually, with new and changed fields highlighted across snapshots. +**FRAME** is the UI name for the current live runtime-memory snapshot. Backend implementation still lives under `runtime/L1_memory*`; the rename is intentionally presentation-only. FRAME keeps the current topic, request/task state, decisions, feedback, and unresolved points needed by upcoming turns. Accepted updates are versioned as snapshots so the UI can step through diffs and inspect what changed. ![Runtime memory snapshot timeline](ui/static/images/runtime-highlight.png) +### Active Memory + +**Active Memory** keeps unfinished intentions and pending commitments separate from the general conversation state. Conditions and unresolved contracts remain active across turns until they are fulfilled, cancelled, paused, or explicitly resolved. Relevant active records are projected back into Brain context without changing their canonical storage order. + ### Delayed Memory -JIN uses **Delayed Memory** for larger pieces of context that enter the prompt when they are relevant. Discussions, specifications, and reports are stored as structured objects with tags and links to L4 facts or persistent files. Reports can be **loaded or unloaded by the runtime**, **pinned from the UI**, or **activated by matching user-text tags**. +**Delayed Memory** stores larger structured context that should be available without living in every prompt. Reports can link L4 facts and persistent files, can be loaded/unloaded by runtime actions, pinned from the UI, or surfaced from matching user-text tags. -### Active Memory +### L-T / L4 Long-Term Facts -**Active Memory** keeps unfinished intentions and pending commitments separate from the general conversation state. Conditions, recall rules, and other unresolved contracts remain active across turns until they are fulfilled, cancelled, or explicitly resolved. Relevant stored conditions are carried back into the Brain context on later turns. +**L-T** is the UI view of canonical L4 durable facts: stable user/project facts, preferences, constraints, decisions, and environment details that should survive sessions. An internal Facts Memory candidate buffer feeds idle L4 extraction/merge; it is not a sixth user-facing memory tab or a revived L2/L3 layer. -### Facts Memory and L4 Consolidation +### Files -**Facts Memory** is an inspectable index of durable context candidates collected from runtime state. During idle consolidation, JIN merges duplicate or overlapping facts with the existing L4 store. Delayed Memory reports can absorb ordinary L4 facts while essential anchor facts remain directly exposed, keeping the injected context compact. +**FILES** exposes the persistent uploaded-file library. Stored files keep stable IDs and can be attached/detached across turns or linked from Delayed Memory. ## Core Capabilities * **Visible Reasoning:** Displays provider/model reasoning separately from the final answer when the backend exposes a reasoning stream. * **Reasoning References:** Maps direct references back to runtime rules, memory records, restored context, and linked runtime objects. -* **Inspectable Memory:** Keeps live state, session handoff, long-term facts, delayed reports, and active commitments as separate visible systems. -* **Session Continuity:** Supports soft WebSocket resume plus explicit session save and archived restore, with continuity state carried through memory and the session handoff. +* **Inspectable Memory:** Keeps FRAME/live state, long-term facts, delayed reports, active commitments, persistent files, and continuity checkpoints as separate systems. +* **Session Continuity:** Supports soft WebSocket resume, atomic browser checkpoints for reload/new-tab continuity, and explicit archived-session restore from persisted logs. * **Persistent Files:** Stores uploaded text, images, PDFs, and other files under stable ids; the same stored files can be attached to or detached from context across turns. * **Runtime Telemetry:** Shows model status, token usage, context pressure, memory updates, action state, and runtime logs. * **Interruptible Generation:** Stops an active response while preserving the logical session and a dedicated interrupted-memory path. @@ -79,9 +78,8 @@ JIN can request an action while answering. The runtime validates and executes it Available actions include: * one-shot web search and bounded multi-query Deep Web Search; -* explicit session save, with archived restore handled by the session bootstrap flow; * Active Memory creation and resolution; -* Delayed Memory save, update, load, and unload; +* Delayed Memory save, load, and unload; * persistent file listing, attachment, and detachment; * focused L4 fact updates and reconciliation; * asset and skill discovery, including bounded local Python skills; @@ -102,8 +100,8 @@ A normal turn follows this path: 3. The Brain streams reasoning and visible answer content through separate runtime channels. 4. Stream validation guards repetition and malformed generation while private runtime-action markers are extracted. 5. Runtime Actions can mutate state or return trusted results; actions that need another model step continue inside the same user sequence. -6. After the visible turn completes, the Service model schedules the L1/L2 memory update. -7. A later user turn waits for any pending memory update, then receives current L1/L2/L3/L4 state, relevant Delayed/Active Memory, files, skills, and action results. +6. After the visible turn completes, the logical Service route performs background FRAME/L1 integration; if no dedicated Service endpoint is configured, this route reuses the Brain client. +7. A later user turn waits for any pending FRAME/L1 update, then receives current Active Memory, recent chat beside ``, loaded Delayed Memory, L4 facts, files/skills, action history, and trusted tool results. The model path is intentionally direct: @@ -122,9 +120,11 @@ The runtime separates model work into roles: * **Brain:** visible reasoning, responses, and runtime decisions; * **Service:** background memory updates and supporting work. -JIN is model-agnostic at the API boundary. Brain and Service roles can use different models: the Brain benefits most from stronger reasoning, while the Service role can use a smaller/faster model for background work. `USE_SERVICE_AS_BRAIN = True` lets one configured model handle both roles while preserving their logical separation. +JIN is model-agnostic at the API boundary. **Brain is the only foreground response route.** Service is background-only. `SERVICE_API_BASE` is optional: when it is empty, the Service client aliases the Brain client, so one physical model can handle both logical roles without changing foreground routing. Set `SERVICE_API_BASE` only when a dedicated background Service node exists. + +Older local configs that still contain `USE_SERVICE_AS_BRAIN = True` are accepted only by a localized migration adapter: their old Service endpoint is promoted to the canonical Brain settings, the legacy flag is removed during normalization, and no runtime code branches on it. Archived `SERVICE` response labels are likewise reader compatibility, not a current response mode. -On Windows, the LM Studio launcher can fill unset/default model ids from a loaded Gemma-family model and currently recommends `google/gemma-3-12b-it`. Explicit provider URLs and model ids remain unchanged. +On Windows, the LM Studio launcher can fill unset/default Brain model settings from a loaded Gemma-family model and can separately initialize a dedicated Service endpoint when one is configured. Explicit provider URLs and model ids remain unchanged. ### Runtime Storage @@ -178,7 +178,7 @@ JIN can inspect available skills, attach the one required for the current task, |-- launch_jin.ps1 # LM Studio readiness and startup script |-- requirements.txt # Python dependencies |-- package.json # Test and probe commands -|-- ARCHITECTURE.md # Runtime ownership, data flow, persistence +|-- docs/ # Current architecture, state, and durable decisions `-- LIVE_AVATAR.md # Avatar visual-state contract ``` @@ -202,7 +202,7 @@ For LM Studio, JIN also probes the provider-native `/api/v1/models` metadata end ### Windows + LM Studio -1. Install LM Studio and load the model or models you want JIN to use. A single model is enough when `USE_SERVICE_AS_BRAIN = True`; with untouched/default model ids, the launcher selects a loaded Gemma-family model automatically. +1. Install LM Studio and load the Brain model you want JIN to use. A single model is enough by default: with `SERVICE_API_BASE` left empty, background Service work reuses Brain. Configure a second endpoint only if you want a dedicated Service model. 2. Start the LM Studio Local Server. 3. Run: @@ -261,12 +261,11 @@ Copy `config.example.py` to `config.py`, then set the provider URLs and model ID | Option | Purpose | | --- | --- | -| `USE_SERVICE_AS_BRAIN` | Use the service model for visible brain responses. | -| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Configure the Brain provider and model. | -| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Configure the Service provider and model. | +| `BRAIN_API_BASE`, `BRAIN_MODEL_UID` | Configure the required foreground Brain provider and model. | +| `SERVICE_API_BASE`, `SERVICE_MODEL_UID` | Optionally configure a dedicated background Service provider/model. Leave `SERVICE_API_BASE` empty to reuse Brain. | | `FOLLOW_UP_ON_LIMIT` | Continue a Brain generation in an internal tick when the provider stops at its output/context limit. | -| `BRAIN_CONTEXT_WINDOW`, `SERVICE_CONTEXT_WINDOW` | Set UI/reference context denominators; live request budgets can come from the context window reported by the loaded model. | -| `BRAIN_IMAGE_INPUT_ENABLED`, `SERVICE_IMAGE_INPUT_ENABLED` | Send image attachments to compatible model roles. | +| `BRAIN_CONTEXT_WINDOW`, `SERVICE_CONTEXT_WINDOW` | Set UI/reference context denominators; optional Service values inherit Brain when no dedicated Service is configured. Live request budgets can come from the loaded model metadata. | +| `BRAIN_IMAGE_INPUT_ENABLED` | Allow image attachments on the foreground Brain request when the selected provider/model supports OpenAI-compatible image input. | | `L4_MEMORY_ENABLED`, `L4_IDLE_SECONDS` | Enable L4 consolidation and set its idle delay. | | `SEARCH_SERPER_API_KEY`, `SEARCH_MAX_RESULTS` | Configure built-in web search. | | `DEEP_WEB_SEARCH_MAX_*` | Bound the Service-worker research sequence used by Deep Web Search. | diff --git a/app.py b/app.py index d2619d74..3fc64ccf 100644 --- a/app.py +++ b/app.py @@ -57,7 +57,11 @@ stop_l4_memory_server_scheduler, ) -from runtime.state import RUNTIME_MEMORY_SUMMARIZER_LABEL +from runtime.registry import runtime_state +from runtime.state import ( + BRAIN_RUNTIME_ID, + SERVICE_RUNTIME_ID, +) from runtime.behavior_contract import ( get_behavior_contract, ) @@ -302,16 +306,7 @@ def build_anonymous_mode_config(): } -def build_runtime_config( - use_service_as_brain=None, -): - - effective_use_service_as_brain = ( - config.USE_SERVICE_AS_BRAIN - if use_service_as_brain is None - else use_service_as_brain - ) - +def build_runtime_config(): return { "service": { "label": "service", @@ -323,27 +318,11 @@ def build_runtime_config( }, "brain": { "label": "brain", - "model": ( - config.SERVICE_MODEL_UID - if effective_use_service_as_brain - else config.BRAIN_MODEL_UID - ), - "used_tokens": 0, - "context_tokens": 0, - "total_tokens": 0, - "max_tokens": ( - config.SERVICE_CONTEXT_WINDOW - if effective_use_service_as_brain - else config.BRAIN_CONTEXT_WINDOW - ), - }, - RUNTIME_MEMORY_SUMMARIZER_LABEL: { - "label": RUNTIME_MEMORY_SUMMARIZER_LABEL, - "model": config.SERVICE_MODEL_UID, + "model": config.BRAIN_MODEL_UID, "used_tokens": 0, "context_tokens": 0, "total_tokens": 0, - "max_tokens": config.SERVICE_CONTEXT_WINDOW, + "max_tokens": config.BRAIN_CONTEXT_WINDOW, }, } @@ -458,6 +437,40 @@ def apply_runtime_config_values( value, ) + if not settings.SERVICE_CONFIGURED: + fallback_pairs = { + "SERVICE_API_BASE": "BRAIN_API_BASE", + "SERVICE_MODEL_UID": "BRAIN_MODEL_UID", + "SERVICE_CONTEXT_WINDOW": "BRAIN_CONTEXT_WINDOW", + "SERVICE_REQUEST_TIMEOUT": "BRAIN_REQUEST_TIMEOUT", + } + for service_name, brain_name in fallback_pairs.items(): + value = getattr( + config, + brain_name, + ) + setattr( + config, + service_name, + value, + ) + object.__setattr__( + settings, + service_name, + value, + ) + + runtime_state.update_runtime_state( + BRAIN_RUNTIME_ID, + model=settings.BRAIN_MODEL_UID, + max_tokens=settings.BRAIN_CONTEXT_WINDOW, + ) + runtime_state.update_runtime_state( + SERVICE_RUNTIME_ID, + model=settings.SERVICE_MODEL_UID, + max_tokens=settings.SERVICE_CONTEXT_WINDOW, + ) + if ( application is None or not hasattr(application.state, "http_client") @@ -530,11 +543,6 @@ async def index( request, "index.html", { - "use_service_as_brain": ( - status_snapshot[ - "use_service_as_brain" - ] - ), "runtime_config": ( status_snapshot[ "runtime_config" @@ -543,6 +551,11 @@ async def index( "runtime_status": { "brain": status_snapshot["brain"], "service": status_snapshot["service"], + "service_configured": ( + status_snapshot[ + "service_configured" + ] + ), }, "format_response": ( status_snapshot["format_response"] @@ -658,55 +671,54 @@ async def build_status_snapshot( client: httpx.AsyncClient, ): - service_request = fetch_runtime_model_status( + brain_request = fetch_runtime_model_status( client, - base_url=config.SERVICE_API_BASE, - model_uid=config.SERVICE_MODEL_UID, + base_url=config.BRAIN_API_BASE, + model_uid=config.BRAIN_MODEL_UID, configured_context_window=( - config.SERVICE_CONTEXT_WINDOW + config.BRAIN_CONTEXT_WINDOW ), ) - if config.USE_SERVICE_AS_BRAIN: - service_status = await service_request - brain_status = service_status - else: - ( - brain_status, - service_status, - ) = await asyncio.gather( + if settings.SERVICE_CONFIGURED: + brain_status, service_status = await asyncio.gather( + brain_request, fetch_runtime_model_status( client, - base_url=config.BRAIN_API_BASE, - model_uid=config.BRAIN_MODEL_UID, + base_url=config.SERVICE_API_BASE, + model_uid=config.SERVICE_MODEL_UID, configured_context_window=( - config.BRAIN_CONTEXT_WINDOW + config.SERVICE_CONTEXT_WINDOW ), ), - service_request, ) + else: + brain_status = await brain_request + service_status = { + "online": False, + "source": "", + "url": "", + "available_models": [], + "loaded": None, + "model": {}, + "loaded_model": {}, + } brain_online = bool(brain_status.get("online")) service_online = bool(service_status.get("online")) - effective_use_service_as_brain = ( - config.USE_SERVICE_AS_BRAIN - and service_online - ) - - runtime_config = build_runtime_config( - use_service_as_brain=( - effective_use_service_as_brain - ), - ) + runtime_config = build_runtime_config() runtime_config["service"]["lm_studio"] = service_status runtime_config["brain"]["lm_studio"] = brain_status return { "brain": brain_online, "service": service_online, - "use_service_as_brain": ( - effective_use_service_as_brain + "service_configured": settings.SERVICE_CONFIGURED, + "service_route": ( + "dedicated" + if settings.SERVICE_CONFIGURED + else "brain_fallback" ), "format_response": bool( getattr( @@ -757,6 +769,17 @@ async def api_update_runtime_config(request: Request): detail="Invalid runtime role", ) + if ( + role == "service" + and not settings.SERVICE_CONFIGURED + ): + raise HTTPException( + status_code=409, + detail=( + "Dedicated Service runtime is not configured" + ), + ) + updates: dict[str, object] = {} if "model" in payload: diff --git a/app_settings.py b/app_settings.py index 71051971..52b9f161 100644 --- a/app_settings.py +++ b/app_settings.py @@ -57,9 +57,10 @@ class AppSettings: MODELS_ENDPOINT: str NATIVE_MODELS_ENDPOINT: str - USE_SERVICE_AS_BRAIN: bool FORMAT_RESPONSE: bool + SERVICE_CONFIGURED: bool + SERVICE_API_BASE: str SERVICE_MODEL_UID: str SERVICE_CONTEXT_WINDOW: int @@ -89,13 +90,19 @@ class AppSettings: "/api/v1/models", ), - USE_SERVICE_AS_BRAIN=config.USE_SERVICE_AS_BRAIN, FORMAT_RESPONSE=getattr( config, "FORMAT_RESPONSE", True, ), + SERVICE_CONFIGURED=bool( + getattr( + config, + "SERVICE_CONFIGURED", + False, + ) + ), SERVICE_API_BASE=config.SERVICE_API_BASE, SERVICE_MODEL_UID=config.SERVICE_MODEL_UID, SERVICE_CONTEXT_WINDOW=config.SERVICE_CONTEXT_WINDOW, diff --git a/clients/brain_client.py b/clients/brain_client.py index 772057f2..a131a67e 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -54,11 +54,6 @@ upsert_session_action_marker_history_since, ) -from clients.service_client import ( - ask_service_model, - ask_service_model_stream, -) - from clients.response_extractor import ( ResponseExtractor, ) @@ -66,6 +61,7 @@ from runtime.client import ( LMStudioAPIError, ) +from runtime.state import BRAIN_RUNTIME_ID from utils.actions import ( @@ -92,21 +88,11 @@ def get_brain_runtime_id() -> str: - - return ( - config.SERVICE_MODEL_UID - if config.USE_SERVICE_AS_BRAIN - else config.BRAIN_MODEL_UID - ) + return BRAIN_RUNTIME_ID def get_brain_fallback_context_window() -> int: - - return ( - config.SERVICE_CONTEXT_WINDOW - if config.USE_SERVICE_AS_BRAIN - else config.BRAIN_CONTEXT_WINDOW - ) + return config.BRAIN_CONTEXT_WINDOW def get_response_enabled_runtime_actions( @@ -196,21 +182,11 @@ def build_brain_user_prompt_content( context=None, ): - image_input_enabled = ( - bool( - getattr( - config, - "SERVICE_IMAGE_INPUT_ENABLED", - False, - ) - ) - if config.USE_SERVICE_AS_BRAIN - else bool( - getattr( - config, - "BRAIN_IMAGE_INPUT_ENABLED", - False, - ) + image_input_enabled = bool( + getattr( + config, + "BRAIN_IMAGE_INPUT_ENABLED", + False, ) ) @@ -364,100 +340,7 @@ async def ask_brain( ) # ----------------------------------------------------- - # SERVICE AS BRAIN - # ----------------------------------------------------- - - if config.USE_SERVICE_AS_BRAIN: - - try: - - result = await ask_service_model( - client=client, - user_prompt=model_user_prompt, - system_prompt=system_prompt, - temperature=config.BRAIN_TEMPERATURE, - max_tokens=None, - ) - - reasoning = ( - ResponseExtractor.extract_reasoning_text( - result - ) - ) - - content = ( - ResponseExtractor - .extract_content_text( - result - ) - ) - - enabled_actions = get_response_enabled_runtime_actions( - runtime_actions, - text, - ) - - content_actions = ( - extract_runtime_actions( - content, - enabled_actions=enabled_actions, - ) - ) - - await log_runtime_action_marker_removals( - context, - content_actions, - source="brain content", - ) - - ( - confirmed_action_ids, - rejected_action_ids, - guard_confirmation_ids, - action_display_ids, - ) = await confirm_runtime_action_guards( - context, - content_actions.actions, - user_message=text, - context_snapshot=action_context_snapshot, - ) - - await apply_runtime_action_calls( - context, - content_actions.actions, - user_message=text, - context_snapshot=action_context_snapshot, - assistant_message=content, - confirmed_action_ids=confirmed_action_ids, - rejected_action_ids=rejected_action_ids, - guard_confirmation_ids=guard_confirmation_ids, - action_display_ids=action_display_ids, - runtime_message_id=runtime_message_id, - ) - - return content_actions.text - - except LMStudioAPIError: - - raise - - except Exception as error: - - formatted_error = ( - format_client_error( - "service_as_brain", - config.SERVICE_API_BASE, - config.SERVICE_MODEL_UID, - error, - ) - ) - - raise RuntimeError( - formatted_error - ) - - # ----------------------------------------------------- - # REAL BRAIN + # BRAIN # ----------------------------------------------------- try: @@ -2207,115 +2090,7 @@ async def apply_runtime_action_result( return True # ----------------------------------------------------- - # SERVICE AS BRAIN - # ----------------------------------------------------- - - if config.USE_SERVICE_AS_BRAIN: - - try: - - async for model_chunk in ( - ask_service_model_stream( - context=context, - client=client, - user_prompt=( - model_user_prompt - ), - system_prompt=( - resolved_system_prompt - ), - temperature=config.BRAIN_TEMPERATURE, - max_tokens=None, - ) - ): - - filtered_chunk = ( - await filter_runtime_action_chunk( - model_chunk - ) - ) - - if filtered_chunk: - yield filtered_chunk - - if stop_for_runtime_action: - break - - tail_result = ( - content_filter.flush_result() - if filter_runtime_actions - else RuntimeActionResult(text="") - ) - tail_counter_entries = ( - capture_observed_action_markers( - tail_result - ) - ) - capture_session_action_message_preview( - tail_result, - tail_counter_entries, - ) - await emit_action_counter_updates( - tail_counter_entries - ) - - await log_runtime_action_marker_removals( - context, - tail_result, - source="brain stream tail", - ) - - await apply_runtime_action_result( - tail_result - ) - - if await stop_on_marker_repetition( - tail_result - ): - await finalize_session_action_history() - yield build_raw_model_output_chunk() - return - content_tail = tail_result.text - if ( - content_tail - and not stop_for_runtime_action - ): - yield { - "type": "content", - "content": content_tail, - } - - await finalize_session_action_history() - yield build_raw_model_output_chunk() - return - - except asyncio.CancelledError: - await finalize_session_action_history() - raise - - except LMStudioAPIError: - await finalize_session_action_history() - raise - - except Exception as error: - - await finalize_session_action_history() - - formatted_error = ( - format_client_error( - "service_as_brain", - config.SERVICE_API_BASE, - config.SERVICE_MODEL_UID, - error, - ) - ) - - raise RuntimeError( - formatted_error - ) - - # ----------------------------------------------------- - # REAL BRAIN + # BRAIN # ----------------------------------------------------- try: diff --git a/clients/registry.py b/clients/registry.py index f33b7507..ee0a7428 100644 --- a/clients/registry.py +++ b/clients/registry.py @@ -9,45 +9,28 @@ def build_clients( http_client, ): - clients = { - - "service": RuntimeClient( - api_base=( - settings.SERVICE_API_BASE - ), - model_uid=( - settings.SERVICE_MODEL_UID - ), - timeout=( - settings - .SERVICE_REQUEST_TIMEOUT - ), - configured_context_window=( - settings.SERVICE_CONTEXT_WINDOW - ), - client=http_client, + brain_client = RuntimeClient( + api_base=settings.BRAIN_API_BASE, + model_uid=settings.BRAIN_MODEL_UID, + timeout=settings.BRAIN_REQUEST_TIMEOUT, + configured_context_window=( + settings.BRAIN_CONTEXT_WINDOW ), - } - - # --------------------------------------------------------- - # DEDICATED BRAIN RUNTIME - # --------------------------------------------------------- + client=http_client, + ) - if not settings.USE_SERVICE_AS_BRAIN: + clients = { + "brain": brain_client, + "service": brain_client, + } - clients["brain"] = RuntimeClient( - api_base=( - settings.BRAIN_API_BASE - ), - model_uid=( - settings.BRAIN_MODEL_UID - ), - timeout=( - settings - .BRAIN_REQUEST_TIMEOUT - ), + if settings.SERVICE_CONFIGURED: + clients["service"] = RuntimeClient( + api_base=settings.SERVICE_API_BASE, + model_uid=settings.SERVICE_MODEL_UID, + timeout=settings.SERVICE_REQUEST_TIMEOUT, configured_context_window=( - settings.BRAIN_CONTEXT_WINDOW + settings.SERVICE_CONTEXT_WINDOW ), client=http_client, ) diff --git a/clients/service_client.py b/clients/service_client.py index cbd7a106..05671eaf 100644 --- a/clients/service_client.py +++ b/clients/service_client.py @@ -1,14 +1,20 @@ import asyncio +from runtime.memory_common import ( + refresh_service_runtime_usage, +) + async def ask_service_model( *, client, + context=None, user_prompt, system_prompt: str = "", temperature: float, max_tokens: int | None, timeout: float | None = None, + track_usage: bool = True, ): request = { @@ -21,10 +27,27 @@ async def ask_service_model( if timeout is not None: request["timeout"] = timeout - return await client.ask( + if track_usage: + await refresh_service_runtime_usage( + context, + system_prompt=system_prompt, + user_prompt=user_prompt, + ) + + response = await client.ask( **request ) + if track_usage: + await refresh_service_runtime_usage( + context, + system_prompt=system_prompt, + user_prompt=user_prompt, + response=response, + ) + + return response + async def ask_service_model_stream( *, diff --git a/config.example.py b/config.example.py index 21cbdb48..93e3d12c 100644 --- a/config.example.py +++ b/config.example.py @@ -1,6 +1,5 @@ # Copy this file to config.py and adjust values for your local nodes. -USE_SERVICE_AS_BRAIN = True FORMAT_RESPONSE = True DEBUG_RULE_CITATIONS = True LOG_CHAT = True @@ -114,24 +113,23 @@ BRAIN_IMAGE_INPUT_ENABLED = False # --------------------------------------------------------- -# SERVICE MODEL +# OPTIONAL SERVICE MODEL # --------------------------------------------------------- -SERVICE_API_BASE = "http://service-host:1234" +# Leave SERVICE_API_BASE empty to run L1/L4 and other background model work +# through the Brain endpoint. Set it only when a dedicated Service node exists. +SERVICE_API_BASE = "" -SERVICE_MODEL_UID = "service-model" +# Empty optional values inherit their Brain equivalents. The Windows launcher +# fills SERVICE_MODEL_UID from the dedicated endpoint when only its URL is set. +SERVICE_MODEL_UID = "" -SERVICE_REQUEST_TIMEOUT = 1000.0 +SERVICE_REQUEST_TIMEOUT = 0.0 -SERVICE_CONTEXT_WINDOW = 4096 +SERVICE_CONTEXT_WINDOW = 0 SERVICE_TEMPERATURE = 0.1 - -# Enable only when the selected runtime/model accepts OpenAI-compatible -# multimodal chat content with {"type": "image_url"} user message parts. -SERVICE_IMAGE_INPUT_ENABLED = False - # --------------------------------------------------------- # L4 LONG-TERM MEMORY # --------------------------------------------------------- diff --git a/config_loader.py b/config_loader.py index 69e07f18..e8f772b2 100644 --- a/config_loader.py +++ b/config_loader.py @@ -103,6 +103,127 @@ def apply_env_overrides( return config_module +def normalize_model_role_config( + config_module, +): + """Resolve the optional Service role onto the required Brain runtime. + + ``USE_SERVICE_AS_BRAIN`` is accepted only as a legacy config adapter. It + is deliberately removed from the normalized module so no runtime path can + branch on the old topology. + """ + + legacy_service_as_brain = getattr( + config_module, + "USE_SERVICE_AS_BRAIN", + None, + ) + + if legacy_service_as_brain is True: + for suffix in ( + "API_BASE", + "MODEL_UID", + "REQUEST_TIMEOUT", + "CONTEXT_WINDOW", + ): + service_name = f"SERVICE_{suffix}" + brain_name = f"BRAIN_{suffix}" + service_value = getattr( + config_module, + service_name, + None, + ) + if service_value not in ( + None, + "", + 0, + 0.0, + ): + setattr( + config_module, + brain_name, + service_value, + ) + + # The legacy Service endpoint becomes the canonical Brain endpoint; + # background work therefore falls back to it instead of activating a + # second physical runtime accidentally. + setattr( + config_module, + "SERVICE_API_BASE", + "", + ) + + if hasattr( + config_module, + "USE_SERVICE_AS_BRAIN", + ): + delattr( + config_module, + "USE_SERVICE_AS_BRAIN", + ) + + raw_service_api_base = str( + getattr( + config_module, + "SERVICE_API_BASE", + "", + ) + or "" + ).strip() + service_configured = bool( + raw_service_api_base + ) + + setattr( + config_module, + "SERVICE_CONFIGURED", + service_configured, + ) + + brain_fallbacks = { + "SERVICE_API_BASE": getattr( + config_module, + "BRAIN_API_BASE", + ), + "SERVICE_MODEL_UID": getattr( + config_module, + "BRAIN_MODEL_UID", + ), + "SERVICE_REQUEST_TIMEOUT": getattr( + config_module, + "BRAIN_REQUEST_TIMEOUT", + ), + "SERVICE_CONTEXT_WINDOW": getattr( + config_module, + "BRAIN_CONTEXT_WINDOW", + ), + } + + for name, fallback in brain_fallbacks.items(): + value = getattr( + config_module, + name, + None, + ) + if ( + not service_configured + or value in ( + None, + "", + 0, + 0.0, + ) + ): + setattr( + config_module, + name, + fallback, + ) + + return config_module + + def load_config_from_path( path: Path, ): @@ -144,16 +265,20 @@ def load_config_module( example_path = ROOT / "config.example.py" if config_path.exists(): - return apply_env_overrides( - load_config_from_path( - config_path + return normalize_model_role_config( + apply_env_overrides( + load_config_from_path( + config_path + ) ) ) if example_path.exists(): - return apply_env_overrides( - load_config_from_path( - example_path + return normalize_model_role_config( + apply_env_overrides( + load_config_from_path( + example_path + ) ) ) diff --git a/docs/JIN_ARCHITECTURE.md b/docs/JIN_ARCHITECTURE.md index fa0b4ff1..061e1c3a 100644 --- a/docs/JIN_ARCHITECTURE.md +++ b/docs/JIN_ARCHITECTURE.md @@ -1,11 +1,11 @@ # JIN Core Engine — Current Architecture -**Verified snapshot:** `jin_core(20260826-090339).zip`
      -**Inspection date:** 2026-08-26
      -**Intent sources used for reconciliation:** current source/tests plus the accumulated 2026-08-23--26 project decisions
      +**Verified snapshot:** `jin_core(20260829-114751).zip`
      +**Inspection date:** 2026-08-29
      +**Intent sources used for reconciliation:** current source plus the accumulated 2026-08-23--29 project decisions; tests were intentionally not executed for this documentation audit
      **Purpose:** describe the architecture that is actually visible in the current source tree, while explicitly separating legacy compatibility from active design. -This document supersedes the architectural parts of the old root `ARCHITECTURE.md` and `README.md` wherever they still describe L2/L3 as live architectural layers or `SAVE_SESSION` as a current model action. +This document is the current architectural baseline. `README.md` is kept as the shorter product-facing overview; legacy tests/comments remain lower-confidence historical evidence. --- @@ -21,7 +21,7 @@ Core product properties: - session continuity across reloads/tabs/archived sessions; - persistent files and memory objects with stable IDs; - runtime state projected into the UI and Live Avatar; -- separate BRAIN and SERVICE roles even when both roles resolve to the same physical model. +- one foreground BRAIN route plus a logical background SERVICE role that may resolve to the same physical model. The codebase is intentionally not structured as a generic autonomous-agent framework. The main foreground path remains direct. @@ -48,11 +48,13 @@ Browser UI Background / secondary paths: completed foreground turn - -> L1 SERVICE summarization - -> L1 diff + Facts Memory companion state + -> logical SERVICE route + -> dedicated Service client when configured + -> otherwise the same physical client as Brain + -> FRAME/L1 integration + diff + Facts Memory companion state browser idle tick - -> L4 extraction/merge pipeline + -> L4 extraction/merge pipeline through the same SERVICE route ``` Main ownership by package: @@ -62,12 +64,12 @@ Main ownership by package: | `app.py` | FastAPI app, HTTP APIs, static files, session-restore endpoint, WebSocket router registration | | `websocket/` | connection lifecycle, queueing, bootstrap/resume, foreground turn orchestration, server->browser events | | `runtime/runtime_context.py` | live in-process state hub for one logical runtime | -| `agent/` | direct Brain execution and per-turn state | +| `agent/` | direct foreground Brain execution and per-turn state | | `runtime/stream.py` | model stream handling, reasoning/content/action separation, recovery/limits | | `contracts/` | canonical model-facing runtime-action contracts and action rule assembly | | `utils/actions/` | payload normalization, action execution, storage/state mutation | | `rules/brain_context_builder.py` | deterministic Brain prompt assembly from current state | -| `runtime/L1_memory*` | live runtime-memory summarization, snapshots, diffs, interrupted-turn memory path | +| `runtime/L1_memory*` | FRAME/live runtime-memory integration, snapshots, diffs, interrupted-turn memory path | | `runtime/L4_memory*` | Facts Memory ingestion, durable L4 extraction/merge, reconciliation, delete/restore | | `runtime/memory_attention.py` | prompt-only Active/Delayed/L4 relevance ranking | | `utils/*_store.py` | durable file/report/fact stores | @@ -133,10 +135,16 @@ There is no active planner/router node in front of Brain. Physical model endpoints are resolved through the client/config layer. Logical roles remain: -- **BRAIN** — foreground reasoning, visible answer, runtime decisions; -- **SERVICE** — L1/L4 integration and supporting model work. +- **BRAIN** — the only foreground reasoning/visible-answer/runtime-decision route; +- **SERVICE** — background FRAME/L1, L4, fact-check/research, document-skill, and supporting model work. -`USE_SERVICE_AS_BRAIN=True` may map both logical roles to one endpoint without removing the role distinction. +`utils/brain_client_utils.py::get_brain_runtime_config()` always returns label `brain`, and `BrainNode` resolves `context.clients["brain"]`. There is no active foreground branch to Service. + +`clients/registry.py` always builds the Brain client first and initially aliases `clients["service"]` to that same object. Only an explicitly configured `SERVICE_API_BASE` replaces the alias with a dedicated Service client. Therefore a one-model setup is the default topology, but the logical Service role remains background-only. + +`config_loader.py::normalize_model_role_config()` accepts `USE_SERVICE_AS_BRAIN=True` only as a migration adapter for old local configs: it promotes the old Service endpoint/settings into canonical Brain settings, clears the dedicated Service URL, deletes the legacy attribute, and then normalizes Service fallbacks. The Windows launcher contains matching legacy detection only so startup can find/migrate those old configs safely. No live runtime path reads the flag. + +Historical archives can still contain `role=service` / `RUNTIME_MODE=SERVICE`, and the logger UI retains presentation code for old `[SERVICE]` model-output cards. Those are reader compatibility paths, not a current response mode. --- @@ -156,15 +164,16 @@ The current high-level order is: 8. skills inventory; 9. loaded skill content; 10. runtime-context group: - - explicit user feedback; + - explicit user feedback / retry context when present; - Active Memory view; - - L1/runtime memory snapshot; + - ordinary turns: ``; + - current `` snapshot; - visible session counters; - runtime TODO state; - loaded Delayed Memory bodies; - L4 long-term memory; - zero-diff stall alert; -11. archived exact-dialogue priming block, otherwise rolling recent visible messages; +11. archived exact-dialogue priming block when restoring; ordinary rolling chat is already adjacent to FRAME above; 12. archived reasoning dump, otherwise previous-reasoning loop/crop; 13. runtime-action instructions assembled from current contracts; 14. identity block; @@ -279,9 +288,9 @@ Active backend modules: - `runtime/L1_memory_rules.py` - `runtime/L1_memory_utils.py` -L1 is the compact live runtime state used for the next turns. It is updated by SERVICE after foreground turns, has snapshots/diffs, and has a distinct interrupted-turn update path. +L1 is the backend implementation of the compact live runtime state exposed as **FRAME** in the UI and as `` in Brain context. It is integrated through the logical Service route after foreground turns, has snapshots/diffs, and has a distinct interrupted-turn update path. With no dedicated Service endpoint, that background route deliberately reuses the Brain client. -It is not a durable long-term memory tier. +It is not a durable long-term memory tier, and `FRAME` is not a global rename of internal L1/runtime identifiers. ### 7.2 L2 and L3 @@ -585,13 +594,13 @@ Use existing semantic IDs/events and existing CSS/DOM primitives first. New visu ## 14. Known architectural traps -Do not infer architecture from these alone: +Do not infer current architecture from compatibility/history alone: -- old root `README.md` / `ARCHITECTURE.md`; -- `tests/test_l2_memory.py` and `tests/test_l3_session_memory.py`; -- `SAVE_SESSION` references in old tests/log parsing; -- comments mentioning “L1/L2/L3 glow”; -- stale CodeGraph/search indexes; +- `USE_SERVICE_AS_BRAIN`, archived `RUNTIME_MODE=SERVICE`, archived `role=service`, or old `[SERVICE]` logger-card code; +- `SAVE_SESSION` references in tests/log parsing; +- comments/log filters mentioning “L1/L2/L3 glow”; +- pre-L3-removal storage migration keys/fields; +- stale repository indexes or historical test assumptions; - historical field names like `runtime_l3_session_memory`; - the name `find_latest_completed_session_restore_payload()`; current selection is newest real USER move, including USER-only interruption; - treating any write to a browser checkpoint as permission to advance `saved_at`; diff --git a/docs/JIN_CURRENT_STATE.md b/docs/JIN_CURRENT_STATE.md index a10991e0..6201a7b7 100644 --- a/docs/JIN_CURRENT_STATE.md +++ b/docs/JIN_CURRENT_STATE.md @@ -1,8 +1,8 @@ # JIN Core Engine — Current State / Migration Notes -**Snapshot inspected:** `jin_core(20260828-060531).zip`
      -**Inspection date:** 2026-08-28
      -**Context reference:** current source/tests plus the accumulated 2026-08-23--28 project decisions +**Snapshot inspected:** `jin_core(20260829-114751).zip`
      +**Inspection date:** 2026-08-29
      +**Context reference:** current production source plus the accumulated 2026-08-23--29 project decisions. Tests were explicitly excluded from this audit and were not executed. This is the document to read before touching transitional code. It lists what is true in the inspected snapshot, what is legacy residue, and where product intent and implementation currently differ. @@ -10,27 +10,24 @@ This is the document to read before touching transitional code. It lists what is ## 1. Executive state -The runtime is already on the post-L2/L3 architecture in the actual source tree, but the repository still contains substantial historical residue: - -- old root documentation still describes four live layers; -- L2/L3 tests still exist and import modules that no longer exist; -- old session names/fields remain in compatibility/tests; -- `SAVE_SESSION` remains in historical tests/log parsing but is not a current action contract; -- Active Memory has a modern flat JSON action boundary but an older string-record internal/browser representation; -- search action exposure now follows real configured provider availability rather than feature flags alone; -- `CLEAN_TOOL_RESULTS` now has explicit cleared-state bootstrap semantics so old tool results cannot resurrect without falsifying checkpoint freshness; -- session-action logger metadata survives bootstrap, including JIN_COLOR swatches/hex hover; -- `JIN_COLOR`/`JIN_SIZE` now advertise paired XML, while legacy inline forms remain parser-only compatibility; -- normal bootstrap restores a bounded three-USER-move chat tail plus a current-session divider, including interrupted/action-only USER-only rows; -- Brain recent-message context keeps the newest three pairs in full, with newline/XML normalization but no per-message character crop; -- ordinary Brain turns again include the previous successful reasoning block with explicit middle-crop semantics, while follow-ups keep their dedicated reasoning context; -- browser continuity now uses page-ephemeral `jin.liveRuntimeMemory.v2` plus one atomic `jin.sessionCheckpoint.v2`; legacy per-session FRAME selection is migration-only and never freshness-scanned; -- common-checkpoint ownership, L1 origin lineage, completed-turn time, and dialogue freshness are now explicitly separated; +The production runtime is on the post-L2/L3, Brain-first architecture and the root documentation is now synchronized with it. + +Current high-signal state: + +- foreground user turns always execute through `AgentRuntime -> BrainNode -> context.clients["brain"]`; no production branch can switch visible responses to Service; +- Service is background-only. With `SERVICE_API_BASE` empty, `clients["service"]` intentionally aliases the Brain client; configuring a dedicated Service endpoint changes only background execution; +- `USE_SERVICE_AS_BRAIN` survives only as a localized old-config migration input in `config_loader.py` plus launcher detection. Normalization promotes old Service settings to Brain, clears the dedicated Service URL, then deletes the legacy flag; +- archived `role=service` / `RUNTIME_MODE=SERVICE` handling and the logger's old `[SERVICE]` output-card presentation are historical reader compatibility only; there is no current writer/foreground route for that mode; +- L2/L3 remain removed architectural layers. Remaining production references are compatibility comments/log filters/storage migration residue, not active modules; +- the memory UI exposes exactly `FRAME`, `ACTIVE`, `DELAYED`, `L-T`, and `FILES`; the internal Facts Memory candidate buffer is not a sixth tab; +- Brain recent-message context is adjacent to `` and keeps the newest three pairs in full, with newline/XML normalization but no per-message character crop; +- ordinary Brain turns include the previous successful reasoning block with explicit middle-crop semantics, while follow-ups keep their dedicated reasoning context; +- browser continuity uses page-ephemeral `jin.liveRuntimeMemory.v2` plus one atomic `jin.sessionCheckpoint.v2`; legacy per-session FRAME selection is migration-only and never freshness-scanned; - Session CLEAR is a durable tombstone that blocks passive resurrection across already-open tabs until a new USER message is successfully sent; -- JIN color now round-trips through server context, raw runtime events, the common checkpoint, early local room restore, and one server reconciliation without a parallel color store; -- the full suite is still not certified green; the 2026-08-26 documentation environment also lacks required `httpx`/`fastapi` dependencies. +- `SAVE_SESSION` is not a current runtime-action contract; archived-session restore is handled by the bootstrap/restore path; +- tests still contain historical Brain-as-Service / `SAVE_SESSION` assumptions, but this audit intentionally skipped all test code and made no test changes. -New agents must not “repair” these contradictions by restoring the old architecture. +New agents must not “repair” compatibility residue by restoring the old topology. --- @@ -55,6 +52,13 @@ Present and active: - `utils/context/*` - `utils/session_restore.py` +Foreground/model-role invariants verified in production source: + +- `utils/brain_client_utils.py::get_brain_runtime_config()` returns only runtime id/label `brain`; +- `agent/nodes/brain.py::BrainNode.run()` resolves that label directly from `context.clients`; +- `clients/registry.py` aliases Service to Brain by default and replaces only the background Service client when `SERVICE_CONFIGURED` is true; +- `websocket/messages.py` gates user sends on Brain availability only; an absent dedicated Service runtime does not block foreground chat. + Not present: - `runtime/L2_memory.py` @@ -64,39 +68,44 @@ Not present: - `runtime/L3_memory_utils.py` - `runtime/L3_memory_rules.py` -Direct import check on the snapshot returns `ModuleNotFoundError` for the missing L2/L3 modules. +Filesystem/source audit confirms those L2/L3 modules are absent from the inspected archive. --- -## 3. L2/L3 legacy conflict +## 3. L2/L3 and old-role residual compatibility ### Product intent -L2 and L3 were removed as architectural layers on 2026-08-19/20. +L2 and L3 were removed as architectural layers. Brain is the only foreground model role. + +### Current production source -### Current source +The runtime package agrees: L2/L3 modules are absent and the visible Brain path does not branch to Service. -The runtime package agrees: the modules are gone. +### Production compatibility residue still present -### Legacy residue still present +- `runtime-storage.js` has one-time compatibility for checkpoints created before L3 removal; +- a few UI memory-log filters/comments still recognize historical L2/L3 labels; +- `config_loader.py` and `launch_jin.ps1` recognize `USE_SERVICE_AS_BRAIN` only to migrate old local configs; +- `utils/session_restore.py` / `ui/static/js/session-restore.js` can render archived `service` roles and `RUNTIME_MODE=SERVICE`; +- `ui/static/js/logger/log-entries.js` can present old `[SERVICE]` model-output cards, although current backend code has no `log_service_output` writer. -- root `README.md` still presents “The Four-Layer Memory Model”; -- root `ARCHITECTURE.md` still describes `runtime/L2_memory.py` and `runtime/L3_memory.py` as active; -- `tests/test_l2_memory.py` and `tests/test_l3_session_memory.py` still import the deleted modules; -- additional tests reference `runtime_l3_session_memory` and old session semantics; -- some JS comments/log filters mention L2/L3; -- `runtime-storage.js` explicitly contains one-time compatibility wording for checkpoints created before L3 removal. +These paths are localized compatibility readers/adapters. None changes current foreground routing. + +### Test residue + +Tests still contain historical assumptions around `USE_SERVICE_AS_BRAIN`, `CAN_SAVE_SESSION`, and old `` parsing. Per the 2026-08-29 audit scope, test code was not inspected for correctness, executed, or modified beyond identifying those references by search. ### Rule for agents -Do not restore deleted modules to satisfy stale tests/docs. Classify each remaining reference as: +Classify every old-role/L2/L3 reference as one of: 1. required backward compatibility; -2. harmless historical wording; -3. stale test/documentation that should eventually be removed or rewritten; +2. stale test/documentation; +3. harmless historical UI/log reader; 4. accidental live dependency. -Only category 4 is a runtime bug. +Only category 4 is a production runtime bug. Do not turn categories 1–3 back into live architecture. --- @@ -455,61 +464,31 @@ Avatar center and scene tint now share one transition duration variable set. Fir --- -## 14. Test status for this exact snapshot - -Command run: - -```text -python -m unittest discover -s tests -``` - -Historical result in the 2026-08-26 documentation environment: - -```text -Ran 431 tests in 3.973s -FAILED (failures=28, errors=68) -``` +## 14. Verification status for this exact snapshot -Important implications: +This 2026-08-29 pass was intentionally a **production-code + documentation audit only**. Per task scope: -- the repository is **not green** in this snapshot; -- this count is **not comparable** to the previous 2026-08-23 run: the current environment lacks project dependencies including `httpx` and `fastapi`, so many modules fail during import before their tests can be collected; -- accessible tests still include unrelated client-contract failures, so dependency errors are not the only reason the command is red; -- a dependency-light targeted run of `test_previous_chat_messages_context`, `test_bootstrap_color_action_regression`, `test_jin_color_transition_client_contract`, and `test_session_bootstrap_lineage_client_contract` runs 18 tests and passes all 18; -- `test_brain_asset_flow`, including the ordinary-turn previous-reasoning regression, cannot import in this environment because `runtime/stream.py` requires the missing `httpx` dependency; -- bootstrap/latest-session/chat-tail/live-checkpoint suites that import `websocket` or L1 client modules cannot be executed in this environment until the missing dependencies are installed; -- stale L2/L3 and old `SAVE_SESSION` tests remain repository debt even though this particular environment reaches dependency failures first; -- do not use “full suite currently fails” as permission to ignore targeted regressions; -- for any patch, run the smallest relevant target set and report exact pass/fail counts. +- no unit tests were run; +- no browser/client-contract tests were run; +- no model probes were run; +- no files under `tests/` were edited. -For the 2026-08-28 browser-storage-v2 increment, the dependency-light targeted storage/bootstrap/anonymous/color/cleanup run executes 26 tests and passes all 26. It includes an executable Node storage round trip covering copied-live clearing, exact-owner migration, orphan rejection, migration write failure, multi-tab CLEAR protection, post-clear USER recovery, and anonymous isolation. +The audit used source tracing and targeted repository searches to verify the foreground model route and classify legacy compatibility. Therefore this document makes **no claim that the test suite is green**. Historical pass/fail counts from older snapshots are not evidence for this archive. -Do not resurrect L2/L3 merely to make stale tests import again. The test suite itself needs a deliberate cleanup/migration task. +For future code changes, run the smallest relevant checks unless the task explicitly excludes tests. Do not resurrect L2/L3, foreground Service routing, or `SAVE_SESSION` merely to satisfy stale tests. --- -## 15. Old documentation status - -### Root `README.md` - -Currently stale in major architecture sections: - -- still describes L1/L2/L3/L4 as four active layers; -- still describes L2 pattern updates and L3 session digest; -- still describes old post-turn L1/L2 behavior; -- lists current features mixed with removed architecture. - -### Root `ARCHITECTURE.md` - -Also stale: +## 15. Documentation status -- describes `runtime/L2_memory.py` and `runtime/L3_memory.py` as active files; -- describes Brain context as including live L2/L3 state; -- describes `SAVE_SESSION`/L3 flow as current. +As of this snapshot, the documentation set has been synchronized with the production architecture: -### Rule +- root `README.md` describes FRAME/L4/Active/Delayed/Files instead of a live four-layer L1/L2/L3/L4 model; +- README model-role/setup/configuration text describes Brain as the only foreground route and Service as optional/dedicated background execution with Brain fallback; +- `AGENTS.md` records the same routing invariant and explicitly classifies old `USE_SERVICE_AS_BRAIN` / archived Service labels as compatibility; +- `docs/JIN_ARCHITECTURE.md`, `docs/JIN_DECISIONS.md`, and this file use the 2026-08-29 Brain-first topology as the baseline. -Use `docs/JIN_ARCHITECTURE.md` + `docs/JIN_DECISIONS.md` + this file as the current architecture baseline. Updating/removing old docs should be a separate explicit cleanup diff so product changes and documentation cleanup are reviewable. +There is no root `ARCHITECTURE.md` in the inspected archive. `docs/JIN_ARCHITECTURE.md` is the canonical architecture document. --- @@ -534,27 +513,26 @@ Rule for future agents: assume pre-existing files/changes are user-owned; do not Do not present these as settled without fresh code evidence: - whether the full night self-review concept is implemented outside the inspected paths; -- which remaining L2/L3-named compatibility fields are still required for real historical data; -- whether all old `SAVE_SESSION` tests should be deleted or rewritten around checkpoints; +- which remaining L2/L3-named compatibility fields/readers are still required for real historical data; +- when stale tests that directly mutate `config.USE_SERVICE_AS_BRAIN` or expect `SAVE_SESSION` should be migrated to the Brain-first/checkpoint architecture; - final intended internal storage format for Active Memory after the flat-JSON boundary migration; - whether reveal debounce should now be restored from 333 ms to the earlier 250 ms preference; - final canonical list of supported noncanonical action marker aliases after compatibility cleanup; -- which current full-suite failures are regressions versus intentionally stale tests; +- which stale tests are still intentional compatibility coverage versus obsolete pre-Brain-first/pre-checkpoint expectations; - whether every linked-highlight/pin/pause/delete path is currently synchronized between panel, prompt-loaded state, and avatar. When one of these becomes the task, investigate first and record the resolved decision in `JIN_DECISIONS.md`. --- -## 19. Recommended near-term documentation cleanup order +## 19. Recommended near-term cleanup order -When the owner explicitly asks for cleanup: +When the owner explicitly asks for legacy cleanup: -1. migrate/delete stale L2/L3 tests so the suite represents the post-L2/L3 architecture; -2. rewrite root `ARCHITECTURE.md` to point to or mirror `docs/JIN_ARCHITECTURE.md`; -3. update the README memory/session sections; -4. remove misleading old `SAVE_SESSION` feature claims while preserving archive compatibility code; -5. audit old comments/UI logger filters for L2/L3 wording; -6. only then remove compatibility fields that are proven unused by real archived data. +1. migrate stale tests away from direct `USE_SERVICE_AS_BRAIN` and old `SAVE_SESSION` assumptions; +2. audit the historical `RUNTIME_MODE=SERVICE` / archived `role=service` readers against real old archives before deleting them; +3. remove the logger's old `[SERVICE]` model-output presentation only after archive/log compatibility is proven unnecessary; +4. audit L2/L3-named UI log filters/comments and pre-L3 storage migration paths against real persisted data; +5. only then delete compatibility adapters/readers that are proven unused. -Do not combine this cleanup with unrelated runtime behavior patches. +Do not combine that cleanup with unrelated runtime behavior patches. diff --git a/docs/JIN_DECISIONS.md b/docs/JIN_DECISIONS.md index fb26603f..d133100c 100644 --- a/docs/JIN_DECISIONS.md +++ b/docs/JIN_DECISIONS.md @@ -1,6 +1,6 @@ # JIN Core Engine — Durable Decisions -**Decision baseline:** reconciled on 2026-08-26 against `jin_core(20260826-090339).zip` and the accumulated project context. +**Decision baseline:** reconciled on 2026-08-29 against `jin_core(20260829-114751).zip` and the accumulated project context. The 2026-08-29 reconciliation intentionally skipped tests and traced production source only. This file records product/architecture intent that should survive refactors. It is not a changelog and not a dump of historical experiments. @@ -17,7 +17,7 @@ Status vocabulary: **Status:** Accepted / implemented -JIN Core Engine is a model-agnostic cognitive runtime. BRAIN/SERVICE model choices are configuration, not architecture. +JIN Core Engine is a model-agnostic cognitive runtime. The foreground BRAIN model and optional dedicated background SERVICE model are configuration choices, not product architecture. **Why:** continuity, visible memory, actions, restore, UI semantics, and runtime state must survive model replacement. @@ -551,3 +551,26 @@ Explicit archived restore keeps dialogue, reasoning, and FRAME under server-arch **Why:** one atomic owner eliminates torn SAVE pairs, stale-key resurrection, ambiguous newest-record selection, and hidden bootstrap sources while preserving soft reconnect. **Rejected alternatives:** split saved-runtime/saved-session keys; durable per-session FRAME keys; newest-timestamp scans; physical deletion without a multi-tab tombstone; automatic `saved_runtime.txt` fallback. + +--- + +## D040 — Brain is the only foreground model route + +**Status:** Accepted / implemented with localized legacy readers + +Foreground user work always follows: + +```text +user -> AgentRuntime -> BrainNode -> clients["brain"] +``` + +SERVICE is a logical background role only. `clients/registry.py` aliases `clients["service"]` to the Brain client by default; an explicitly configured `SERVICE_API_BASE` replaces only that background client with a dedicated runtime. Foreground routing does not depend on Service availability or configuration. + +`USE_SERVICE_AS_BRAIN` is not a current runtime option. It is accepted only as old-config migration input: the loader promotes the legacy Service endpoint/settings into canonical Brain settings, clears the dedicated Service URL, removes the flag, and exposes normalized Brain-first configuration to the rest of the process. Launcher detection exists only to preserve this migration during startup. + +Archived `service` message roles, `RUNTIME_MODE=SERVICE`, and old `[SERVICE]` model-output logger cards may remain as reader compatibility until real historical data no longer needs them. They must not be used to reintroduce foreground Service execution. + +**Why:** the visible model path needs one canonical owner. A one-model installation should be possible without role inversion, while a second machine/model can still accelerate background memory/research work. + +**Rejected alternatives:** switching visible replies to Service; keeping `USE_SERVICE_AS_BRAIN` as a live runtime branch; requiring a dedicated Service endpoint; treating archived Service labels as evidence of current topology. + diff --git a/launch_jin.ps1 b/launch_jin.ps1 index c4b0f19e..37cc0194 100644 --- a/launch_jin.ps1 +++ b/launch_jin.ps1 @@ -244,6 +244,20 @@ function Test-AutoProviderBaseValue { ) } +function Test-LegacyServiceAsBrainEnabled { + param([string]$ConfigPath) + + $value = Get-PythonConfigValue -Path $ConfigPath -Name "USE_SERVICE_AS_BRAIN" + + if ($null -eq $value) { + return $false + } + + $normalizedValue = $value.Trim().ToLowerInvariant() + + return @("1", "true", "yes", "on") -contains $normalizedValue +} + function Ensure-JinConfig { $configPath = Join-Path $Root "config.py" $examplePath = Join-Path $Root "config.example.py" @@ -266,10 +280,11 @@ function Get-ConfiguredBaseUrlCandidates { param([string]$ConfigPath) $baseUrls = New-Object System.Collections.Generic.List[string] - $baseNames = @( - "SERVICE_API_BASE", - "BRAIN_API_BASE" - ) + $baseNames = @("BRAIN_API_BASE") + + if (Test-LegacyServiceAsBrainEnabled -ConfigPath $ConfigPath) { + $baseNames = @("SERVICE_API_BASE") + } foreach ($name in $baseNames) { $value = Get-PythonConfigValue -Path $ConfigPath -Name $name @@ -285,7 +300,10 @@ function Get-ConfiguredBaseUrlCandidates { } function Get-LmStudioModels { - param([string[]]$BaseUrls) + param( + [string[]]$BaseUrls, + [switch]$Optional + ) $checkedUrls = New-Object System.Collections.Generic.List[string] @@ -320,6 +338,12 @@ function Get-LmStudioModels { $checkedUrls -join "`r`n" ) + if ($Optional) { + Write-Host "Optional LM Studio service is unavailable. Checked endpoints:" + Write-Host $checkedText + return $null + } + Fail-WithMessage "LM Studio is not running.`r`nOpen LM Studio, start Local Server, then run this script again.`r`nChecked endpoints:`r`n$checkedText" } @@ -381,10 +405,44 @@ function Write-JinConfig { } Update-ProviderBaseConfig -ConfigPath $configPath -Name "BRAIN_API_BASE" -ActiveBaseUrl $ActiveBaseUrl - Update-ProviderBaseConfig -ConfigPath $configPath -Name "SERVICE_API_BASE" -ActiveBaseUrl $ActiveBaseUrl Update-ModelConfig -ConfigPath $configPath -Name "BRAIN_MODEL_UID" -SuggestedModel $suggestedModel - Update-ModelConfig -ConfigPath $configPath -Name "SERVICE_MODEL_UID" -SuggestedModel $suggestedModel +} + +function Initialize-DedicatedServiceConfig { + param([string]$ConfigPath) + + if (Test-LegacyServiceAsBrainEnabled -ConfigPath $ConfigPath) { + Write-Host "Legacy Service-as-Brain config detected. It will migrate to Brain at startup." + return + } + + $serviceBaseUrl = Get-PythonConfigValue -Path $ConfigPath -Name "SERVICE_API_BASE" + + if (Test-AutoProviderBaseValue -Name "SERVICE_API_BASE" -Value $serviceBaseUrl) { + Write-Host "SERVICE_API_BASE is empty. Service requests will use Brain." + return + } + + Write-Host "Dedicated Service URL configured: $serviceBaseUrl" + $serviceRuntime = Get-LmStudioModels -BaseUrls @($serviceBaseUrl) -Optional + + if ($null -eq $serviceRuntime) { + Write-Host "Keeping the dedicated Service config; JIN will start with its Service status offline." + return + } + + $serviceModelIds = @($serviceRuntime.ModelIds) + $suggestedServiceModel = Find-GemmaModel -ModelIds $serviceModelIds + + if (-not $suggestedServiceModel) { + Write-Host "No supported Gemma model found on the dedicated Service runtime." + Write-Host "Keeping SERVICE_MODEL_UID unchanged." + return + } + + Write-Host "Found supported Service model: $suggestedServiceModel" + Update-ModelConfig -ConfigPath $ConfigPath -Name "SERVICE_MODEL_UID" -SuggestedModel $suggestedServiceModel } function Test-PythonCommand { @@ -467,6 +525,7 @@ try { Write-Host "" Write-Host "Checking local config model IDs..." Write-JinConfig -ConfigPath $configPath -ActiveBaseUrl $LmStudioBaseUrl -ModelIds $modelIds + Initialize-DedicatedServiceConfig -ConfigPath $configPath $venvPath = Join-Path $Root ".venv" $venvPython = Join-Path $venvPath "Scripts\python.exe" diff --git a/rules/__init__.py b/rules/__init__.py index 21727ea3..30bd4428 100644 --- a/rules/__init__.py +++ b/rules/__init__.py @@ -13,7 +13,6 @@ "IDENTITY", "LOOP_RULES", "REASONING_REVIEW_RULE", - "SERVICE_AS_BRAIN_RUNTIME_ACTIONS", "build_brain_context", ] @@ -21,18 +20,15 @@ def __getattr__(name): if name in { "BRAIN_RUNTIME_ACTIONS", - "SERVICE_AS_BRAIN_RUNTIME_ACTIONS", "build_brain_context", }: from .brain_context_builder import ( BRAIN_RUNTIME_ACTIONS, - SERVICE_AS_BRAIN_RUNTIME_ACTIONS, build_brain_context, ) exports = { "BRAIN_RUNTIME_ACTIONS": BRAIN_RUNTIME_ACTIONS, - "SERVICE_AS_BRAIN_RUNTIME_ACTIONS": SERVICE_AS_BRAIN_RUNTIME_ACTIONS, "build_brain_context": build_brain_context, } diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index d9d22c25..09f9b291 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -26,21 +26,6 @@ "CAN_WEB_SEARCH", ) -SERVICE_AS_BRAIN_RUNTIME_ACTIONS = { - "CAN_DEEP_WEB_SEARCH": True, - "CAN_WEB_SEARCH": True, - "CAN_USE_ASSETS": True, - "CAN_SAVE_DELAYED_MEMORY": True, - "CAN_SAVE_ACTIVE_MEMORY": True, - "CAN_RUNTIME_TODO": False, - "CAN_CLEAN_TOOL_RESULTS": True, - "CAN_JIN_COLOR": True, - "CAN_JIN_SIZE": True, - "CAN_JIN_POSITION": True, - "CAN_JIN_SPEED": True, - "CAN_UPDATE_L4_FACTS": True, -} - BRAIN_RUNTIME_ACTIONS = { "CAN_DEEP_WEB_SEARCH": True, "CAN_WEB_SEARCH": True, diff --git a/runtime/L1_memory.py b/runtime/L1_memory.py index 94af4e09..7e000503 100644 --- a/runtime/L1_memory.py +++ b/runtime/L1_memory.py @@ -34,7 +34,7 @@ log_runtime_summarizer_payload, log_runtime_summarizer_stream_event, looks_like_incomplete_runtime_memory, - refresh_runtime_memory_summarizer_usage, + refresh_service_runtime_usage, runtime_prompt_is_context_overloaded, ) from runtime.L1_memory_utils import ( @@ -253,11 +253,13 @@ async def ask_l1_summarizer( if not stream_enabled: return await ask_service_model( client=service_client, + context=context, system_prompt=system_prompt, user_prompt=user_prompt, temperature=temperature, max_tokens=max_tokens, timeout=config.SERVICE_REQUEST_TIMEOUT, + track_usage=False, ) reasoning_parts = [] @@ -463,7 +465,7 @@ async def ask_runtime_memory_model( last_turn_context_overloaded=True, ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, @@ -485,7 +487,7 @@ async def ask_runtime_memory_model( max_tokens=max_tokens, ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, @@ -529,7 +531,7 @@ async def ask_runtime_memory_batch_model( ), ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, @@ -555,7 +557,7 @@ async def ask_runtime_memory_batch_model( max_tokens=max_tokens, ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 468b1458..5b426355 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -67,7 +67,7 @@ log_memory_event, log_runtime_summarizer_payload, log_runtime_summarizer_result, - refresh_runtime_memory_summarizer_usage, + refresh_service_runtime_usage, safe_call, ) @@ -1894,7 +1894,7 @@ async def ask_l4_model( or 1 ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, @@ -1914,11 +1914,13 @@ async def ask_l4_model( response = await ask_service_model( client=service_client, + context=context, system_prompt=system_prompt, user_prompt=user_prompt, temperature=getattr(config, "SERVICE_TEMPERATURE", 0.1), max_tokens=effective_max_tokens, timeout=getattr(config, "SERVICE_REQUEST_TIMEOUT", 1000.0), + track_usage=False, ) if isinstance(response, dict): response["_jin_l4_request_meta"] = { @@ -1940,7 +1942,7 @@ async def ask_l4_model( label=label, result=response_text, ) - await refresh_runtime_memory_summarizer_usage( + await refresh_service_runtime_usage( context, system_prompt=system_prompt, user_prompt=user_prompt, diff --git a/runtime/deep_web_search.py b/runtime/deep_web_search.py index 17c4c746..4cb11917 100644 --- a/runtime/deep_web_search.py +++ b/runtime/deep_web_search.py @@ -436,6 +436,7 @@ async def _call_worker( response = await ask_service_model( client=service_client, + context=context, system_prompt=DEEP_WEB_SEARCH_WORKER_SYSTEM_PROMPT, user_prompt=build_deep_search_current_sequence(pool, worker), temperature=float(getattr(config, "SERVICE_TEMPERATURE", 0.1) or 0.1), diff --git a/runtime/fact_check.py b/runtime/fact_check.py index 73dbfc17..2c9f72fc 100644 --- a/runtime/fact_check.py +++ b/runtime/fact_check.py @@ -988,6 +988,7 @@ async def ask_fact_check_plan( response = await ask_service_model( client=service_client, + context=context, system_prompt=build_fact_check_plan_system_prompt(), user_prompt=build_fact_check_plan_user_prompt( candidate=candidate, @@ -1326,6 +1327,7 @@ async def ask_fact_check_decision( response = await ask_service_model( client=service_client, + context=context, system_prompt=build_fact_check_judge_system_prompt(), user_prompt=build_fact_check_judge_user_prompt( candidate=candidate, diff --git a/runtime/memory_common.py b/runtime/memory_common.py index a9a0e87b..331e3172 100644 --- a/runtime/memory_common.py +++ b/runtime/memory_common.py @@ -24,9 +24,7 @@ from runtime.registry import ( runtime_state, ) -from runtime.state import ( - RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID, -) +from runtime.state import SERVICE_RUNTIME_ID from runtime.state_sync import ( refresh_runtime_state, ) @@ -147,7 +145,7 @@ def build_runtime_summarizer_trusted_context( ) or "" ).strip(), - service_model_uid=settings.SERVICE_MODEL_UID, + current_model_uid=settings.SERVICE_MODEL_UID, timestamp=str(timestamp), current_date=str( current_date @@ -544,7 +542,7 @@ def looks_like_incomplete_runtime_memory( ) -async def refresh_runtime_memory_summarizer_usage( +async def refresh_service_runtime_usage( context, *, system_prompt: str, @@ -612,9 +610,7 @@ async def refresh_runtime_memory_summarizer_usage( await refresh_runtime_state( context, - runtime_id=( - RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID - ), + runtime_id=SERVICE_RUNTIME_ID, used_tokens=( total_tokens or context_tokens diff --git a/runtime/state.py b/runtime/state.py index a13c1857..9e4db1ed 100644 --- a/runtime/state.py +++ b/runtime/state.py @@ -2,10 +2,8 @@ UNCHANGED = object() -RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID = ( - f"{settings.SERVICE_MODEL_UID}:runtime-memory" -) -RUNTIME_MEMORY_SUMMARIZER_LABEL = "summarizer" +BRAIN_RUNTIME_ID = "brain" +SERVICE_RUNTIME_ID = "service" class RuntimeState: @@ -14,29 +12,22 @@ def __init__(self): self.states = {} - runtimes = [ + runtimes = ( ( - settings.SERVICE_MODEL_UID, - "service", - settings.SERVICE_CONTEXT_WINDOW, + BRAIN_RUNTIME_ID, + "brain", + settings.BRAIN_MODEL_UID, + settings.BRAIN_CONTEXT_WINDOW, ), ( - RUNTIME_MEMORY_SUMMARIZER_RUNTIME_ID, - RUNTIME_MEMORY_SUMMARIZER_LABEL, + SERVICE_RUNTIME_ID, + "service", + settings.SERVICE_MODEL_UID, settings.SERVICE_CONTEXT_WINDOW, ), - ] - - if not settings.USE_SERVICE_AS_BRAIN: - runtimes.append( - ( - settings.BRAIN_MODEL_UID, - "brain", - settings.BRAIN_CONTEXT_WINDOW, - ) - ) + ) - for runtime_id, label, max_tokens in runtimes: + for runtime_id, label, model, max_tokens in runtimes: if runtime_id in self.states: continue @@ -44,7 +35,7 @@ def __init__(self): self.states[runtime_id] = { "id": runtime_id, "label": label, - "model": runtime_id, + "model": model, "used_tokens": 0, "context_tokens": 0, "total_tokens": 0, @@ -56,6 +47,7 @@ def __init__(self): def update_runtime_state( self, runtime_id: str, + model: str | None = None, used_tokens: int | None = None, context_tokens: int | None = None, total_tokens: int | None = None, @@ -67,6 +59,9 @@ def update_runtime_state( runtime_state = self.states[runtime_id] + if model is not None: + runtime_state["model"] = model + if used_tokens is not None: runtime_state["used_tokens"] = used_tokens if context_tokens is None: diff --git a/tests/test_dom_lifecycle_client_contract.py b/tests/test_dom_lifecycle_client_contract.py new file mode 100644 index 00000000..ea42a6dc --- /dev/null +++ b/tests/test_dom_lifecycle_client_contract.py @@ -0,0 +1,67 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +MEMORY_VIEW_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" +TRACE_MODAL_JS = ROOT / "ui" / "static" / "js" / "logger" / "trace-modal.js" + + +class DomLifecycleClientContractTests(unittest.TestCase): + + def test_memory_tab_switch_releases_previous_rows(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "function releaseRuntimeMemoryDynamicDom(options = {})", + source, + ) + self.assertIn( + "releaseRuntimeMemoryDynamicDom({\n renderOnResume: false,", + source, + ) + self.assertIn( + "runtimeMemoryText.replaceChildren();", + source, + ) + + def test_avatar_mode_releases_dynamic_memory_dom_after_detach(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + + self.assertIn( + "if (!isRuntimeMemoryViewDomConnected())", + source, + ) + self.assertIn( + "releaseRuntimeMemoryDynamicDom({\n renderOnResume: true,", + source, + ) + self.assertIn( + "pendingRuntimeMemoryRender = true;", + source, + ) + + def test_hidden_modals_release_heavy_payload_dom(self): + memory_source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + trace_source = TRACE_MODAL_JS.read_text(encoding="utf-8") + + self.assertIn( + "delayedMemoryModalContent.replaceChildren();", + memory_source, + ) + self.assertIn( + "closeActiveDelayedMemoryAttachmentPicker();", + memory_source, + ) + self.assertIn( + "traceModalContent.replaceChildren();", + trace_source, + ) + self.assertIn( + 'traceModalContextCopyText = "";', + trace_source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 36cdf6ba..876f1746 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -217,7 +217,7 @@ color: rgba(153, 246, 228, 0.95); /* Типографика букв SV/US. */ - font-size: 10px; + font-size: 14px; line-height: 1; letter-spacing: 0.04em; @@ -770,12 +770,13 @@ body:not(.theme-win95) .jin-think-wrapper:has(> .jin-think-content.is-collapsed) } .jin-chat-bubble-service { - /* Финальный JIN-фон делаем ровнее: убираем круглые радиальные пятна, - из-за которых на широком/растущем бабле появлялась светлая половина. */ - background: - linear-gradient(180deg, rgba(204, 251, 241, 0.026), rgba(10, 18, 22, 0.018)), - linear-gradient(90deg, rgba(45, 212, 191, 0.014), rgba(103, 232, 249, 0.008)), - rgba(10, 18, 22, 0.58); + /* Финальный JIN-фон делаем полностью ровным: без затемняющих градиентов + поверх текста, чтобы бабл читался одним чистым слоем. */ + background: color-mix( + in srgb, + rgba(10, 18, 22, 0.58) calc(var(--jin-bubble-invert-off) * 100%), + rgba(246, 249, 247, 0.90) calc(var(--JIN_BUBBLE_INVERT) * 100%) + ); /* Финальный цвет текста JIN: чуть спокойнее, но без потери читабельности. */ color: rgba(220, 235, 232, 0.66); @@ -814,6 +815,11 @@ body:not(.theme-win95) .jin-think-wrapper:has(> .jin-think-content.is-collapsed) } .jin-chat-bubble-brain { + background: color-mix( + in srgb, + rgba(10, 18, 22, 0.58) calc(var(--jin-bubble-invert-off) * 100%), + rgba(246, 249, 247, 0.90) calc(var(--JIN_BUBBLE_INVERT) * 100%) + ); border-color: color-mix( in srgb, rgba(165, 180, 252, 0.20) calc(var(--jin-bubble-invert-off) * 100%), diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index 3e76183e..45c246b2 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -310,32 +310,6 @@ body.theme-win95 #memory-panel .memory-scroll > .grid:first-child { padding: 0 2px; } -body.theme-win95 #memory-panel [data-context-tab] { - height: 32px; - background: var(--win95-face); - border: 1px solid var(--win95-shadow); - border-radius: 1px; - color: var(--win95-text); - font-family: Calibri, Arial, sans-serif; - font-size: 15px; - font-weight: 500; - letter-spacing: 0; - text-transform: uppercase; - text-shadow: none; - box-shadow: - inset 1px 1px 0 var(--win95-light), - inset -1px -1px 0 #8a8a8a; -} - -body.theme-win95 #memory-panel [data-context-tab][aria-selected="true"] { - background: var(--win95-face-dark); - border-color: var(--win95-shadow); - box-shadow: - inset 1px 1px 0 #8a8a8a, - inset -1px -1px 0 #d6d6d6; - transform: none; -} - body.theme-win95 #context-runtime-panel, body.theme-win95 #runtime-memory-panel { background: var(--win95-face); @@ -353,7 +327,6 @@ body.theme-win95 #context-runtime-panel .space-y-3 > :not([hidden]) ~ :not([hidd margin-top: 0 !important; } -body.theme-win95 #context-panel-model, body.theme-win95 #runtime-memory-text, body.theme-win95 #context-summary-tokens, body.theme-win95 .runtime-memory-header, @@ -371,13 +344,6 @@ body.theme-win95 #memory-panel #memory-layers-toggle { text-shadow: 1px 1px 0 var(--win95-blue-shadow); } -body.theme-win95 #context-panel-model { - font-size: 14px; - font-weight: 500; - padding-bottom: 8px; - border-bottom: 1px solid var(--win95-mid); -} - body.theme-win95 .runtime-memory-header { border-bottom: 0; font-size: 14px; @@ -506,24 +472,24 @@ body.theme-win95 #context-summary-max { white-space: nowrap; } -body.theme-win95 #context-window-line, -body.theme-win95 #summarizer-window-line { +body.theme-win95 #brain-context-window-line, +body.theme-win95 #service-context-window-line { align-items: center; gap: 10px; color: var(--win95-text) !important; font-size: 12px; } -body.theme-win95 #context-window-percent, -body.theme-win95 #summarizer-window-percent { +body.theme-win95 #brain-context-window-percent, +body.theme-win95 #service-context-window-percent { color: var(--win95-text) !important; width: 4ch; font-family: Calibri, Arial, sans-serif; font-size: 14px; } -body.theme-win95 #context-window-bar, -body.theme-win95 #summarizer-window-bar { +body.theme-win95 #brain-context-window-bar, +body.theme-win95 #service-context-window-bar { position: relative; height: 15px; min-width: 0; @@ -538,13 +504,13 @@ body.theme-win95 #summarizer-window-bar { box-shadow: none; } -body.theme-win95 #context-window-bar *, -body.theme-win95 #summarizer-window-bar * { +body.theme-win95 #brain-context-window-bar *, +body.theme-win95 #service-context-window-bar * { color: transparent !important; } -body.theme-win95 #context-window-bar::before, -body.theme-win95 #summarizer-window-bar::before { +body.theme-win95 #brain-context-window-bar::before, +body.theme-win95 #service-context-window-bar::before { content: ""; position: absolute; top: 2px; @@ -562,8 +528,8 @@ body.theme-win95 #summarizer-window-bar::before { ); } -body.theme-win95 #context-window-bar::after, -body.theme-win95 #summarizer-window-bar::after { +body.theme-win95 #brain-context-window-bar::after, +body.theme-win95 #service-context-window-bar::after { content: ""; position: absolute; top: 2px; diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index f1a70d75..d8987bbc 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -1241,7 +1241,7 @@ function getRoleConfig(role) { case "user": return { - avatar: "US", + avatar: "U", bubbleClass: "jin-chat-bubble jin-chat-bubble-user", avatarClass: @@ -1260,7 +1260,7 @@ function getRoleConfig(role) { case "brain": default: return { - avatar: "BR", + avatar: "J", bubbleClass: "jin-chat-bubble jin-chat-bubble-brain jin-chat-bubble-rateable", avatarClass: diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index 26186778..66b599f3 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -201,6 +201,33 @@ function ensureTraceModal() { traceModalL1StreamFrame = null; } + + // Keep only the tiny reusable modal shell. Context snapshots can be + // thousands of DOM nodes; once hidden they must not stay in live DOM. + if (traceModalContent) { + traceModalContent.replaceChildren(); + } + traceModalContextCopyText = ""; + + if (traceModalCopyButton) { + traceModalCopyButton.classList.add( + "hidden" + ); + traceModalCopyButton.classList.remove( + "is-copied" + ); + } + + if (traceModalReason) { + traceModalReason.textContent = ""; + traceModalReason.classList.add( + "hidden" + ); + } + + traceModal.classList.remove( + "jin-context-trace-modal" + ); } async function copyTraceModalContext() { diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 627b6831..b6784c2d 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -833,8 +833,50 @@ ); } + function releaseRuntimeMemoryDynamicDom(options = {}) { + clearRuntimeMemoryLineAvatarHover(); + clearDelayedMemoryAvatarHover(); + hideLongTermMemoryHoverCard(); + + if (runtimeMemoryText) { + runtimeMemoryText.replaceChildren(); + runtimeMemoryText.classList.remove( + "runtime-memory-text-pinned" + ); + runtimeMemoryText.removeAttribute( + "title" + ); + } + + userIdleValueNode = null; + + if (idle) { + idle.stop(); + } + + resetRuntimeMemoryLazyRows({ + keepMode: true, + }); + + memoryHighlightsSuspended = true; + + if (options.renderOnResume !== false) { + pendingRuntimeMemoryRender = true; + } + } + function handleRuntimeMemoryPanelVisibilityChange() { if (isRuntimeMemoryViewSuspended()) { + // Keep the collapse animation intact while the scroll body is still + // mounted. Once logger.js detaches it, drop the dynamic rows too so + // avatar mode does not retain a hidden memory tree through JS refs. + if (!isRuntimeMemoryViewDomConnected()) { + releaseRuntimeMemoryDynamicDom({ + renderOnResume: true, + }); + return; + } + resetRuntimeMemoryLazyRows({ keepMode: true, }); @@ -6452,6 +6494,7 @@ } closeActiveDelayedMemoryFactPicker(); + closeActiveDelayedMemoryAttachmentPicker(); if (options.save !== false) { commitDelayedMemoryModalEdits({ @@ -6476,6 +6519,16 @@ delayedMemoryModal.classList.remove( "flex" ); + + // The report body can contain fact/file pickers and many rows. Do not + // keep that subtree alive merely because the modal shell is hidden. + if (delayedMemoryModalContent) { + delayedMemoryModalContent.replaceChildren(); + } + if (delayedMemoryModalTitle) { + delayedMemoryModalTitle.textContent = ""; + } + delayedMemoryModalReport = null; delayedMemoryModalTitleEditor = null; delayedMemoryModalDetailsTitle = null; @@ -8706,6 +8759,11 @@ return; } + // Destroy the previous tab rows before switching modes. The next + // render is synchronous, so no inactive tab DOM is retained. + releaseRuntimeMemoryDynamicDom({ + renderOnResume: false, + }); setRuntimeMemoryDisplayMode(displayMode); renderRuntimeMemorySnapshot({ availableModes: getAvailableRuntimeMemoryDisplayModes(), diff --git a/ui/static/js/runtime/runtime-panel.js b/ui/static/js/runtime/runtime-panel.js index 4b020716..63996fb3 100644 --- a/ui/static/js/runtime/runtime-panel.js +++ b/ui/static/js/runtime/runtime-panel.js @@ -43,14 +43,10 @@ let telemetryFrameScheduled = false; let contextPanelRenderTimer = null; - let contextTabButtons = {}; let contextRuntimePanel = null; let contextPanelResizeObserver = null; const runtimePanelState = { - activeTab: "service", - useServiceAsBrain: false, - runtimeStatus: {}, fallbackRuntimes: {}, liveRuntimes: [], }; @@ -149,143 +145,14 @@ } - function runtimeHasUsage(runtime) { - - return getRuntimeUsageAmount( - runtime - ) > 0; - - } - - - function buildServiceRuntimeUsageFallback( - serviceRuntime, - summarizerRuntime - ) { - - if ( - !summarizerRuntime - || runtimeHasUsage(serviceRuntime) - || !runtimeHasUsage(summarizerRuntime) - ) { - return serviceRuntime; - } - - return { - ...summarizerRuntime, - label: "service", - model: ( - serviceRuntime - && serviceRuntime.model - ) - ? serviceRuntime.model - : summarizerRuntime.model, - status: ( - serviceRuntime - && serviceRuntime.status - ) - ? serviceRuntime.status - : summarizerRuntime.status, - }; - - } - - function getServiceRuntime() { - - return buildServiceRuntimeUsageFallback( - getRuntimeByLabel("service"), - getSummarizerRuntime() - ); + return getRuntimeByLabel("service"); } function getBrainRuntime() { - - return ( - getRuntimeByLabel("brain") - || ( - runtimePanelState.useServiceAsBrain - ? getRuntimeByLabel("service") - : null - ) - ); - - } - - - function getSummarizerRuntime() { - - return getRuntimeByLabel( - "summarizer" - ); - - } - - - function getSelectedRuntime() { - - if (runtimePanelState.activeTab === "brain") { - return getBrainRuntime(); - } - - return getServiceRuntime(); - - } - - - function hasRuntimeStatus(role) { - - return typeof ( - runtimePanelState.runtimeStatus[role] - ) === "boolean"; - - } - - - function isRuntimeOnline(role) { - - if (!hasRuntimeStatus(role)) { - return false; - } - - return Boolean( - runtimePanelState.runtimeStatus[role] - ); - - } - - - function isContextTabDisabled(role) { - - return !isRuntimeOnline(role); - - } - - - function formatContextTokens(runtime) { - - const runtimeInfo = - runtime; - - if (!runtimeInfo) { - return { - used: 0, - max: 0, - }; - } - - return { - // The headline is live occupancy, not prompt-only context. - // `used_tokens` / `total_tokens` continue growing while reasoning - // streams, whereas `context_tokens` is the fixed prompt baseline. - used: getRuntimeUsageAmount( - runtimeInfo - ), - max: runtimeInfo.max_tokens || 0, - }; - + return getRuntimeByLabel("brain"); } @@ -956,258 +823,156 @@ } - function setTabClasses(role) { - - const button = - contextTabButtons[role]; - - if (!button) { - return; - } - - const isActive = - runtimePanelState.activeTab === role; - - const isDisabled = - isContextTabDisabled(role); - - button.disabled = - isDisabled; - - button.setAttribute( - "aria-selected", - String(isActive) - ); - - button.setAttribute( - "aria-disabled", - String(isDisabled) - ); - - if (isDisabled) { - const borderClass = - role === "service" - ? "border-r border-slate-500/70 " - : ""; - - button.className = - "h-8 " - + borderClass - + "text-[11px] font-bold uppercase tracking-widest text-slate-500 cursor-not-allowed"; - - return; - } - - if (isActive && role === "service") { - button.className = - "h-8 border-r border-slate-500/70 bg-slate-600/70 text-[11px] font-bold uppercase tracking-widest text-zinc-50 transition"; - - return; - } - - if (isActive) { - button.className = - "h-8 bg-slate-600/70 text-[11px] font-bold uppercase tracking-widest text-zinc-50 transition"; - - return; - } - - if (role === "service") { - button.className = - "h-8 border-r border-slate-500/70 text-[11px] font-bold uppercase tracking-widest text-slate-300 transition hover:bg-slate-600/50 hover:text-zinc-50"; - - return; - } - - button.className = - "h-8 text-[11px] font-bold uppercase tracking-widest text-slate-300 transition hover:bg-slate-600/50 hover:text-zinc-50"; - - } - - - function setContextPanelRuntime(runtime) { + function setContextPanelRuntimes( + brainRuntime, + serviceRuntime + ) { if (contextRuntimePanel) { contextRuntimePanel.classList.toggle( "hidden", - !runtime + !brainRuntime && !serviceRuntime ); } - const titleElement = - document.getElementById( - "context-panel-title" - ); - - const modelElement = - document.getElementById( - "context-panel-model" - ); - - const summaryElement = + const brainLineElement = document.getElementById( - "context-summary-tokens" + "brain-context-window-line" ); - - const summaryUsedElement = + const brainBarElement = document.getElementById( - "context-summary-used" + "brain-context-window-bar" ); - - const summaryMaxElement = + const brainPercentElement = document.getElementById( - "context-summary-max" + "brain-context-window-percent" ); - - const lineElement = + const serviceLineElement = document.getElementById( - "context-window-line" + "service-context-window-line" ); - - const barElement = + const serviceBarElement = document.getElementById( - "context-window-bar" + "service-context-window-bar" ); - - const percentElement = + const servicePercentElement = document.getElementById( - "context-window-percent" + "service-context-window-percent" ); - - const summarizerLineElement = + const summaryElement = document.getElementById( - "summarizer-window-line" + "context-summary-tokens" ); - - const summarizerBarElement = + const summaryUsedElement = document.getElementById( - "summarizer-window-bar" + "context-summary-used" ); - - const summarizerPercentElement = + const summaryMaxElement = document.getElementById( - "summarizer-window-percent" - ); - - const tokenText = - formatContextTokens(runtime); - - const contextLine = - buildContextLine( - runtime, - getContextBarCells( - barElement - ) - ); - - const pressureColor = - getContextPressureColor( - Math.max( - contextLine.percent, - contextLine.totalPercent - ) - ); - - const summarizerRuntime = - getSummarizerRuntime(); - - const summarizerTokenText = - formatContextTokens( - summarizerRuntime - ); - - const summarizerLine = - buildContextLine( - summarizerRuntime, - getContextBarCells( - summarizerBarElement - ) + "context-summary-max" ); - const summarizerPressureColor = - getContextPressureColor( - Math.max( - summarizerLine.percent, - summarizerLine.totalPercent - ) - ); - - if (titleElement) { - titleElement.textContent = - `STATUS`; - } - - if (modelElement) { - modelElement.textContent = - `${runtime ? runtime.model : "unknown"}`; - } + const brainLine = buildContextLine( + brainRuntime, + getContextBarCells(brainBarElement) + ); + const serviceLine = buildContextLine( + serviceRuntime, + getContextBarCells(serviceBarElement) + ); if (summaryElement) { summaryElement.setAttribute( "aria-label", - `${tokenText.used} / ${tokenText.max}` + `${brainLine.totalUsed} / ${brainLine.max}` ); } if (summaryUsedElement) { summaryUsedElement.textContent = - `${tokenText.used}\u00a0/`; + `${brainLine.totalUsed}\u00a0/`; } if (summaryMaxElement) { summaryMaxElement.textContent = - `${tokenText.max}`; - } - - if (lineElement) { - lineElement.title = - `context: ${contextLine.contextUsed} / ${contextLine.max} ` - + `(${contextLine.contextPercent}%), total: ` - + `${contextLine.totalUsed} / ${contextLine.max} ` - + `(${contextLine.totalPercent}%)`; + `${brainLine.max}`; } - renderContextBar( + function renderRuntimeLine( + role, + runtime, + line, + lineElement, barElement, - contextLine, - pressureColor - ); + percentElement + ) { + const pressureColor = getContextPressureColor( + Math.max( + line.percent, + line.totalPercent + ) + ); - if (percentElement) { - percentElement.textContent = - contextLine.percentLabel; - percentElement.style.color = - pressureColor; - } + if (lineElement) { + lineElement.title = + role.toUpperCase() + + " · " + + (runtime ? runtime.model : "unknown") + + " · context: " + + line.contextUsed + + " / " + + line.max + + " (" + + line.contextPercent + + "%), total: " + + line.totalUsed + + " / " + + line.max + + " (" + + line.totalPercent + + "%)"; + lineElement.setAttribute( + "aria-label", + lineElement.title + ); + } + + renderContextBar( + barElement, + line, + pressureColor + ); - if (summarizerLineElement) { - summarizerLineElement.title = - `context: ${summarizerLine.contextUsed} / ${summarizerLine.max} ` - + `(${summarizerLine.contextPercent}%), total: ` - + `${summarizerLine.totalUsed} / ${summarizerLine.max} ` - + `(${summarizerLine.totalPercent}%)`; + if (percentElement) { + percentElement.textContent = + line.percentLabel; + percentElement.style.color = + pressureColor; + } } - renderContextBar( - summarizerBarElement, - summarizerLine, - summarizerPressureColor + renderRuntimeLine( + "brain", + brainRuntime, + brainLine, + brainLineElement, + brainBarElement, + brainPercentElement + ); + renderRuntimeLine( + "service", + serviceRuntime, + serviceLine, + serviceLineElement, + serviceBarElement, + servicePercentElement ); - - if (summarizerPercentElement) { - summarizerPercentElement.textContent = - summarizerLine.percentLabel; - summarizerPercentElement.style.color = - summarizerPressureColor; - } updateSceneContextPressureFromLines( - contextLine, - summarizerLine + brainLine, + serviceLine ); - void summarizerTokenText; - } @@ -1262,15 +1027,9 @@ brainRuntime ); - const selectedRuntime = - isContextTabDisabled( - runtimePanelState.activeTab - ) - ? null - : getSelectedRuntime(); - - setContextPanelRuntime( - selectedRuntime + setContextPanelRuntimes( + brainRuntime, + serviceRuntime ); } @@ -1374,77 +1133,10 @@ function renderContextPanel() { - - if (isContextTabDisabled( - runtimePanelState.activeTab - )) { - - const fallbackTab = - ["brain", "service"].find( - role => !isContextTabDisabled(role) - ); - - if (fallbackTab) { - runtimePanelState.activeTab = - fallbackTab; - } - } - - setTabClasses("service"); - setTabClasses("brain"); - - const selectedRuntime = - isContextTabDisabled( - runtimePanelState.activeTab - ) - ? null - : getSelectedRuntime(); - - setContextPanelRuntime(selectedRuntime); - - } - - - function selectContextTab(role) { - - if (isContextTabDisabled(role)) { - return; - } - - runtimePanelState.activeTab = - role; - - renderContextPanel(); - - } - - - function focusBrainContextTab() { - - selectContextTab( - "brain" + setContextPanelRuntimes( + getBrainRuntime(), + getServiceRuntime() ); - - } - - - function setUseServiceAsBrain(enabled) { - - runtimePanelState.useServiceAsBrain = - Boolean(enabled); - - renderContextPanel(); - - } - - - function setRuntimeStatusSnapshot(runtimeStatus) { - - runtimePanelState.runtimeStatus = - runtimeStatus || {}; - - renderContextPanel(); - } @@ -1476,10 +1168,8 @@ } window.jinRuntimeConfig = { - useServiceAsBrain: - Boolean( - data.use_service_as_brain - ), + serviceConfigured: + Boolean(data.service_configured), formatResponse: data.format_response !== false, runtimeStatus: { @@ -1490,18 +1180,6 @@ data.runtime_config || {}, }; - setRuntimeStatusSnapshot( - window - .jinRuntimeConfig - .runtimeStatus - ); - - setUseServiceAsBrain( - window - .jinRuntimeConfig - .useServiceAsBrain - ); - setRuntimeConfigSnapshot( window .jinRuntimeConfig @@ -1541,27 +1219,10 @@ window.jinRuntimeConfig = initialRuntimeConfig; - runtimePanelState.useServiceAsBrain = Boolean( - initialRuntimeConfig.useServiceAsBrain - ); - - runtimePanelState.runtimeStatus = ( - initialRuntimeConfig.runtimeStatus - ) || {}; - runtimePanelState.fallbackRuntimes = ( initialRuntimeConfig.runtimeConfig ) || {}; - contextTabButtons = { - service: document.getElementById( - "service-context-tab" - ), - brain: document.getElementById( - "brain-context-tab" - ), - }; - contextRuntimePanel = document.getElementById( "context-runtime-panel" @@ -1583,27 +1244,6 @@ ); } - Object.entries( - contextTabButtons - ).forEach( - ([role, button]) => { - - if (!button) { - return; - } - - button.addEventListener( - "click", - function () { - selectContextTab( - role - ); - } - ); - - } - ); - window.addEventListener( "resize", function () { @@ -1619,14 +1259,6 @@ } else if (window.jinRuntimeConfig) { - setRuntimeStatusSnapshot( - window.jinRuntimeConfig.runtimeStatus || {} - ); - - setUseServiceAsBrain( - window.jinRuntimeConfig.useServiceAsBrain - ); - setRuntimeConfigSnapshot( window.jinRuntimeConfig.runtimeConfig || {} ); @@ -1647,16 +1279,11 @@ getRuntimeByLabel, getServiceRuntime, getBrainRuntime, - getSummarizerRuntime, - getSelectedRuntime, handleTelemetryMessage, updateRuntimePanelFromStatus, flushRuntimeTelemetryRender, renderContextPanel, - setUseServiceAsBrain, - setRuntimeStatusSnapshot, setRuntimeConfigSnapshot, - focusBrainContextTab, }; window.JinRuntime.panel = api; @@ -1673,20 +1300,8 @@ return api.flushRuntimeTelemetryRender(options); }; - window.setUseServiceAsBrain = function (enabled) { - return api.setUseServiceAsBrain(enabled); - }; - - window.setRuntimeStatusSnapshot = function (runtimeStatus) { - return api.setRuntimeStatusSnapshot(runtimeStatus); - }; - window.setRuntimeConfigSnapshot = function (runtimeConfig) { return api.setRuntimeConfigSnapshot(runtimeConfig); }; - window.focusBrainContextTab = function () { - return api.focusBrainContextTab(); - }; - }()); diff --git a/ui/static/js/socket.js b/ui/static/js/socket.js index ac011a0a..e9982230 100644 --- a/ui/static/js/socket.js +++ b/ui/static/js/socket.js @@ -183,7 +183,6 @@ window.requestJinLastResponseRetry = function () { ) || {}; if ( runtimeStatus.brain === false - && runtimeStatus.service === false ) { return false; } @@ -213,10 +212,6 @@ window.requestJinLastResponseRetry = function () { window.clearLatestJinMemoryReferenceText(); } - if (window.focusBrainContextTab) { - window.focusBrainContextTab(); - } - const sent = sendSocketMessage( payload ); diff --git a/ui/static/js/socket/input.js b/ui/static/js/socket/input.js index abaa67e8..6cf1e8f1 100644 --- a/ui/static/js/socket/input.js +++ b/ui/static/js/socket/input.js @@ -211,7 +211,6 @@ function allModelRuntimesOffline() { return ( status.brain === false - && status.service === false ); } @@ -305,10 +304,6 @@ chatForm.addEventListener( window.clearLatestJinMemoryReferenceText(); } - if (window.focusBrainContextTab) { - window.focusBrainContextTab(); - } - const attachments = window.prepareJinAttachments ? await window.prepareJinAttachments() diff --git a/ui/static/js/status.js b/ui/static/js/status.js index a2791324..60619caa 100644 --- a/ui/static/js/status.js +++ b/ui/static/js/status.js @@ -806,9 +806,9 @@ function renderRuntimeStatusModal(role) { ], [ "route", - normalizedRole === "brain" && status.use_service_as_brain - ? "service runtime" - : "dedicated runtime", + normalizedRole === "brain" + ? "primary runtime" + : "dedicated worker", ], ] ); @@ -961,7 +961,23 @@ function setRuntimeChecking(dot, label, button, name) { } -function setRuntimeState(dot, label, button, name, online) { +function setRuntimeState(dot, label, button, name, online, configured = true) { + + if (!configured) { + + dot.className = + "h-2 w-2 rounded-full bg-slate-600 transition-all duration-300"; + + label.textContent = + name; + + setRuntimeButtonInteractivity( + button, + false + ); + + return; + } if (online) { @@ -997,7 +1013,8 @@ function applyRuntimeStatusSnapshot(data) { brainLabel, brainStatusButton, "BRAIN", - data.brain + data.brain, + true ); setRuntimeState( @@ -1005,14 +1022,13 @@ function applyRuntimeStatusSnapshot(data) { serviceLabel, serviceStatusButton, "SERVICE", - data.service + data.service, + Boolean(data.service_configured) ); window.jinRuntimeConfig = { - useServiceAsBrain: - Boolean( - data.use_service_as_brain - ), + serviceConfigured: + Boolean(data.service_configured), formatResponse: data.format_response !== false, runtimeStatus: { @@ -1061,12 +1077,17 @@ async function updateRuntime(options = {}) { "BRAIN" ); - setRuntimeChecking( - serviceDot, - serviceLabel, - serviceStatusButton, - "SERVICE" - ); + if ( + window.jinRuntimeConfig + && window.jinRuntimeConfig.serviceConfigured + ) { + setRuntimeChecking( + serviceDot, + serviceLabel, + serviceStatusButton, + "SERVICE" + ); + } } @@ -1090,7 +1111,10 @@ async function updateRuntime(options = {}) { const offlineStatus = { brain: false, service: false, - use_service_as_brain: false, + service_configured: Boolean( + window.jinRuntimeConfig + && window.jinRuntimeConfig.serviceConfigured + ), format_response: ( window.jinRuntimeConfig && window.jinRuntimeConfig.formatResponse @@ -1125,15 +1149,8 @@ function runtimeStatusIsHealthy() { return false; } - const status = - runtimeConfig.runtimeStatus; - return Boolean( - status.service - && ( - runtimeConfig.useServiceAsBrain - || status.brain - ) + runtimeConfig.runtimeStatus.brain ); } diff --git a/ui/templates/index.html b/ui/templates/index.html index 1f71870d..ed09909b 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -11,12 +11,12 @@ - + - + @@ -40,11 +40,11 @@ - - - -
      -

      dCoC{eFGzi69$B3?exiYrWPC~ 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 Date: Sun, 16 Aug 2026 15:08:05 +0300 Subject: [PATCH 071/168] chore: ui - add disable rating by click - add preview for ids inside JIN answer bubble - remove cluttered backgrounds --- tests/test_answer_rating_client_contract.py | 34 ++ tests/test_attachment_ui_contract.py | 13 + ...test_chat_reference_ids_client_contract.py | 63 +++ ui/static/css/base.css | 2 +- ui/static/css/chat-rating.css | 26 ++ ui/static/css/chat.css | 44 ++ ui/static/js/answer-rating.js | 233 +++++++++- ui/static/js/chat-attachments.js | 18 +- ui/static/js/chat-reference-ids.js | 397 ++++++++++++++++++ ui/static/js/chat.js | 9 + ui/templates/index.html | 13 +- 11 files changed, 836 insertions(+), 16 deletions(-) create mode 100644 tests/test_chat_reference_ids_client_contract.py create mode 100644 ui/static/js/chat-reference-ids.js diff --git a/tests/test_answer_rating_client_contract.py b/tests/test_answer_rating_client_contract.py index a49d1e79..37d387f6 100644 --- a/tests/test_answer_rating_client_contract.py +++ b/tests/test_answer_rating_client_contract.py @@ -71,6 +71,40 @@ def test_win95_theme_keeps_selected_rules_for_committed_bubbles(self): source, ) + + def test_center_zone_disables_rating_and_double_click_restores_latest_bubble(self): + source = ANSWER_RATING_JS.read_text(encoding="utf-8") + + self.assertIn( + '["jin-rating-zone jin-rating-zone-neutral", "disable", "disable rating"]', + source, + ) + self.assertIn('bubble.classList.add("jin-rating-disabled");', source) + self.assertIn('bubble.dataset.ratingModeTitle = label;', source) + self.assertIn('bubble.addEventListener("dblclick"', source) + self.assertIn( + 'if (!isLatestRateableBubble(bubble) || isBubbleLockedBelowCurrentGeneration(bubble))', + source, + ) + self.assertIn('function clearBrowserTextSelection()', source) + self.assertIn('selection.removeAllRanges();', source) + self.assertIn('window.requestAnimationFrame(clearSelection);', source) + self.assertIn( + 'if (enableBubbleRating(bubble)) {\n clearBrowserTextSelection();', + source, + ) + + def test_disabled_rating_exposes_selectable_text_mode(self): + source = CHAT_RATING_CSS.read_text(encoding="utf-8") + + self.assertIn(".jin-chat-bubble-rateable.jin-rating-disabled {", source) + self.assertIn("user-select: text;", source) + self.assertIn( + ".jin-chat-bubble-rateable.jin-rating-disabled .jin-rating-hover-zones", + source, + ) + self.assertIn("display: none;", source) + def test_cache_versions_are_bumped_for_rating_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") diff --git a/tests/test_attachment_ui_contract.py b/tests/test_attachment_ui_contract.py index 592e7acc..246ad6eb 100644 --- a/tests/test_attachment_ui_contract.py +++ b/tests/test_attachment_ui_contract.py @@ -35,6 +35,19 @@ def test_attachment_hover_title_uses_name_middle_dot_size(self): source, ) + def test_attachment_modal_info_starts_with_system_id(self): + source = ( + ROOT + / "ui" + / "static" + / "js" + / "chat-attachments.js" + ).read_text(encoding="utf-8") + + self.assertIn("const systemId =", source) + self.assertIn("attachment && attachment.id", source) + self.assertIn("detailParts[0] = systemId;", source) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_chat_reference_ids_client_contract.py b/tests/test_chat_reference_ids_client_contract.py new file mode 100644 index 00000000..a81da289 --- /dev/null +++ b/tests/test_chat_reference_ids_client_contract.py @@ -0,0 +1,63 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +REFERENCE_JS = ROOT / "ui" / "static" / "js" / "chat-reference-ids.js" +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +CHAT_CSS = ROOT / "ui" / "static" / "css" / "chat.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class ChatReferenceIdsClientContractTests(unittest.TestCase): + + def test_answer_ids_resolve_only_against_live_file_and_delayed_stores(self): + source = REFERENCE_JS.read_text(encoding="utf-8") + + self.assertIn("window.JinFiles.getFiles()", source) + self.assertIn("window.JinRuntime.runtime", source) + self.assertIn("runtime.getDelayedMemoryReports()", source) + self.assertIn('kind: "file"', source) + self.assertIn('kind: "delayed"', source) + self.assertIn('references.has(id)', source) + self.assertIn('new RegExp(`\\\\b(?:${ids.map(escapeRegex).join("|")})\\\\b`', source) + + def test_image_ids_use_existing_hover_preview_and_attachment_modal(self): + source = REFERENCE_JS.read_text(encoding="utf-8") + + self.assertIn("window.bindJinAttachmentHoverPreview", source) + self.assertIn("{ hoverPreviewMaxPx: 100 }", source) + self.assertIn("window.openJinAttachmentModal(reference.record)", source) + + def test_text_ids_show_clean_filename_and_delayed_ids_open_report_modal(self): + source = REFERENCE_JS.read_text(encoding="utf-8") + + self.assertIn("cleanPersistentFileName", source) + self.assertIn('element.title = cleanPersistentFileName(reference.record) || reference.id;', source) + self.assertIn("memoryView.openDelayedMemoryReportModal(reference.record)", source) + self.assertIn("reference.record && reference.record.title", source) + + def test_stream_render_decorates_ids_without_replacing_chat_parser(self): + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn("window.JinChatReferenceIds.decorate", source) + self.assertIn("renderChatTextHtml", source) + + def test_reference_ids_have_dark_and_light_theme_treatment(self): + source = CHAT_CSS.read_text(encoding="utf-8") + + self.assertIn(".jin-chat-reference-id {", source) + self.assertIn("font-weight: 700;", source) + self.assertIn("body.theme-win95 .jin-chat-reference-id {", source) + + def test_reference_parser_is_loaded_before_chat_renderer(self): + source = INDEX_HTML.read_text(encoding="utf-8") + reference_script = '/static/js/chat-reference-ids.js?v=reference-ids-1' + chat_script = '/static/js/chat.js?v=' + + self.assertIn(reference_script, source) + self.assertLess(source.index(reference_script), source.index(chat_script)) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/static/css/base.css b/ui/static/css/base.css index b4a0838d..4b31969c 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -145,7 +145,7 @@ main.panel-startup-collapse-active { main.scene-clutter-middle #scene-clutter-overlay, main.scene-cluttered #scene-cluttered-overlay, main.scene-searching #scene-search-overlay { - opacity: 1; + opacity: 0; } #scene-jin-tint { diff --git a/ui/static/css/chat-rating.css b/ui/static/css/chat-rating.css index d707c3be..d2d7e902 100644 --- a/ui/static/css/chat-rating.css +++ b/ui/static/css/chat-rating.css @@ -371,3 +371,29 @@ .jin-chat-bubble-rateable.jin-rating-selected-plus .jin-chat-pre { transform: translateX(2px) skewY(0.18deg); } + +/* Rating can be temporarily disabled on the latest JIN answer so the bubble + behaves like ordinary selectable text. Double-click restores rating. */ +.jin-chat-bubble-rateable.jin-rating-disabled { + cursor: text; + user-select: text; + -webkit-user-select: text; + filter: none; +} + +.jin-chat-bubble-rateable.jin-rating-disabled .jin-rating-hover-zones { + display: none; + pointer-events: none; +} + +/* While rating is active the three hit zones own normal text clicks, but + system-reference ids stay hoverable above them. */ +.jin-chat-bubble-rateable .jin-chat-pre { + z-index: 6; + pointer-events: none; +} + +.jin-chat-bubble-rateable.jin-rating-disabled .jin-chat-pre, +.jin-chat-bubble-rateable.jin-rating-committed .jin-chat-pre { + pointer-events: auto; +} diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 019b1379..cc8050bd 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -778,3 +778,47 @@ rgba(30, 35, 39, 0.94) calc(var(--JIN_BUBBLE_INVERT) * 100%) ); } + +/* Resolved six-character JIN ids inside answer text. The id remains part of + the sentence, but reads as a live system reference. */ +.jin-chat-reference-id { + position: relative; + z-index: 1; + pointer-events: auto; + font-weight: 700; + color: color-mix( + in srgb, + rgba(244, 250, 252, 0.98) calc(var(--jin-bubble-invert-off) * 100%), + rgba(20, 24, 28, 0.96) calc(var(--JIN_BUBBLE_INVERT) * 100%) + ); + text-shadow: + 0 0 8px rgba(226, 247, 250, calc(var(--jin-bubble-invert-off) * 0.28)), + 0 0 14px rgba(103, 232, 249, calc(var(--jin-bubble-invert-off) * 0.08)); + cursor: help; + transition: + color 0.14s ease, + text-shadow 0.14s ease; +} + +.jin-chat-reference-id:hover { + color: color-mix( + in srgb, + rgba(255, 255, 255, 1) calc(var(--jin-bubble-invert-off) * 100%), + rgba(8, 12, 16, 0.98) calc(var(--JIN_BUBBLE_INVERT) * 100%) + ); +} + +.jin-chat-bubble-rateable.jin-rating-disabled .jin-chat-reference-id, +.jin-chat-bubble-rateable.jin-rating-committed .jin-chat-reference-id { + cursor: pointer; +} + +body.theme-win95 .jin-chat-reference-id { + color: #171717; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.20); +} + +body.theme-win95 .jin-chat-reference-id:hover { + color: #000; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.28); +} diff --git a/ui/static/js/answer-rating.js b/ui/static/js/answer-rating.js index f9ba7160..647a2453 100644 --- a/ui/static/js/answer-rating.js +++ b/ui/static/js/answer-rating.js @@ -137,6 +137,9 @@ } bubble.classList.remove(...ratingPressClasses); + bubble.classList.remove("jin-rating-disabled"); + delete bubble.dataset.ratingDisabled; + clearBubbleRatingModeTitle(bubble); bubble.classList.add("jin-rating-committed"); bubble.dataset.ratingPending = "false"; bubble.dataset.ratingCommitted = "true"; @@ -186,6 +189,207 @@ return bubble && bubble === syncLatestRateableBubbleState(); } + function isBubbleLockedBelowCurrentGeneration(bubble) { + const bubbleGeneration = Number( + bubble && bubble.dataset.ratingGateGeneration || 0 + ); + const gateState = window.getJinAnswerRatingL1GateState + ? window.getJinAnswerRatingL1GateState() + : {}; + const lockedBelow = Number(gateState.lockedBelowGeneration || 0); + + return Boolean( + bubbleGeneration > 0 + && lockedBelow > 0 + && bubbleGeneration < lockedBelow + ); + } + + function clearBubbleRatingModeTitle(bubble) { + if (!bubble || bubble.dataset.ratingModeTitle !== "enable rating") { + return; + } + + bubble.removeAttribute("alt"); + bubble.removeAttribute("aria-label"); + bubble.removeAttribute("title"); + delete bubble.dataset.ratingModeTitle; + } + + function setBubbleRatingModeTitle(bubble) { + if (!bubble) { + return; + } + + const label = "enable rating"; + bubble.dataset.ratingModeTitle = label; + bubble.setAttribute("alt", label); + bubble.setAttribute("aria-label", label); + bubble.setAttribute("title", label); + } + + function disableBubbleRating(bubble) { + if ( + !bubble + || bubble.classList.contains("jin-rating-committed") + || bubble.dataset.ratingCommitted === "true" + || bubble.dataset.ratingPastTurn === "true" + ) { + return false; + } + + if (!isLatestRateableBubble(bubble)) { + markBubbleAsPastTurn(bubble); + return false; + } + + if (isBubbleLockedBelowCurrentGeneration(bubble)) { + markBubbleAsPastTurn(bubble); + return false; + } + + const previousRating = bubble.dataset.ratingSelected || null; + + bubble.classList.remove(...ratingSelectionClasses); + delete bubble.dataset.ratingSelected; + delete bubble.dataset.ratingPending; + clearBubbleRatingIntensity(bubble); + setBubbleRatingClickAlt(bubble, 0); + + if (previousRating) { + if (window.clearJinAnswerRating) { + window.clearJinAnswerRating({ + previousRating, + reason: "rating-disabled", + runtimeSnapshotIndex: bubble.dataset.runtimeSnapshotIndex || null, + ratingGateGeneration: bubble.dataset.ratingGateGeneration || null, + ratingBubbleSequence: bubble.dataset.ratingBubbleSequence || null, + }); + } + + bubble.dispatchEvent(new CustomEvent("jin:answer-rating-cleared", { + bubbles: true, + detail: { + previousRating, + reason: "rating-disabled", + }, + })); + } + + bubble.classList.add("jin-rating-disabled"); + bubble.dataset.ratingDisabled = "true"; + setBubbleRatingModeTitle(bubble); + + bubble.dispatchEvent(new CustomEvent("jin:answer-rating-disabled", { + bubbles: true, + })); + + return true; + } + + function enableBubbleRating(bubble) { + if ( + !bubble + || bubble.dataset.ratingDisabled !== "true" + || bubble.classList.contains("jin-rating-committed") + || bubble.dataset.ratingCommitted === "true" + || bubble.dataset.ratingPastTurn === "true" + ) { + return false; + } + + if (!isLatestRateableBubble(bubble) || isBubbleLockedBelowCurrentGeneration(bubble)) { + markBubbleAsPastTurn(bubble); + return false; + } + + bubble.classList.remove("jin-rating-disabled"); + delete bubble.dataset.ratingDisabled; + clearBubbleRatingModeTitle(bubble); + markBubbleRatingL1State(bubble); + + bubble.dispatchEvent(new CustomEvent("jin:answer-rating-enabled", { + bubbles: true, + })); + + return true; + } + + function clearBrowserTextSelection() { + const clearSelection = () => { + const selection = window.getSelection ? window.getSelection() : null; + if (selection && selection.rangeCount) { + selection.removeAllRanges(); + } + }; + + clearSelection(); + window.requestAnimationFrame(clearSelection); + } + + function bindBubbleRatingModeInteractions(bubble, zones) { + if (!bubble || bubble.dataset.ratingModeBound === "true") { + return; + } + + bubble.dataset.ratingModeBound = "true"; + + bubble.addEventListener("dblclick", (event) => { + if (bubble.dataset.ratingDisabled !== "true") { + return; + } + + if (event.target.closest && event.target.closest(".jin-chat-reference-id")) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + if (enableBubbleRating(bubble)) { + clearBrowserTextSelection(); + } + }); + + bubble.addEventListener("click", (event) => { + const reference = event.target.closest + ? event.target.closest(".jin-chat-reference-id") + : null; + + if (!reference || bubble.dataset.ratingDisabled === "true") { + return; + } + + if ( + bubble.classList.contains("jin-rating-committed") + || bubble.dataset.ratingCommitted === "true" + || bubble.dataset.ratingPastTurn === "true" + ) { + return; + } + + const rect = bubble.getBoundingClientRect(); + if (!rect.width) { + return; + } + + const ratio = Math.max(0, Math.min(0.999, (event.clientX - rect.left) / rect.width)); + const zoneIndex = ratio < (1 / 3) + ? 0 + : (ratio < (2 / 3) ? 1 : 2); + const zone = zones && zones.children + ? zones.children[zoneIndex] + : null; + + if (!zone) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + zone.click(); + }); + } + function getCurrentRatingGateGeneration() { if (!window.getJinAnswerRatingL1GateState) { return 0; @@ -341,18 +545,26 @@ [ ["jin-rating-zone jin-rating-zone-minus", "minus", "negative feedback hover zone"], - ["jin-rating-zone jin-rating-zone-neutral", "neutral", "neutral feedback hover zone"], + ["jin-rating-zone jin-rating-zone-neutral", "disable", "disable rating"], ["jin-rating-zone jin-rating-zone-plus", "plus", "positive feedback hover zone"], ].forEach(([className, ratingValue, label]) => { const zone = document.createElement("div"); zone.className = className; zone.dataset.ratingValue = ratingValue; zone.dataset.ratingHover = label; + if (ratingValue === "disable") { + zone.title = "disable rating"; + } zone.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); + if (ratingValue === "disable") { + disableBubbleRating(bubble); + return; + } + if ( bubble.classList.contains("jin-rating-committed") || bubble.dataset.ratingCommitted === "true" @@ -372,12 +584,7 @@ // Generation guard: if a newer turn has already been // submitted, this bubble's gate generation is below the // lock threshold — treat it as permanently committed. - const bubbleGen = Number(bubble.dataset.ratingGateGeneration || 0); - const gateState = window.getJinAnswerRatingL1GateState - ? window.getJinAnswerRatingL1GateState() - : {}; - const lockedBelow = Number(gateState.lockedBelowGeneration || 0); - if (bubbleGen > 0 && bubbleGen < lockedBelow) { + if (isBubbleLockedBelowCurrentGeneration(bubble)) { bubble.classList.add("jin-rating-committed"); bubble.dataset.ratingCommitted = "true"; bubble.dataset.ratingPastTurn = "true"; @@ -455,6 +662,7 @@ }); bubble.appendChild(zones); + bindBubbleRatingModeInteractions(bubble, zones); }); } @@ -491,6 +699,17 @@ const chatForm = document.getElementById("chat-form"); if (chatForm) { chatForm.addEventListener("submit", () => { + document + .querySelectorAll(ratingBubbleSelector) + .forEach((bubble) => { + if ( + bubble.dataset.ratingDisabled === "true" + && bubble.dataset.ratingPastTurn !== "true" + ) { + markBubbleAsPastTurn(bubble); + } + }); + document .querySelectorAll(activeRatingBubbleSelector) .forEach((bubble) => { diff --git a/ui/static/js/chat-attachments.js b/ui/static/js/chat-attachments.js index 8e7cbcef..6717115b 100644 --- a/ui/static/js/chat-attachments.js +++ b/ui/static/js/chat-attachments.js @@ -477,10 +477,24 @@ function createAttachmentInfoElement(attachment) { info.className = "jin-attachment-modal-info"; - info.textContent = + + const detailParts = getAttachmentDetailParts( attachment - ).join(" - "); + ); + const systemId = + normalizeAttachmentValue( + attachment && attachment.id + ).trim(); + + if (systemId && detailParts.length) { + detailParts[0] = systemId; + } else if (systemId) { + detailParts.push(systemId); + } + + info.textContent = + detailParts.join(" - "); return info; } diff --git a/ui/static/js/chat-reference-ids.js b/ui/static/js/chat-reference-ids.js new file mode 100644 index 00000000..2bdea072 --- /dev/null +++ b/ui/static/js/chat-reference-ids.js @@ -0,0 +1,397 @@ +(function () { + const REFERENCE_CLASS = "jin-chat-reference-id"; + const REFERENCE_SELECTOR = `.${REFERENCE_CLASS}`; + const RESPONSE_SELECTOR = + ".jin-chat-bubble-brain .jin-chat-pre, .jin-chat-bubble-service .jin-chat-pre"; + const FILES_CHANGED_EVENT = "jin:files-store-changed"; + const DELAYED_CHANGED_EVENT = "jin:delayed-memory-store-changed"; + const ATTACHMENT_UI_READY_EVENT = "jin:attachment-ui-ready"; + + let referenceCache = null; + + function normalizeId(value) { + return String(value || "").trim().toLowerCase(); + } + + function escapeRegex(value) { + return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + function cleanPersistentFileName(record) { + if (!record) return ""; + + const id = normalizeId(record.id); + const raw = String( + record.name + || record.filename + || record.stored_name + || "" + ).trim(); + + if (!raw) return id; + + const prefixed = id + ? new RegExp(`^${escapeRegex(id)}[_-]`, "i") + : null; + + return prefixed + ? raw.replace(prefixed, "") + : raw; + } + + function getPersistentFiles() { + if (!window.JinFiles || typeof window.JinFiles.getFiles !== "function") { + return []; + } + + try { + return window.JinFiles.getFiles() || []; + } catch (_error) { + return []; + } + } + + function getDelayedReports() { + const runtime = + window.JinRuntime + && window.JinRuntime.runtime; + if (!runtime || typeof runtime.getDelayedMemoryReports !== "function") { + return []; + } + + let reports = null; + try { + reports = runtime.getDelayedMemoryReports(); + } catch (_error) { + return []; + } + + if (Array.isArray(reports)) { + return reports; + } + + if (!reports || typeof reports !== "object") { + return []; + } + + return Object.entries(reports).map(([storageKey, report]) => { + if (!report || typeof report !== "object" || Array.isArray(report)) { + return null; + } + + return { + ...report, + _storage_key: report._storage_key || storageKey, + }; + }).filter(Boolean); + } + + function buildReferenceCache() { + const references = new Map(); + + getPersistentFiles().forEach((record) => { + const id = normalizeId(record && record.id); + if (!/^[a-z0-9]{6}$/.test(id)) return; + + references.set(id, { + kind: "file", + id, + record, + }); + }); + + getDelayedReports().forEach((report) => { + const id = normalizeId( + report && ( + report._storage_key + || report.id + ) + ); + if (!/^[a-z0-9]{6}$/.test(id) || references.has(id)) return; + + references.set(id, { + kind: "delayed", + id, + record: report, + }); + }); + + const ids = Array.from(references.keys()); + const pattern = ids.length + ? new RegExp(`\\b(?:${ids.map(escapeRegex).join("|")})\\b`, "gi") + : null; + + referenceCache = { + references, + pattern, + }; + + return referenceCache; + } + + function getReferenceCache() { + return referenceCache || buildReferenceCache(); + } + + function invalidateReferenceCache() { + referenceCache = null; + } + + function isBubbleRatingCapturingClick(element) { + const bubble = element && element.closest + ? element.closest(".jin-chat-bubble-rateable") + : null; + + if (!bubble) return false; + + return !( + bubble.classList.contains("jin-rating-disabled") + || bubble.classList.contains("jin-rating-committed") + || bubble.dataset.ratingCommitted === "true" + || bubble.dataset.ratingPastTurn === "true" + ); + } + + function hasActiveSelection() { + const selection = window.getSelection && window.getSelection(); + return Boolean(selection && !selection.isCollapsed && String(selection).trim()); + } + + function openDelayedReference(reference) { + const memoryView = + window.JinRuntime + && window.JinRuntime.memoryView; + + if ( + memoryView + && typeof memoryView.openDelayedMemoryReportModal === "function" + ) { + memoryView.openDelayedMemoryReportModal(reference.record); + } + } + + function openFileReference(reference) { + if (typeof window.openJinAttachmentModal === "function") { + window.openJinAttachmentModal(reference.record); + } + } + + function openReference(reference) { + if (!reference) return; + + if (reference.kind === "delayed") { + openDelayedReference(reference); + return; + } + + if (reference.kind === "file") { + openFileReference(reference); + } + } + + function bindImageHoverPreview(element, reference) { + if ( + !element + || !reference + || reference.kind !== "file" + || String(reference.record && reference.record.kind || "").toLowerCase() !== "image" + ) { + return; + } + + const bind = () => { + if (element.dataset.jinReferenceHoverBound === "1") return true; + if (typeof window.bindJinAttachmentHoverPreview !== "function") return false; + + window.bindJinAttachmentHoverPreview( + element, + reference.record, + { hoverPreviewMaxPx: 100 } + ); + element.dataset.jinReferenceHoverBound = "1"; + return true; + }; + + if (!bind()) { + window.addEventListener( + ATTACHMENT_UI_READY_EVENT, + bind, + { once: true } + ); + } + } + + function configureReferenceElement(element, reference) { + element.className = REFERENCE_CLASS; + element.dataset.jinReferenceId = reference.id; + element.dataset.jinReferenceKind = reference.kind; + + if (reference.kind === "delayed") { + const title = String( + reference.record && reference.record.title + || reference.id + ).trim(); + element.title = title || reference.id; + } else if (reference.kind === "file") { + const kind = String(reference.record && reference.record.kind || "").toLowerCase(); + if (kind === "text") { + element.title = cleanPersistentFileName(reference.record) || reference.id; + } + bindImageHoverPreview(element, reference); + } + + element.addEventListener("click", (event) => { + if (isBubbleRatingCapturingClick(element)) { + return; + } + + if (hasActiveSelection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + openReference(reference); + }); + } + + function shouldSkipTextNode(node, root) { + const parent = node && node.parentElement; + if (!parent) return true; + if (parent.closest(REFERENCE_SELECTOR)) return true; + if (parent.closest("code, a, button, input, textarea, select")) return true; + return !root.contains(parent); + } + + function decorateTextNode(node, root, references, pattern) { + if (!node || !node.nodeValue || shouldSkipTextNode(node, root)) return; + + const text = node.nodeValue; + pattern.lastIndex = 0; + + let match = pattern.exec(text); + if (!match) return; + + const fragment = document.createDocumentFragment(); + let lastIndex = 0; + + do { + const matchedText = match[0]; + const id = normalizeId(matchedText); + const reference = references.get(id); + + if (!reference) { + match = pattern.exec(text); + continue; + } + + if (match.index > lastIndex) { + fragment.appendChild( + document.createTextNode(text.slice(lastIndex, match.index)) + ); + } + + const element = document.createElement("span"); + element.textContent = matchedText; + configureReferenceElement(element, reference); + fragment.appendChild(element); + + lastIndex = match.index + matchedText.length; + match = pattern.exec(text); + } while (match); + + if (lastIndex < text.length) { + fragment.appendChild( + document.createTextNode(text.slice(lastIndex)) + ); + } + + node.replaceWith(fragment); + } + + function decorate(element) { + if (!element) return; + + const { references, pattern } = getReferenceCache(); + if (!pattern || !references.size) return; + + const nodes = []; + const walker = document.createTreeWalker( + element, + NodeFilter.SHOW_TEXT, + { + acceptNode(node) { + if (shouldSkipTextNode(node, element)) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + } + ); + + let node = walker.nextNode(); + while (node) { + nodes.push(node); + node = walker.nextNode(); + } + + nodes.forEach((textNode) => { + decorateTextNode(textNode, element, references, pattern); + }); + } + + function decorateAll(root = document) { + const scope = root && root.querySelectorAll ? root : document; + const elements = Array.from(scope.querySelectorAll(RESPONSE_SELECTOR)); + + if (scope.matches && scope.matches(RESPONSE_SELECTOR)) { + elements.unshift(scope); + } + + elements.forEach(decorate); + } + + function clearDecorations(root = document) { + const scope = root && root.querySelectorAll ? root : document; + const elements = Array.from(scope.querySelectorAll(REFERENCE_SELECTOR)); + + if (scope.matches && scope.matches(REFERENCE_SELECTOR)) { + elements.unshift(scope); + } + + const parents = new Set(); + elements.forEach((element) => { + if (!element || !element.parentNode) return; + parents.add(element.parentNode); + element.replaceWith( + document.createTextNode(element.textContent || "") + ); + }); + + parents.forEach((parent) => { + if (parent && typeof parent.normalize === "function") { + parent.normalize(); + } + }); + } + + function refreshReferences() { + invalidateReferenceCache(); + clearDecorations(document); + decorateAll(document); + } + + window.addEventListener(FILES_CHANGED_EVENT, refreshReferences); + window.addEventListener(DELAYED_CHANGED_EVENT, refreshReferences); + window.addEventListener(ATTACHMENT_UI_READY_EVENT, () => { + decorateAll(document); + }); + + window.JinChatReferenceIds = { + decorate, + decorateAll, + refresh: refreshReferences, + }; + + decorateAll(document); +})(); diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index 977930d5..20c81e93 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -306,6 +306,15 @@ function renderChatTextElement( text ); + if ( + window.JinChatReferenceIds + && typeof window.JinChatReferenceIds.decorate === "function" + ) { + window.JinChatReferenceIds.decorate( + element + ); + } + } function isStreamDebugEnabled() { diff --git a/ui/templates/index.html b/ui/templates/index.html index 8762691f..e3ce64cc 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -12,7 +12,7 @@ - + @@ -78,14 +78,14 @@

      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 z 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 KT6)%xlmFT0eEL2B diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 254f3899..2491c89e 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -50,6 +50,8 @@ startAngle: -9, }), }); + const MEMORY_SIGNAL_KIND_ORDER = + Object.freeze(["l4", "delayed", "active"]); const SNAPSHOT_GLOW_CLEAR_DELAY_MS = 360; const CENTER_COLOR_STEP_MS = 120; @@ -929,6 +931,7 @@ serializeL4FactIds(options.delayedMemoryAnchorFactIds), "data-l4-fact-id": options.l4FactId || null, "data-l4-fact-ids": serializeL4FactIds(options.l4FactIds), + "data-avatar-memory-angle": options.angle, }); setAvatarMemoryReferenceAliases( @@ -1012,7 +1015,15 @@ "--jin-avatar-memory-hover-dot-radius", `${Number(dotRadius + 0.85).toFixed(2)}px` ); + return; } + + dashGroup.style.removeProperty( + "--jin-avatar-memory-dot-radius" + ); + dashGroup.style.removeProperty( + "--jin-avatar-memory-hover-dot-radius" + ); } function getMemoryRingAnimation(records, kind) { @@ -1166,6 +1177,108 @@ svg.appendChild(ring); } + function getMemorySignalRecords(kind) { + if (kind === "active") { + return getActiveMemoryAvatarRecords(); + } + + if (kind === "delayed") { + return getDelayedMemoryAvatarRecords(); + } + + if (kind === "l4") { + return getL4MemoryAvatarRecords(); + } + + return null; + } + + function getMemorySignalInsertionReference(svg, kind) { + const kindIndex = + MEMORY_SIGNAL_KIND_ORDER.indexOf(kind); + + if (kindIndex < 0) { + return null; + } + + for ( + let index = kindIndex + 1; + index < MEMORY_SIGNAL_KIND_ORDER.length; + index += 1 + ) { + const nextRing = + svg.querySelector( + `.jin-avatar-memory-ring-${MEMORY_SIGNAL_KIND_ORDER[index]}` + ); + + if (nextRing) { + return nextRing; + } + } + + return svg.querySelector(".jin-avatar-center"); + } + + function applyAvatarReactiveGlows() { + applyThinkRuntimeCitationGlow(); + applyMemoryReferenceGlow(); + applyMemoryRowAvatarHoverGlow(); + } + + function syncMemorySignalLayer(kind, options = {}) { + const svg = + avatarRoot.querySelector("svg"); + const layout = + MEMORY_RING_LAYOUT[kind]; + const records = + getMemorySignalRecords(kind); + + if ( + !svg + || !layout + || !Array.isArray(records) + ) { + return false; + } + + svg.querySelectorAll( + `.jin-avatar-memory-ring-${kind}` + ).forEach(ring => ring.remove()); + + if (records.length) { + const temporaryParent = + createSvgElement("g"); + + appendMemorySignalRing( + temporaryParent, + records, + layout, + kind, + avatarRoot.style + .getPropertyValue("--jin-avatar-overall-color") + .trim() || DEFAULT_RING_COLOR + ); + + const nextRing = + temporaryParent.firstElementChild; + + if (!nextRing) { + return false; + } + + svg.insertBefore( + nextRing, + getMemorySignalInsertionReference(svg, kind) + ); + } + + if (options.applyGlows !== false) { + applyAvatarReactiveGlows(); + } + + return true; + } + function setDelayedMemoryDashPinned(reportId, pinned) { const delayedMemoryId = String(reportId || "").trim().toLowerCase(); @@ -1223,6 +1336,280 @@ return true; } + function getMemoryDashNodesByDataset(svg, selector, datasetKey) { + const nodesById = new Map(); + const nodes = + Array.from(svg.querySelectorAll(selector)); + + for (const node of nodes) { + const id = + String( + node && node.dataset + ? node.dataset[datasetKey] + : "" + ).trim(); + + if (!id || nodesById.has(id)) { + return null; + } + + nodesById.set(id, node); + } + + return { + nodes, + nodesById, + }; + } + + function setL4MemoryDashArchivedState(dashGroup, archived) { + if (!dashGroup) { + return false; + } + + const path = + dashGroup.querySelector("path"); + + if (!path) { + return false; + } + + const nextArchived = + Boolean(archived); + const nextOpacity = + nextArchived ? 0.26 : 0.52; + const overallColor = + avatarRoot.style.getPropertyValue("--jin-avatar-overall-color").trim() + || DEFAULT_RING_COLOR; + const nextColor = + mixColors( + L4_MEMORY_RING_COLOR, + overallColor, + 0.08 + ); + const dotRadius = + Math.max( + MEMORY_RING_LAYOUT.l4.strokeWidth * 1.45, + 1.35 + ); + const angle = + Number(dashGroup.dataset.avatarMemoryAngle); + + if (nextArchived && !Number.isFinite(angle)) { + return false; + } + + dashGroup.classList.toggle( + "is-memory-archived", + nextArchived + ); + dashGroup.classList.toggle( + "is-memory-dot", + nextArchived + ); + + setMemoryDashGlowVariables( + dashGroup, + L4_MEMORY_RING_COLOR, + MEMORY_RING_LAYOUT.l4.strokeWidth + 0.75, + nextArchived ? dotRadius : 0 + ); + + path.classList.toggle( + "jin-avatar-memory-dash-arc", + nextArchived + ); + path.setAttribute( + "stroke", + nextColor + ); + path.setAttribute( + "stroke-opacity", + nextOpacity.toFixed(2) + ); + + if (!nextArchived) { + dashGroup.querySelectorAll(".jin-avatar-memory-dot") + .forEach(dot => dot.remove()); + dashGroup.style.removeProperty( + "--jin-avatar-memory-dot-opacity" + ); + return true; + } + + const dotPoint = + polarPoint( + MEMORY_RING_LAYOUT.l4.radius, + angle + ); + let dot = + dashGroup.querySelector(".jin-avatar-memory-dot"); + + if (!dot) { + dot = createSvgElement("circle", { + class: "jin-avatar-memory-dot", + }); + dashGroup.appendChild(dot); + } + + dashGroup.style.setProperty( + "--jin-avatar-memory-dot-opacity", + nextOpacity.toFixed(2) + ); + dot.setAttribute("cx", dotPoint.x.toFixed(3)); + dot.setAttribute("cy", dotPoint.y.toFixed(3)); + dot.setAttribute("r", dotRadius.toFixed(2)); + dot.setAttribute("fill", nextColor); + dot.setAttribute("fill-opacity", nextOpacity.toFixed(2)); + + return true; + } + + function syncDelayedMemoryDashState() { + const svg = + avatarRoot.querySelector("svg"); + + if (!svg) { + return false; + } + + const delayedMemoryRecords = + getDelayedMemoryAvatarRecords(); + const nodeLookup = + getMemoryDashNodesByDataset( + svg, + ".jin-avatar-memory-dash-delayed", + "delayedMemoryId" + ); + + if ( + !nodeLookup + || nodeLookup.nodes.length !== delayedMemoryRecords.length + ) { + return false; + } + + let synced = true; + + delayedMemoryRecords.forEach((record) => { + const dashGroup = + nodeLookup.nodesById.get(record.id); + + if (!dashGroup) { + synced = false; + return; + } + + dashGroup.dataset.delayedMemoryFactIds = + serializeL4FactIds(record.factIds) || ""; + dashGroup.dataset.delayedMemoryAnchorFactIds = + serializeL4FactIds(record.anchorFactIds) || ""; + + if ( + !setDelayedMemoryDashPinned( + record.id, + Boolean(record.pinned) + ) + ) { + synced = false; + } + }); + + return synced; + } + + function syncL4MemoryArchiveState() { + const svg = + avatarRoot.querySelector("svg"); + + if (!svg) { + return false; + } + + const l4MemoryRecords = + getL4MemoryAvatarRecords(); + const nodeLookup = + getMemoryDashNodesByDataset( + svg, + ".jin-avatar-memory-dash-l4", + "l4FactId" + ); + + if ( + !nodeLookup + || nodeLookup.nodes.length !== l4MemoryRecords.length + ) { + return false; + } + + let synced = true; + + l4MemoryRecords.forEach((record) => { + const dashGroup = + nodeLookup.nodesById.get(record.id); + + if (!dashGroup) { + synced = false; + return; + } + + dashGroup.dataset.l4FactIds = + serializeL4FactIds(record.l4FactIds) || ""; + dashGroup.dataset.runtimeLineKey = + normalizeRuntimeCitationIdentity(record.key); + dashGroup.dataset.runtimeLineText = + record.citationText || ""; + dashGroup.dataset.runtimeLineIdentity = + record.citationIdentity || ""; + setAvatarMemoryReferenceAliases( + dashGroup, + record.referenceAliases + ); + + if ( + !setL4MemoryDashArchivedState( + dashGroup, + record.archived + ) + ) { + synced = false; + } + }); + + return synced; + } + + function syncDelayedMemoryState() { + const delayedSynced = + syncDelayedMemoryDashState() + || syncMemorySignalLayer( + "delayed", + { applyGlows: false } + ); + const l4Synced = + syncL4MemoryArchiveState() + || syncMemorySignalLayer( + "l4", + { applyGlows: false } + ); + + if (!delayedSynced || !l4Synced) { + return false; + } + + applyAvatarReactiveGlows(); + + return true; + } + + function syncActiveMemoryState() { + return syncMemorySignalLayer("active"); + } + + function syncL4MemoryState() { + return syncMemorySignalLayer("l4"); + } + function appendMemorySignalRings( svg, activeMemoryRecords, @@ -1619,6 +2006,7 @@ function appendCenter(svg, overallColor, currentCenterColor) { const center = createSvgElement("g", { + class: "jin-avatar-center", "pointer-events": "none", }); @@ -2464,6 +2852,9 @@ refresh: reinitializeAvatar, setCenterColor, setDelayedMemoryPinned: setDelayedMemoryDashPinned, + syncActiveMemoryState, + syncDelayedMemoryState, + syncL4MemoryState, get aggressivePalette() { return AGGRESSIVE_PALETTE; }, diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index 37a01006..a5f696df 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -280,6 +280,25 @@ return normalized; } + function syncL4MemoryStateToAvatar() { + if ( + window.JinRuntime.avatar + && typeof window.JinRuntime.avatar.syncL4MemoryState === "function" + ) { + return window.JinRuntime.avatar.syncL4MemoryState(); + } + + if ( + window.JinRuntime.avatar + && typeof window.JinRuntime.avatar.refresh === "function" + ) { + window.JinRuntime.avatar.refresh(); + return true; + } + + return false; + } + function getFacts() { return readStore().facts; } @@ -483,12 +502,7 @@ ) { window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); } - if ( - window.JinRuntime.avatar - && typeof window.JinRuntime.avatar.refresh === "function" - ) { - window.JinRuntime.avatar.refresh(); - } + syncL4MemoryStateToAvatar(); return true; } @@ -559,12 +573,7 @@ ) { window.JinRuntime.runtime.renderRuntimeMemorySnapshot(); } - if ( - window.JinRuntime.avatar - && typeof window.JinRuntime.avatar.refresh === "function" - ) { - window.JinRuntime.avatar.refresh(); - } + syncL4MemoryStateToAvatar(); return store; } diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 58646530..2689e609 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -631,13 +631,46 @@ function setDelayedMemoryPinnedOnAvatar( return false; } +function syncDelayedMemoryStateToAvatar() { + const avatar = + window.JinRuntime + && window.JinRuntime.avatar; + + if ( + avatar + && typeof avatar.syncDelayedMemoryState === "function" + ) { + return avatar.syncDelayedMemoryState(); + } + + return false; +} + +function syncActiveMemoryStateToAvatar() { + const avatar = + window.JinRuntime + && window.JinRuntime.avatar; + + if ( + avatar + && typeof avatar.syncActiveMemoryState === "function" + ) { + return avatar.syncActiveMemoryState(); + } + + return false; +} + function writeActiveMemoryRecordsAndRefresh( records ) { writeActiveMemoryRecords( records ); - refreshRuntimeAvatar(); + + if (!syncActiveMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return readActiveMemoryRecords(); } @@ -798,7 +831,10 @@ function appendActiveMemoryRecordsAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncActiveMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return nextRecords; @@ -815,7 +851,10 @@ function replaceActiveMemoryRecordsAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncActiveMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return readActiveMemoryRecords(); @@ -833,7 +872,10 @@ function removeActiveMemoryRecordByIdAndRender( showLatestRuntimeMemorySnapshot(); renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncActiveMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return nextRecords; @@ -883,6 +925,7 @@ function setDelayedMemoryReportPinned( normalizedId, pinned ) + && !syncDelayedMemoryStateToAvatar() ) { refreshRuntimeAvatar(); } @@ -942,7 +985,10 @@ function setDelayedMemoryReportAnchorFactIds( updatedReports[normalizedId]; renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { window.syncDelayedMemoryReportsToRuntime(); @@ -1038,12 +1084,8 @@ function removeLongTermFactIdFromDelayedMemoryReports( anchor_fact_ids: nextAnchorFactIds, facts_ids: nextFactIds, }; - updatedReport.pop( - "absorbed_fact_ids" - ); - updatedReport.pop( - "long_term_facts_ids" - ); + delete updatedReport.absorbed_fact_ids; + delete updatedReport.long_term_facts_ids; reports[reportId] = updatedReport; changed = true; }); @@ -1059,7 +1101,10 @@ function removeLongTermFactIdFromDelayedMemoryReports( if (runtimeMemoryDisplayMode === "delayed") { renderRuntimeMemorySnapshot(); } - refreshRuntimeAvatar(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } if (typeof window.syncDelayedMemoryReportsToRuntime === "function") { window.syncDelayedMemoryReportsToRuntime(); @@ -1100,7 +1145,10 @@ function appendDelayedMemoryReports( ); renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return nextReports; @@ -1128,63 +1176,6 @@ function buildDelayedMemoryReportsSignature( } -function buildDelayedMemoryAvatarLayoutSignature( - reports -) { - - const normalizedReports = - normalizeDelayedMemoryReports( - reports - ); - - return JSON.stringify( - Object.keys(normalizedReports) - .sort() - .map( - reportId => { - const report = - normalizedReports[reportId]; - - return [ - reportId, - report.title, - report.summary, - (report.anchor_fact_ids || []).join(","), - (report.facts_ids || []).join(","), - ]; - } - ) - ); - -} - -function syncDelayedMemoryPinsToAvatar( - reports -) { - - const normalizedReports = - normalizeDelayedMemoryReports( - reports - ); - let synced = true; - - Object.entries(normalizedReports).forEach( - ([reportId, report]) => { - if ( - !setDelayedMemoryPinnedOnAvatar( - reportId, - Boolean(report && report.pinned) - ) - ) { - synced = false; - } - } - ); - - return synced; - -} - function replaceDelayedMemoryReportsAndRender( reports ) { @@ -1194,14 +1185,6 @@ function replaceDelayedMemoryReportsAndRender( normalizeDelayedMemoryReports( reports ); - const currentAvatarLayoutSignature = - buildDelayedMemoryAvatarLayoutSignature( - currentReports - ); - const nextAvatarLayoutSignature = - buildDelayedMemoryAvatarLayoutSignature( - nextReports - ); if ( buildDelayedMemoryReportsSignature(currentReports) @@ -1215,10 +1198,7 @@ function replaceDelayedMemoryReportsAndRender( ); renderRuntimeMemorySnapshot(); - if ( - currentAvatarLayoutSignature === nextAvatarLayoutSignature - && syncDelayedMemoryPinsToAvatar(nextReports) - ) { + if (syncDelayedMemoryStateToAvatar()) { return readDelayedMemoryReports(); } @@ -1437,7 +1417,10 @@ window.JinRuntime.runtime = { clearActiveMemoryRecords(); renderRuntimeMemorySnapshot(); - refreshRuntimeAvatar(); + + if (!syncActiveMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } return records; }, diff --git a/ui/templates/index.html b/ui/templates/index.html index 7353dbbb..26cd9a82 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -502,13 +502,13 @@ - + - - + + From a9280d211c934165e1a5da09c7aaf5ccf2cf68fd Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:10:21 +0300 Subject: [PATCH 026/168] chore: memory sync logging - clean up L4 memory sync logging - remove dubious rule for delayed memory save --- contracts/save_delayed_memory.json | 3 +-- websocket/__init__.py | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index 7ce93afb..a77db058 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -31,8 +31,7 @@ "anchor_fact_ids are the most important/key facts among facts_ids. They remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", "facts_ids is the full set of L4 facts attached to the report, including every anchor_fact_ids entry. Non-anchor facts_ids are represented by the report and are hidden from while the report exists.", "Use exact ids copied from the [ id: ... ] suffixes. Do not list unrelated ids and never use PF pending ids.", - "anchor_fact_ids must be a subset of facts_ids. Leave both arrays empty when the report does not consolidate L4 facts.", - "DO NOT add any other text while making a report. Do not notify or acknowledge saving; immediately emit only the marker." + "anchor_fact_ids must be a subset of facts_ids. Leave both arrays empty when the report does not consolidate L4 facts." ], "close_tag": true } diff --git a/websocket/__init__.py b/websocket/__init__.py index 0a20bda0..6284a8c1 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -317,13 +317,10 @@ async def process_pending_requests(): {}, ), ) - await logger.log_system( - ( - "[WS] L4 memory store synced" - if applied - else "[WS] L4 memory store sync ignored" + if applied: + await logger.log_system( + "[WS] L4 memory store updated from browser profile" ) - ) await emit_l4_memory_update( context, change={ From a63de5bb0b1f705cd0854e7508d5fe024f8c5ffd Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:32:34 +0300 Subject: [PATCH 027/168] chore: runtime telemetry - expose stream validator repeat thresholds through config - add service usage fallback and focus brain context tab on submit - add avatar memory layer visibility toggle - tighten reasoning/action spacing and scene tint layering - update client contract tests and cache-busting versions --- config.example.py | 15 +++ runtime/stream.py | 6 - ..._chat_reasoning_spacing_client_contract.py | 66 ++++++++++ tests/test_config_loader.py | 5 + ..._memory_citation_layers_client_contract.py | 4 +- ..._runtime_avatar_l4_ring_client_contract.py | 42 ++++++- tests/test_runtime_panel_client_contract.py | 64 ++++++++++ tests/test_runtime_stream_tokens.py | 114 +++++++++++++++++- tests/test_stream_validator.py | 50 ++++++++ ui/static/css/base.css | 43 +++---- ui/static/css/chat.css | 4 + ui/static/css/runtime-avatar.css | 6 + ui/static/js/chat.js | 2 +- ui/static/js/runtime/runtime-avatar.js | 46 +++++++ ui/static/js/runtime/runtime-panel.js | 92 ++++++++++++-- ui/static/js/socket/input.js | 21 +++- ui/templates/index.html | 19 +-- utils/stream_validator.py | 104 +++++++++++++--- 18 files changed, 632 insertions(+), 71 deletions(-) create mode 100644 tests/test_chat_reasoning_spacing_client_contract.py create mode 100644 tests/test_runtime_panel_client_contract.py diff --git a/config.example.py b/config.example.py index 95bad9ba..8d706636 100644 --- a/config.example.py +++ b/config.example.py @@ -23,6 +23,21 @@ # Base64 adds overhead, so 64 MiB allows roughly 45 MiB source files. WEBSOCKET_MAX_MESSAGE_BYTES = 64 * 1024 * 1024 +# --------------------------------------------------------- +# STREAM VALIDATOR +# --------------------------------------------------------- + +# Repetition loop guards. Raise these values to make the validator softer. +# Set a repeat threshold to 0 to disable that specific loop check. +STREAM_VALIDATOR_WORD_WINDOW_SIZE = 30 +STREAM_VALIDATOR_MAX_REPEAT_WORDS = 8 +STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_SIZE = 6 +STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_REPETITIONS = 6 +STREAM_VALIDATOR_MAX_REPEAT_SENTENCES = 5 +STREAM_VALIDATOR_MAX_SENTENCE_LOOP_SEQUENCE_SIZE = 16 +STREAM_VALIDATOR_MIN_RECURRENT_SENTENCE_WORDS = 5 +STREAM_VALIDATOR_MIN_RECURRENT_SENTENCE_ALNUM = 20 + # --------------------------------------------------------- # TOKEN BUDGETING # --------------------------------------------------------- diff --git a/runtime/stream.py b/runtime/stream.py index 5ee72002..b262f3a0 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -294,9 +294,6 @@ def is_brain_context(self) -> bool: async def refresh_provider_token_usage(self): - if not self.is_brain_context(): - return - prompt_tokens = getattr( self.stream, "prompt_tokens", @@ -405,9 +402,6 @@ def calibrate_token_estimate(self) -> float: async def refresh_token_usage(self): - if not self.is_brain_context(): - return - prompt_tokens = getattr( self.stream, "prompt_tokens", diff --git a/tests/test_chat_reasoning_spacing_client_contract.py b/tests/test_chat_reasoning_spacing_client_contract.py new file mode 100644 index 00000000..ea87305d --- /dev/null +++ b/tests/test_chat_reasoning_spacing_client_contract.py @@ -0,0 +1,66 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BASE_CSS = ROOT / "ui" / "static" / "css" / "base.css" +CHAT_CSS = ROOT / "ui" / "static" / "css" / "chat.css" +CHAT_JS = ROOT / "ui" / "static" / "js" / "chat.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class ChatReasoningSpacingClientContractTests(unittest.TestCase): + + def test_reasoning_bubbles_use_compact_default_gap(self): + css = BASE_CSS.read_text(encoding="utf-8") + + self.assertIn("--chat-reasoning-gap: 0.28rem;", css) + self.assertIn("--chat-action-adjacent-gap: 0.82rem;", css) + self.assertIn( + "#chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child)\n" + "+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child)", + css, + ) + + def test_action_bubble_expands_reasoning_stack_only_at_action_boundary(self): + css = BASE_CSS.read_text(encoding="utf-8") + + self.assertIn( + "#chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child)\n" + "+ .jin-runtime-action-row", + css, + ) + self.assertIn( + "#chat-history > .jin-runtime-action-row\n" + "+ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child)", + css, + ) + + def test_stream_wrapper_uses_explicit_inner_gap_instead_of_tailwind_space(self): + css = CHAT_CSS.read_text(encoding="utf-8") + source = CHAT_JS.read_text(encoding="utf-8") + + self.assertIn( + ".jin-stream-wrapper > :not([hidden]) ~ :not([hidden])", + css, + ) + self.assertIn("margin-top: var(--chat-inner-gap) !important;", css) + self.assertIn( + '"jin-stream-wrapper mx-auto w-full max-w-4xl";', + source, + ) + self.assertNotIn( + "jin-stream-wrapper mx-auto w-full max-w-4xl space-y-3", + source, + ) + + def test_cache_versions_are_bumped_for_reasoning_spacing_assets(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("/static/css/base.css?v=reasoning-gap-1", source) + self.assertIn("/static/css/chat.css?v=reasoning-gap-1", source) + self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index c06b4ea2..d22373ad 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -52,6 +52,11 @@ def test_falls_back_to_example_config(self): 50, ) + self.assertEqual( + config.STREAM_VALIDATOR_MAX_REPEAT_SENTENCES, + 5, + ) + def test_env_overrides_fallback_config_values(self): root = Path(__file__).resolve().parents[1] diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index bb2e81a5..c0ba8616 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -114,9 +114,9 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-17", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-18", source) self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) - self.assertIn("/static/js/chat.js?v=memory-reference-sync-2", source) + self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) if __name__ == "__main__": diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 57652b4e..1478dd8c 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -9,6 +9,7 @@ ) RUNTIME_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" AVATAR_CSS = ROOT / "ui" / "static" / "css" / "runtime-avatar.css" +INPUT_JS = ROOT / "ui" / "static" / "js" / "socket" / "input.js" INDEX_HTML = ROOT / "ui" / "templates" / "index.html" @@ -140,15 +141,52 @@ def test_open_delayed_report_keeps_avatar_fact_links_highlighted(self): source, ) + def test_avatar_core_button_toggles_memory_layers_without_refresh(self): + index_source = INDEX_HTML.read_text(encoding="utf-8") + avatar_source = AVATAR_JS.read_text(encoding="utf-8") + input_source = INPUT_JS.read_text(encoding="utf-8") + css_source = AVATAR_CSS.read_text(encoding="utf-8") + + self.assertIn('title="hide"', index_source) + self.assertIn('aria-label="hide"', index_source) + self.assertIn('alt="hide"', index_source) + self.assertIn( + "function toggleRuntimeAvatarMemoryLayers()", + input_source, + ) + self.assertIn( + "avatar.toggleMemoryLayers();", + input_source, + ) + self.assertIn( + "function setMemoryLayersHidden(hidden)", + avatar_source, + ) + self.assertIn("toggleMemoryLayers,", avatar_source) + self.assertIn( + ".jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-ring", + css_source, + ) + + click_start = input_source.index("if (factCheckTrigger) {") + click_end = input_source.index("chatForm.addEventListener", click_start) + click_block = input_source[click_start:click_end] + + self.assertNotIn("avatar.refresh", click_block) + def test_avatar_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/css/runtime-avatar.css?v=memory-rings-6", + "/static/css/runtime-avatar.css?v=memory-rings-7", + source, + ) + self.assertIn( + "/static/js/runtime/runtime-avatar.js?v=memory-rings-18", source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-17", + "/static/js/socket/input.js?v=socket-input-memory-layer-toggle-brain-tab-1", source, ) diff --git a/tests/test_runtime_panel_client_contract.py b/tests/test_runtime_panel_client_contract.py new file mode 100644 index 00000000..c57c734d --- /dev/null +++ b/tests/test_runtime_panel_client_contract.py @@ -0,0 +1,64 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_PANEL_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-panel.js" +SOCKET_INPUT_JS = ROOT / "ui" / "static" / "js" / "socket" / "input.js" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class RuntimePanelClientContractTests(unittest.TestCase): + + def test_service_tab_can_display_summarizer_usage_when_service_is_empty(self): + source = RUNTIME_PANEL_JS.read_text(encoding="utf-8") + + self.assertIn( + "function buildServiceRuntimeUsageFallback(", + source, + ) + self.assertIn( + "runtimeHasUsage(serviceRuntime)", + source, + ) + self.assertIn( + "runtimeHasUsage(summarizerRuntime)", + source, + ) + self.assertIn( + "return getServiceRuntime();", + source, + ) + + def test_submit_focuses_brain_context_tab(self): + panel_source = RUNTIME_PANEL_JS.read_text(encoding="utf-8") + input_source = SOCKET_INPUT_JS.read_text(encoding="utf-8") + + self.assertIn( + "function focusBrainContextTab()", + panel_source, + ) + self.assertIn( + "window.focusBrainContextTab = function ()", + panel_source, + ) + self.assertIn( + "window.focusBrainContextTab();", + input_source, + ) + + def test_cache_versions_are_bumped(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn( + "/static/js/runtime/runtime-panel.js?v=runtime-panel-service-usage-1", + source, + ) + self.assertIn( + "/static/js/socket/input.js?v=socket-input-memory-layer-toggle-brain-tab-1", + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index 0ed774c5..b38bdd47 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -698,6 +698,93 @@ async def test_runtime_context_counter_grows_during_stream(self): status=original_state["status"], ) + async def test_service_context_counter_grows_during_stream(self): + + runtime_id = settings.SERVICE_MODEL_UID + original_state = runtime_state.get_runtime_state( + runtime_id + ) + + context = SimpleNamespace( + websocket=FakeWebSocket(), + logger=FakeLogger(), + emitter=FakeEmitter(), + runtime_action_events=[], + runtime_usage_events=[], + ) + + stream = RuntimeStream( + context=context, + runtime_id=runtime_id, + role="service", + context_window=( + settings.SERVICE_CONTEXT_WINDOW + ), + log_method=( + context.logger.log_service + ), + context_snapshot={ + "context_role": "service", + "system_prompt": "service system prompt", + "user_prompt": "service payload", + }, + ) + + try: + await stream.run( + fake_generator() + ) + + service_state = runtime_state.get_runtime_state( + runtime_id + ) + + self.assertEqual( + service_state["used_tokens"], + 42, + ) + self.assertEqual( + service_state["context_tokens"], + 12, + ) + self.assertEqual( + service_state["total_tokens"], + 42, + ) + + telemetry_counts = [ + event["runtime"][runtime_id]["used_tokens"] + for event in context.emitter.events + if event.get("type") == "telemetry" + ] + self.assertGreaterEqual( + len(telemetry_counts), + 2, + ) + self.assertEqual( + telemetry_counts[-1], + 42, + ) + self.assertEqual( + context.runtime_usage_events[-1]["role"], + "service", + ) + self.assertEqual( + context.runtime_usage_events[-1]["kind"], + "service", + ) + + finally: + runtime_state.update_runtime_state( + runtime_id=runtime_id, + used_tokens=original_state["used_tokens"], + context_tokens=original_state["context_tokens"], + total_tokens=original_state["total_tokens"], + max_tokens=original_state["max_tokens"], + last_error=original_state["last_error"], + status=original_state["status"], + ) + async def test_runtime_counter_keeps_estimated_total_when_provider_usage_has_no_total(self): runtime_id = settings.SERVICE_MODEL_UID @@ -1010,7 +1097,7 @@ async def test_thinking_sentence_loop_interrupts_and_arms_recovery(self): ) - async def test_non_brain_stream_does_not_update_context_counter(self): + async def test_non_brain_stream_updates_context_counter(self): runtime_id = settings.SERVICE_MODEL_UID original_state = runtime_state.get_runtime_state( @@ -1049,12 +1136,29 @@ async def test_non_brain_stream_does_not_update_context_counter(self): self.assertEqual( service_state["used_tokens"], - original_state["used_tokens"], + 42, + ) + self.assertEqual( + service_state["context_tokens"], + 12, + ) + self.assertEqual( + service_state["total_tokens"], + 42, ) + telemetry_counts = [ + event["runtime"][runtime_id]["used_tokens"] + for event in context.emitter.events + if event.get("type") == "telemetry" + ] + self.assertGreaterEqual( + len(telemetry_counts), + 2, + ) self.assertEqual( - context.emitter.events, - [], + telemetry_counts[-1], + 42, ) self.assertEqual( @@ -1076,6 +1180,8 @@ async def test_non_brain_stream_does_not_update_context_counter(self): runtime_state.update_runtime_state( runtime_id=runtime_id, used_tokens=original_state["used_tokens"], + context_tokens=original_state["context_tokens"], + total_tokens=original_state["total_tokens"], max_tokens=original_state["max_tokens"], last_error=original_state["last_error"], status=original_state["status"], diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index a050ed8f..659b926d 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -6,6 +6,8 @@ str(Path(__file__).resolve().parents[1]), ) +import utils.stream_validator as stream_validator_module + from utils.stream_validator import ( MAX_REPEAT_SENTENCES, StreamValidator, @@ -161,6 +163,54 @@ def test_stream_validator_stops_recurrent_sentence_inside_mixed_loop(): ) +def test_stream_validator_sentence_repeat_threshold_can_be_raised(monkeypatch): + threshold = 7 + + monkeypatch.setattr( + stream_validator_module, + "MAX_REPEAT_SENTENCES", + threshold, + ) + + validator = StreamValidator() + repeated = "Actually, I'll just do the task.\n" + + for repeat_index in range(threshold): + clean, is_valid = validator.filter_chunk( + repeated + ) + + if repeat_index < threshold - 1: + assert clean == repeated + assert is_valid + continue + + assert clean == "" + assert not is_valid + + +def test_stream_validator_sentence_repeat_threshold_zero_disables_check(monkeypatch): + monkeypatch.setattr( + stream_validator_module, + "MAX_REPEAT_SENTENCES", + 0, + ) + + validator = StreamValidator() + repeated = "Actually, I'll just do the task.\n" + + text = collect( + validator, + [ + repeated + for _ in range(10) + ], + ) + + assert text == repeated * 10 + assert validator.last_failure_reason is None + + def test_stream_validator_stops_repeated_sentence_sequence_with_markers(): validator = StreamValidator() diff --git a/ui/static/css/base.css b/ui/static/css/base.css index a911d728..0597bc45 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -33,19 +33,20 @@ --chat-min-width: 200px; --scene-shade-alpha: 0; --jin-color: #1f4f8f; - --scene-base-color: #0f1b2c; - --scene-jin-tint-alpha: 0.40; - --scene-context-tint-boost: 0; + --scene-base-color: #122540; + --scene-jin-tint-alpha: 0.048; --scene-layer-base: 0; - --scene-layer-context: 1; - --scene-layer-search: 2; - --scene-layer-tint: 3; + --scene-layer-tint: 1; + --scene-layer-context: 2; + --scene-layer-search: 3; --scene-layer-shade: 4; /* Вертикальный ритм чата: добавляет немного воздуха между ходами. */ --chat-turn-gap: 2.05rem; --chat-inner-gap: 0.85rem; + --chat-reasoning-gap: 0.28rem; + --chat-action-adjacent-gap: 0.82rem; --chat-question-gap: 2.55rem; --chat-history-edge-gap: 1.9rem; } @@ -101,31 +102,15 @@ main.panel-startup-collapse-active { } main.scene-clutter-middle #scene-clutter-overlay, -main.scene-cluttered #scene-clutter-overlay, main.scene-cluttered #scene-cluttered-overlay, main.scene-searching #scene-search-overlay { opacity: 1; } -main.scene-clutter-middle { - --scene-context-tint-boost: 0.06; -} - -main.scene-cluttered { - --scene-context-tint-boost: 0.12; -} - -main.scene-searching { - --scene-context-tint-boost: 0.08; -} - #scene-jin-tint { z-index: var(--scene-layer-tint); background-color: var(--jin-color); - opacity: min( - 0.34, - calc(var(--scene-jin-tint-alpha) + var(--scene-context-tint-boost)) - ); + opacity: var(--scene-jin-tint-alpha, 0.048); transition: background-color 0.22s ease, opacity 0.22s ease; @@ -478,11 +463,23 @@ main.panels-collapsed-2 #scene-shade { margin-top: var(--chat-question-gap) !important; } +#chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child) ++ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child) { + margin-top: var(--chat-reasoning-gap) !important; +} + #chat-history > :not(.jin-runtime-action-row) + .jin-runtime-action-row { /* Первый action-бабл держим рядом с ответом JIN, а не как отдельный ход. */ margin-top: 0.45rem !important; } +#chat-history > .jin-stream-wrapper:has(> .jin-think-wrapper:last-child) ++ .jin-runtime-action-row, +#chat-history > .jin-runtime-action-row ++ .jin-stream-wrapper:has(> .jin-think-wrapper:first-child) { + margin-top: var(--chat-action-adjacent-gap) !important; +} + #chat-history > .jin-runtime-action-row + .jin-runtime-action-row { /* Последовательные action-баблы визуально образуют один компактный стек. */ margin-top: 0.1rem !important; diff --git a/ui/static/css/chat.css b/ui/static/css/chat.css index 77d76c0a..c9e23395 100644 --- a/ui/static/css/chat.css +++ b/ui/static/css/chat.css @@ -46,6 +46,10 @@ position: relative; } +.jin-stream-wrapper > :not([hidden]) ~ :not([hidden]) { + margin-top: var(--chat-inner-gap) !important; +} + #chat-history .jin-stream-wrapper + .jin-stream-wrapper, #chat-history .jin-stream-wrapper + .jin-message-shell, #chat-history .jin-message-shell + .jin-stream-wrapper, diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index a94ab639..df4a7e4a 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -210,6 +210,12 @@ opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); } +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-ring, +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-dash { + opacity: 0; + pointer-events: none; +} + .jin-avatar-memory-dash path { transition: stroke-opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index 546d7107..f142de43 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -969,7 +969,7 @@ function createStreamGroup( document.createElement("div"); wrapper.className = - "jin-stream-wrapper mx-auto w-full max-w-4xl space-y-3"; + "jin-stream-wrapper mx-auto w-full max-w-4xl"; // THINKING diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 2491c89e..a79fafa7 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -54,6 +54,7 @@ Object.freeze(["l4", "delayed", "active"]); const SNAPSHOT_GLOW_CLEAR_DELAY_MS = 360; const CENTER_COLOR_STEP_MS = 120; + const MEMORY_LAYERS_HIDDEN_CLASS = "is-memory-layers-hidden"; // 0 = no scene recolor, 1 = current full-strength scene recolor. const JIN_SCENE_COLOR_INTENSITY = 0.40; @@ -2618,6 +2619,47 @@ return true; } + function syncMemoryLayersToggleLabel(hidden) { + if (!factCheckTrigger) { + return; + } + + const memoryLayersHidden = + hidden === undefined + ? avatarRoot.classList.contains(MEMORY_LAYERS_HIDDEN_CLASS) + : Boolean(hidden); + const label = + memoryLayersHidden + ? "show" + : "hide"; + + factCheckTrigger.setAttribute("title", label); + factCheckTrigger.setAttribute("aria-label", label); + factCheckTrigger.setAttribute("alt", label); + factCheckTrigger.dataset.memoryLayersHidden = + memoryLayersHidden ? "true" : "false"; + } + + function setMemoryLayersHidden(hidden) { + const nextHidden = Boolean(hidden); + + avatarRoot.classList.toggle( + MEMORY_LAYERS_HIDDEN_CLASS, + nextHidden + ); + avatarRoot.dataset.memoryLayersHidden = + nextHidden ? "true" : "false"; + syncMemoryLayersToggleLabel(nextHidden); + + return nextHidden; + } + + function toggleMemoryLayers() { + return setMemoryLayersHidden( + !avatarRoot.classList.contains(MEMORY_LAYERS_HIDDEN_CLASS) + ); + } + function reinitializeAvatar() { avatarRefreshNonce += 1; snapshotRenderSequence += 1; @@ -2835,6 +2877,8 @@ } if (factCheckTrigger) { + syncMemoryLayersToggleLabel(); + factCheckTrigger.addEventListener("mousedown", (event) => { event.stopPropagation(); }); @@ -2851,6 +2895,8 @@ render: renderAvatar, refresh: reinitializeAvatar, setCenterColor, + setMemoryLayersHidden, + toggleMemoryLayers, setDelayedMemoryPinned: setDelayedMemoryDashPinned, syncActiveMemoryState, syncDelayedMemoryState, diff --git a/ui/static/js/runtime/runtime-panel.js b/ui/static/js/runtime/runtime-panel.js index 35bdba68..7207768d 100644 --- a/ui/static/js/runtime/runtime-panel.js +++ b/ui/static/js/runtime/runtime-panel.js @@ -152,6 +152,73 @@ } + function getRuntimeUsageAmount(runtime) { + + if (!runtime) { + return 0; + } + + return Math.max( + Number(runtime.used_tokens || 0), + Number(runtime.context_tokens || 0), + Number(runtime.total_tokens || 0) + ); + + } + + + function runtimeHasUsage(runtime) { + + return getRuntimeUsageAmount( + runtime + ) > 0; + + } + + + function buildServiceRuntimeUsageFallback( + serviceRuntime, + summarizerRuntime + ) { + + if ( + !summarizerRuntime + || runtimeHasUsage(serviceRuntime) + || !runtimeHasUsage(summarizerRuntime) + ) { + return serviceRuntime; + } + + return { + ...summarizerRuntime, + label: "service", + model: ( + serviceRuntime + && serviceRuntime.model + ) + ? serviceRuntime.model + : summarizerRuntime.model, + status: ( + serviceRuntime + && serviceRuntime.status + ) + ? serviceRuntime.status + : summarizerRuntime.status, + }; + + } + + + function getServiceRuntime() { + + return buildServiceRuntimeUsageFallback( + getRuntimeByLabel("service"), + getSummarizerRuntime() + ); + + } + + function getBrainRuntime() { return ( @@ -181,7 +248,7 @@ return getBrainRuntime(); } - return getRuntimeByLabel("service"); + return getServiceRuntime(); } @@ -1203,9 +1270,7 @@ function renderLiveRuntimeTelemetry() { const serviceRuntime = - getRuntimeByLabel( - "service" - ); + getServiceRuntime(); const brainRuntime = getBrainRuntime(); @@ -1372,6 +1437,15 @@ } + function focusBrainContextTab() { + + selectContextTab( + "brain" + ); + + } + + function setUseServiceAsBrain(enabled) { runtimePanelState.useServiceAsBrain = @@ -1398,9 +1472,7 @@ runtimeConfig || {}; const serviceRuntime = - getRuntimeByLabel( - "service" - ); + getServiceRuntime(); const brainRuntime = getBrainRuntime(); @@ -1575,6 +1647,7 @@ init, findRuntimeByLabel, getRuntimeByLabel, + getServiceRuntime, getBrainRuntime, getSummarizerRuntime, getSelectedRuntime, @@ -1585,6 +1658,7 @@ setUseServiceAsBrain, setRuntimeStatusSnapshot, setRuntimeConfigSnapshot, + focusBrainContextTab, }; window.JinRuntime.panel = api; @@ -1613,4 +1687,8 @@ return api.setRuntimeConfigSnapshot(runtimeConfig); }; + window.focusBrainContextTab = function () { + return api.focusBrainContextTab(); + }; + }()); diff --git a/ui/static/js/socket/input.js b/ui/static/js/socket/input.js index 3e6799b8..b51edab2 100644 --- a/ui/static/js/socket/input.js +++ b/ui/static/js/socket/input.js @@ -155,14 +155,25 @@ function allModelRuntimesOffline() { } if (factCheckTrigger) { + function toggleRuntimeAvatarMemoryLayers() { + const avatar = + window.JinRuntime + && window.JinRuntime.avatar; + + if ( + avatar + && typeof avatar.toggleMemoryLayers === "function" + ) { + avatar.toggleMemoryLayers(); + } + } + factCheckTrigger.addEventListener( "click", (event) => { event.preventDefault(); event.stopPropagation(); - if (window.JinRuntime && window.JinRuntime.avatar && typeof window.JinRuntime.avatar.refresh === "function") { - window.JinRuntime.avatar.refresh(); - } + toggleRuntimeAvatarMemoryLayers(); } ); } @@ -227,6 +238,10 @@ chatForm.addEventListener( } + if (window.focusBrainContextTab) { + window.focusBrainContextTab(); + } + const attachments = window.prepareJinAttachments ? await window.prepareJinAttachments() diff --git a/ui/templates/index.html b/ui/templates/index.html index 26cd9a82..83541a87 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,14 +10,14 @@ - - + + - + @@ -318,8 +318,9 @@ @@ -505,10 +506,10 @@ - + - + @@ -519,7 +520,7 @@ - + @@ -527,7 +528,7 @@ - + diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 74322b96..80b1b9cf 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -1,6 +1,7 @@ from contracts.rules_assembler import ( get_stream_validator_excluded_markers, ) +from config_loader import config # --------------------------------------------------------- # STREAM VALIDATOR @@ -38,18 +39,81 @@ # VALIDATION THRESHOLDS # --------------------------------------------------------- -WORD_WINDOW_SIZE = 30 -MAX_REPEAT_WORDS = 8 -MAX_REPEAT_WORD_SEQUENCE_SIZE = 6 -MAX_REPEAT_WORD_SEQUENCE_REPETITIONS = 6 -MAX_REPEAT_SENTENCES = 5 -MAX_SENTENCE_LOOP_SEQUENCE_SIZE = 16 +def configured_int( + name: str, + default: int, + *, + minimum: int = 1, + zero_disables: bool = False, +) -> int: + + try: + value = int( + getattr( + config, + name, + default, + ) + ) + except (TypeError, ValueError): + value = default + + if zero_disables and value <= 0: + return 0 + + return max( + minimum, + value, + ) + + +WORD_WINDOW_SIZE = configured_int( + "STREAM_VALIDATOR_WORD_WINDOW_SIZE", + 30, +) +MAX_REPEAT_WORDS = configured_int( + "STREAM_VALIDATOR_MAX_REPEAT_WORDS", + 8, + minimum=2, + zero_disables=True, +) +MAX_REPEAT_WORD_SEQUENCE_SIZE = configured_int( + "STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_SIZE", + 6, + minimum=2, + zero_disables=True, +) +MAX_REPEAT_WORD_SEQUENCE_REPETITIONS = configured_int( + "STREAM_VALIDATOR_MAX_REPEAT_WORD_SEQUENCE_REPETITIONS", + 6, + minimum=2, + zero_disables=True, +) +MAX_REPEAT_SENTENCES = configured_int( + "STREAM_VALIDATOR_MAX_REPEAT_SENTENCES", + 5, + minimum=2, + zero_disables=True, +) +MAX_SENTENCE_LOOP_SEQUENCE_SIZE = configured_int( + "STREAM_VALIDATOR_MAX_SENTENCE_LOOP_SEQUENCE_SIZE", + 16, +) MAX_RECURRENT_SENTENCE_HISTORY_SIZE = ( MAX_SENTENCE_LOOP_SEQUENCE_SIZE - * MAX_REPEAT_SENTENCES + * max( + 1, + MAX_REPEAT_SENTENCES, + ) +) +MIN_RECURRENT_SENTENCE_WORDS = configured_int( + "STREAM_VALIDATOR_MIN_RECURRENT_SENTENCE_WORDS", + 5, +) +MIN_RECURRENT_SENTENCE_ALNUM = configured_int( + "STREAM_VALIDATOR_MIN_RECURRENT_SENTENCE_ALNUM", + 20, ) -MIN_RECURRENT_SENTENCE_WORDS = 5 -MIN_RECURRENT_SENTENCE_ALNUM = 20 SENTENCE_HISTORY_SIZE = ( MAX_SENTENCE_LOOP_SEQUENCE_SIZE + 1 @@ -583,7 +647,10 @@ def validate_word_loops( self.recent_words[-WORD_WINDOW_SIZE:] ) - if len(self.recent_words) >= MAX_REPEAT_WORDS: + if ( + MAX_REPEAT_WORDS > 0 + and len(self.recent_words) >= MAX_REPEAT_WORDS + ): last_word = self.recent_words[-1] @@ -611,10 +678,13 @@ def validate_word_loops( return False - max_sequence_size = min( - MAX_REPEAT_WORD_SEQUENCE_SIZE, - len(self.recent_words) // 2, - ) + max_sequence_size = 0 + + if MAX_REPEAT_WORD_SEQUENCE_REPETITIONS > 0: + max_sequence_size = min( + MAX_REPEAT_WORD_SEQUENCE_SIZE, + len(self.recent_words) // 2, + ) for sequence_size in range( 2, @@ -882,6 +952,9 @@ def validate_recurrent_sentence_loop( if not sentence_key: return True + if MAX_REPEAT_SENTENCES <= 0: + return True + matching_sentences = [ history_sentence for history_sentence in ( @@ -948,6 +1021,9 @@ def validate_sentence_sequence_loop( ): del self.recurrent_sentence_history[0] + if MAX_REPEAT_SENTENCES <= 0: + return True + max_sequence_size = min( MAX_SENTENCE_LOOP_SEQUENCE_SIZE, len(self.sentence_history) - 1, From e037be1a7eb1344d882f8cf55897c0ae96bf2f7b Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:54:11 +0300 Subject: [PATCH 028/168] chore: memory flow cleanup - add delayed memory delete/restore sync - keep loaded delayed facts visible in l4 context - persist submitted answer rating visuals - make active memory autonomous and hide reasoning fallback --- contracts/save_active_memory.json | 11 +- rules/runtime.py | 11 +- runtime/L1_memory.py | 4 - runtime/L2_memory.py | 3 + runtime/L4_memory.py | 174 ++++++++++- runtime/memory_common.py | 33 +-- tests/test_answer_rating_client_contract.py | 87 ++++++ tests/test_behavior_contract.py | 25 ++ tests/test_brain_runtime_actions.py | 31 ++ tests/test_chat_runtime_marker_ui.py | 2 +- tests/test_delayed_memory_client_contract.py | 88 ++++++ tests/test_delayed_memory_file_store.py | 93 ++++++ ...ayed_memory_pinned_glow_client_contract.py | 4 +- tests/test_l2_memory.py | 252 ++++++++++++++++ tests/test_l4_logger_cards_client_contract.py | 16 +- tests/test_l4_memory.py | 58 ++++ ..._memory_citation_layers_client_contract.py | 6 +- ...t_memory_reference_sync_client_contract.py | 50 +++- ui/static/css/base.css | 6 +- ui/static/css/chat-rating.css | 16 +- ui/static/css/runtime-memory.css | 11 + ui/static/css/theme-win95.css | 6 +- ui/static/js/answer-rating.js | 40 ++- ui/static/js/logger/log-entries.js | 201 +++++++++++++ ui/static/js/runtime/runtime-feedback.js | 29 +- ui/static/js/runtime/runtime-l4-memory.js | 31 +- ui/static/js/runtime/runtime-memory-view.js | 64 +++++ ui/static/js/runtime/runtime.js | 271 ++++++++++++++++++ ui/static/js/socket/delayed-memory.js | 45 ++- ui/static/js/socket/runtime-actions.js | 27 ++ ui/templates/index.html | 24 +- utils/delayed_memory_file_store.py | 56 ++++ websocket/__init__.py | 12 +- websocket/bootstrap.py | 92 +++++- 34 files changed, 1755 insertions(+), 124 deletions(-) create mode 100644 tests/test_answer_rating_client_contract.py diff --git a/contracts/save_active_memory.json b/contracts/save_active_memory.json index 58d30653..7b2b6d3f 100644 --- a/contracts/save_active_memory.json +++ b/contracts/save_active_memory.json @@ -1,7 +1,7 @@ { "save_active_memory": { "version": 1, - "description": "Allows the model to save active memory.", + "description": "Allows the model to autonomously save active memory.", "runtime_action": "SAVE_ACTIVE_MEMORY", "private_marker": "", "triggers": [], @@ -10,9 +10,12 @@ "emit_followup": false }, "rules": [ - "Use SAVE_ACTIVE_MEMORY action marker when user asks to remember, remind, track, or keep a pending live-session condition.", - "CONDITIONS is a placeholder word; replace placeholder with description, value, or conditions.", - "When JIN explicitly derives a live rule from the current context and needs to remember it, fulfill the conditions and emit a marker." + "Use SAVE_ACTIVE_MEMORY when a clear, concrete unresolved intention, promise, condition, reminder, live rule, or future checkpoint appears and keeping it pending would help the current or near-future conversation.", + "Do not wait for explicit confirmation when the save candidate is clear; active memory is an autonomous runtime action.", + "Ask before saving only when the candidate is ambiguous, sensitive, broad, identity-like, or better suited to long-term memory or delayed memory.", + "CONDITIONS is a placeholder word; replace it with the concrete pending item to preserve.", + "After emitting the marker, briefly notify the user in natural text that the active memory was saved; never expose marker syntax in visible text.", + "Do not save trivial exchanges, unstable ideas, identity anchors, broad user preferences, core project rules, or facts that should remain useful across unrelated topics as active memory." ], "close_tag": false } diff --git a/rules/runtime.py b/rules/runtime.py index cffd6649..15667a8d 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -57,16 +57,19 @@ PROPOSAL_RULES = ( "MEMORY AND SESSION PROPOSALS:\n" - "A proposal is optional user-facing text, not a runtime action. Never emit a save or memory marker during proposal until the user clearly accepts it.\n" + "Use active memory autonomously when a clear concrete unresolved intention, condition, reminder, promise, or future checkpoint appears and would help ongoing work.\n" + "Active memory does not require explicit confirmation. Emit the save active memory action in the same answer and mention the saved item briefly in natural text.\n" + "Ask before using active memory only when the candidate is ambiguous, sensitive, broad, identity-like, or better suited to long-term memory or delayed memory.\n" + "Do not store trivial exchanges, unstable ideas, identity anchors, broad user preferences, core project rules, or facts that remain useful across unrelated topics as active memory.\n" + "A proposal is optional user-facing text, not a runtime action. Use proposals for save-session and delayed-memory decisions that need user acceptance before any save or update marker.\n" "Offer only after the current request is answered and a natural boundary with clear durable value has appeared. Never interrupt active work, a runtime sequence, or a follow-up tick.\n" "Choose only one best-fit proposal. Do not present a menu of storage types, expose marker names, or explain internal mechanics.\n" "Propose saving the session when the conversation has reached a stable checkpoint worth restoring later, especially after a substantial task, decision, or coherent phase is complete.\n" - "Propose active memory when the user introduces a concrete unresolved intention, condition, reminder, promise, or future checkpoint that would be useful to keep pending.\n" "Propose a delayed memory report when a substantial reusable result, analysis, design, or report has crystallized and may be useful to append or continue in another context later.\n" "When contains a coherent cluster of detailed facts that no longer needs to remain always-on, propose consolidating that cluster into one delayed memory report. State which fact keys and ids would be grouped, what the report would preserve, and why moving them would reduce context noise.\n" "When new long-term facts belong to an existing delayed memory report, propose extending that report instead of creating a duplicate. Name the report and list the exact fact keys and ids that would be appended.\n" - "Do not propose moving identity anchors, broad user preferences, core project rules, or facts that remain useful across unrelated topics. Never emit a save or update marker until the user explicitly accepts the proposal.\n" - "Phrase the proposal as one short natural sentence describing what would be preserved and why it may help. Ask for confirmation and never imply that anything has already been saved.\n" + "Do not propose moving identity anchors, broad user preferences, core project rules, or facts that remain useful across unrelated topics.\n" + "Phrase save-session and delayed-memory proposals as one short natural sentence describing what would be preserved and why it may help. Ask for confirmation and never imply that anything has already been saved.\n" "Do not propose after trivial exchanges, while the idea is still unstable, or merely because the topic changed. Do not repeat a declined or ignored proposal unless meaningful new state has appeared.\n" ) diff --git a/runtime/L1_memory.py b/runtime/L1_memory.py index 86ca9ac8..71fb574a 100644 --- a/runtime/L1_memory.py +++ b/runtime/L1_memory.py @@ -661,7 +661,6 @@ async def summarize_runtime_memory( updated_memory = extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) updated_memory = normalize_compound_runtime_memory_lines( updated_memory @@ -670,7 +669,6 @@ async def summarize_runtime_memory( build_runtime_summarizer_response_details( response, extracted_memory=updated_memory, - allow_reasoning_fallback=False, ) ) updated_memory = remove_runtime_response_feedback_text( @@ -875,7 +873,6 @@ async def summarize_runtime_memory_pending_turns( updated_memory = extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) updated_memory = normalize_compound_runtime_memory_lines( updated_memory @@ -884,7 +881,6 @@ async def summarize_runtime_memory_pending_turns( build_runtime_summarizer_response_details( response, extracted_memory=updated_memory, - allow_reasoning_fallback=False, ) ) updated_memory = remove_runtime_response_feedback_text( diff --git a/runtime/L2_memory.py b/runtime/L2_memory.py index fa641bfd..1aeab199 100644 --- a/runtime/L2_memory.py +++ b/runtime/L2_memory.py @@ -309,6 +309,9 @@ async def maybe_summarize_runtime_l2_memory( response ) + if not updated_l2_memory.strip(): + updated_l2_memory = current_l2_memory + skip_reason = None if is_runtime_memory_response_truncated(response): diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index 57dcef43..ab5ff021 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -401,6 +401,157 @@ def set_runtime_l4_state(context, store) -> dict: return merged +def get_loaded_delayed_memory_reports( + context, +) -> dict: + + from utils.brain_client_utils import ( + get_appended_delayed_memory_report, + ) + + return get_appended_delayed_memory_report( + context + ) + + +def collect_loaded_delayed_memory_fact_ids( + context, +) -> set[str]: + + return collect_long_term_fact_ids_from_reports( + get_loaded_delayed_memory_reports( + context + ) + ) + + +def collect_loaded_delayed_memory_fact_report_ids( + context, +) -> dict[str, list[str]]: + + reports = get_loaded_delayed_memory_reports( + context + ) + + if not isinstance( + reports, + dict, + ): + return {} + + report_ids_by_fact: dict[str, list[str]] = {} + + for report_id, report in reports.items(): + if not isinstance( + report, + dict, + ): + continue + + normalized_report_id = str( + report_id + or report.get( + "id", + "", + ) + or "" + ).strip().casefold() + + if not normalized_report_id: + continue + + _anchor_ids, fact_ids = normalize_delayed_memory_fact_ids( + report.get( + "anchor_fact_ids", + [], + ), + report.get( + "facts_ids", + [], + ), + legacy_absorbed_fact_ids=report.get( + "absorbed_fact_ids", + [], + ), + legacy_long_term_fact_ids=report.get( + "long_term_facts_ids", + [], + ), + ) + + for fact_id in fact_ids: + report_ids_by_fact.setdefault( + fact_id, + [], + ) + if normalized_report_id not in report_ids_by_fact[fact_id]: + report_ids_by_fact[fact_id].append( + normalized_report_id + ) + + return report_ids_by_fact + + +def merge_l4_fact_report_id_maps( + *maps, +) -> dict[str, list[str]]: + + merged: dict[str, list[str]] = {} + + for mapping in maps: + if not isinstance( + mapping, + dict, + ): + continue + + for fact_id, report_ids in mapping.items(): + normalized_fact_id = str( + fact_id + or "" + ).strip().upper() + + if not normalized_fact_id: + continue + + if isinstance( + report_ids, + str, + ): + candidates = [ + report_ids, + ] + elif isinstance( + report_ids, + list, + ): + candidates = report_ids + else: + candidates = [] + + merged.setdefault( + normalized_fact_id, + [], + ) + + for report_id in candidates: + normalized_report_id = str( + report_id + or "" + ).strip().casefold() + + if ( + normalized_report_id + and normalized_report_id + not in merged[normalized_fact_id] + ): + merged[normalized_fact_id].append( + normalized_report_id + ) + + return merged + + def refresh_runtime_l4_archived_fact_ids( context, ) -> set[str]: @@ -421,6 +572,14 @@ def refresh_runtime_l4_archived_fact_ids( ) ) fact_ids.difference_update(anchor_fact_ids) + # A loaded delayed-memory report is in the prompt as an active context + # attachment, so every L4 fact represented by that report becomes visible + # again for the duration of the load. + fact_ids.difference_update( + collect_loaded_delayed_memory_fact_ids( + context + ) + ) context.runtime_l4_archived_fact_ids = set( fact_ids ) @@ -893,7 +1052,6 @@ async def ask_l4_model( response_text = extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) if response_text: @@ -985,7 +1143,6 @@ async def run_l4_extraction_phase( payload = extract_l4_json_payload( extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) ) if payload is None: @@ -1168,7 +1325,6 @@ async def run_l4_merge_phase(*, context, service_client) -> dict: payload = extract_l4_json_payload( extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) ) if payload is None: @@ -1331,7 +1487,6 @@ async def run_l4_jin_note( payload = extract_l4_json_payload( extract_runtime_memory_text( response, - allow_reasoning_fallback=False, ) ) if payload is None: @@ -1596,6 +1751,15 @@ def build_runtime_l4_memory_context(*, context) -> str: anchor_report_ids_by_fact_id = collect_anchor_fact_report_ids( reports ) + loaded_report_ids_by_fact_id = ( + collect_loaded_delayed_memory_fact_report_ids( + context + ) + ) + report_ids_by_fact_id = merge_l4_fact_report_id_maps( + anchor_report_ids_by_fact_id, + loaded_report_ids_by_fact_id, + ) active_facts = [ fact for fact in store.get("facts") or [] @@ -1612,7 +1776,7 @@ def build_runtime_l4_memory_context(*, context) -> str: continue report_ids = _get_l4_fact_anchor_report_ids( fact, - anchor_report_ids_by_fact_id, + report_ids_by_fact_id, ) if report_ids: delayed_memory_ids_by_fact_id[fact_id] = report_ids diff --git a/runtime/memory_common.py b/runtime/memory_common.py index 8f0a6bf0..0cea2d09 100644 --- a/runtime/memory_common.py +++ b/runtime/memory_common.py @@ -458,22 +458,12 @@ async def log_active_memory_event( def extract_runtime_memory_text( response: dict, - *, - allow_reasoning_fallback: bool = True, ) -> str: text = ResponseExtractor.extract_content_text( response ) - if ( - not text - and allow_reasoning_fallback - ): - text = ResponseExtractor.extract_reasoning_text( - response - ) - return text.strip() @@ -844,7 +834,6 @@ def build_runtime_summarizer_response_details( response: dict, *, extracted_memory: str = "", - allow_reasoning_fallback: bool = False, ) -> str: content = ResponseExtractor.extract_content_text( @@ -870,6 +859,17 @@ def build_runtime_summarizer_response_details( ): usage = {} + reasoning_fields = { + "reasoning_content", + "reasoning", + "thinking", + } + visible_message = { + key: value + for key, value in message.items() + if key not in reasoning_fields + } + payload = { "kind": "summarizer_response", "model": ResponseExtractor.extract_model( @@ -879,16 +879,11 @@ def build_runtime_summarizer_response_details( response ), "content": content, - "reasoning_content": reasoning, + "reasoning_generated": bool(reasoning), + "reasoning_length": len(reasoning), "extracted_memory": extracted_memory, - "allow_reasoning_fallback": allow_reasoning_fallback, - "used_reasoning_fallback": ( - bool(reasoning) - and not bool(content) - and allow_reasoning_fallback - ), "usage": usage, - "message": message, + "message": visible_message, "choice_index": choice.get( "index", 0, diff --git a/tests/test_answer_rating_client_contract.py b/tests/test_answer_rating_client_contract.py new file mode 100644 index 00000000..59dedf5c --- /dev/null +++ b/tests/test_answer_rating_client_contract.py @@ -0,0 +1,87 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +ANSWER_RATING_JS = ROOT / "ui" / "static" / "js" / "answer-rating.js" +RUNTIME_FEEDBACK_JS = ROOT / "ui" / "static" / "js" / "runtime" / "runtime-feedback.js" +CHAT_RATING_CSS = ROOT / "ui" / "static" / "css" / "chat-rating.css" +THEME_WIN95_CSS = ROOT / "ui" / "static" / "css" / "theme-win95.css" +INDEX_HTML = ROOT / "ui" / "templates" / "index.html" + + +class AnswerRatingClientContractTests(unittest.TestCase): + + def test_submit_commit_preserves_selected_rating_visual_state(self): + source = ANSWER_RATING_JS.read_text(encoding="utf-8") + + self.assertIn("const ratingPressClasses = [", source) + self.assertIn( + "const waitingForL1 = !blocked && !pastTurn && !l1Ready;", + source, + ) + self.assertIn( + 'bubble.classList.toggle("jin-rating-l1-waiting", waitingForL1);', + source, + ) + self.assertIn("bubble.classList.remove(...ratingPressClasses);", source) + self.assertIn('bubble.dataset.ratingCommitted = "true";', source) + self.assertIn('bubble.dataset.ratingPastTurn = "true";', source) + self.assertNotIn( + "delete bubble.dataset.ratingSelected;\n clearBubbleRatingIntensity", + source, + ) + + def test_runtime_gate_lock_does_not_clear_committed_rating_visuals(self): + source = RUNTIME_FEEDBACK_JS.read_text(encoding="utf-8") + + self.assertIn("const ratingLockedTransientClasses = [", source) + self.assertNotIn("const ratingLockedVisualClasses = [", source) + self.assertNotIn("delete bubble.dataset.ratingSelected;", source) + self.assertNotIn("delete bubble.dataset.ratingClickAlt;", source) + self.assertNotIn("bubble.removeAttribute(\"aria-label\");", source) + + def test_committed_selected_rating_still_uses_selected_css(self): + source = CHAT_RATING_CSS.read_text(encoding="utf-8") + + self.assertIn(".jin-chat-bubble-rateable.jin-rating-selected-minus {", source) + self.assertIn(".jin-chat-bubble-rateable.jin-rating-selected-plus {", source) + self.assertIn( + ".jin-chat-bubble-rateable.jin-rating-committed .jin-rating-zone", + source, + ) + self.assertNotIn( + ".jin-chat-bubble-rateable.jin-rating-selected-minus:not(.jin-rating-committed)", + source, + ) + self.assertNotIn( + ".jin-chat-bubble-rateable.jin-rating-selected-plus:not(.jin-rating-committed)", + source, + ) + + def test_win95_theme_keeps_selected_rules_for_committed_bubbles(self): + source = THEME_WIN95_CSS.read_text(encoding="utf-8") + + self.assertIn( + "body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-minus,", + source, + ) + self.assertIn( + "body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-plus {", + source, + ) + + def test_cache_versions_are_bumped_for_rating_assets(self): + source = INDEX_HTML.read_text(encoding="utf-8") + + self.assertIn("/static/css/chat-rating.css?v=rating-persist-1", source) + self.assertIn("/static/css/theme-win95.css?v=rating-persist-1", source) + self.assertIn("/static/js/answer-rating.js?v=answer-rating-persist-1", source) + self.assertIn( + "/static/js/runtime/runtime-feedback.js?v=runtime-feedback-persist-1", + source, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_behavior_contract.py b/tests/test_behavior_contract.py index 527e508b..49192728 100644 --- a/tests/test_behavior_contract.py +++ b/tests/test_behavior_contract.py @@ -255,6 +255,31 @@ def test_empty_triggers_allow_action_guard_execution(self): ) ) + def test_save_active_memory_contract_is_autonomous(self): + + instructions = build_runtime_action_contract_instructions( + "SAVE_ACTIVE_MEMORY" + ) + + self.assertEqual( + get_action_guard_triggers("save_active_memory"), + (), + ) + self.assertFalse( + should_pause_action_guard_for_confirmation( + "save_active_memory", + "normal message", + ) + ) + self.assertIn( + "active memory is an autonomous runtime action", + instructions, + ) + self.assertIn( + "briefly notify the user in natural text", + instructions, + ) + def test_runtime_action_instructions_include_marker_and_followup(self): instructions = build_runtime_action_contract_instructions( diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 9ce8a22e..3a7bff79 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -448,6 +448,37 @@ def test_brain_system_prompt_keeps_runtime_rule_sentences_separated(self): "When no actions needed or sequence is done stop instantly and notify user naturally.\n\n" "MEMORY AND SESSION PROPOSALS:", ) + assert_contains_text( + self, + prompt, + "Active memory does not require explicit confirmation.", + ) + assert_contains_text( + self, + prompt, + "Use proposals for save-session and delayed-memory decisions", + ) + assert_not_contains_text( + self, + prompt, + ( + "Never emit a save or memory marker during proposal until " + "the user clearly accepts it." + ), + ) + assert_not_contains_text( + self, + prompt, + "Propose active memory when", + ) + assert_not_contains_text( + self, + prompt, + ( + "Never emit a save or update marker until the user " + "explicitly accepts the proposal." + ), + ) def test_non_stream_blocks_save_session_meta_request_in_reasoning(self): diff --git a/tests/test_chat_runtime_marker_ui.py b/tests/test_chat_runtime_marker_ui.py index c3b372f2..b8215f79 100644 --- a/tests/test_chat_runtime_marker_ui.py +++ b/tests/test_chat_runtime_marker_ui.py @@ -79,7 +79,7 @@ def test_chat_script_cache_version_is_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/js/chat.js?v=memory-reference-sync-2', + '/static/js/chat.js?v=reasoning-gap-1', source, ) diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index d33d7360..84a57543 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -85,6 +85,94 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): runtime_actions_source, ) + def test_delayed_memory_delete_sync_and_loaded_state_are_registered(self): + + runtime_source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime.js" + ).read_text( + encoding="utf-8" + ) + l4_source = ( + ROOT + / "ui" + / "static" + / "js" + / "runtime" + / "runtime-l4-memory.js" + ).read_text( + encoding="utf-8" + ) + socket_delayed_source = ( + ROOT + / "ui" + / "static" + / "js" + / "socket" + / "delayed-memory.js" + ).read_text( + encoding="utf-8" + ) + runtime_actions_source = ( + ROOT + / "ui" + / "static" + / "js" + / "socket" + / "runtime-actions.js" + ).read_text( + encoding="utf-8" + ) + + self.assertIn( + "loadedDelayedMemoryReportIds", + runtime_source, + ) + self.assertIn( + "isDelayedMemoryReportLoaded", + runtime_source, + ) + self.assertIn( + "markDelayedMemoryReportLoaded", + runtime_source, + ) + self.assertIn( + "deleteDelayedMemoryReport: deleteDelayedMemoryReportAndRender", + runtime_source, + ) + self.assertIn( + "restoreDelayedMemoryReport: restoreDelayedMemoryReportAndRender", + runtime_source, + ) + self.assertIn( + "isDelayedMemoryReportLoaded(reportId, report)", + l4_source, + ) + self.assertIn( + "loadedIds.forEach((factId) => {", + l4_source, + ) + self.assertIn( + "deleted_delayed_memory_report_ids: deletedReportIds", + socket_delayed_source, + ) + self.assertIn( + 'action === "append_delayed_memory"', + runtime_actions_source, + ) + self.assertIn( + 'action === "remove_delayed_memory"', + runtime_actions_source, + ) + self.assertIn( + "markDelayedMemoryReportLoaded(", + runtime_actions_source, + ) + def test_appended_delayed_memory_bubbles_open_their_own_reports(self): runtime_actions_source = ( diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index d211cb91..1fbef9cd 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -6,6 +6,7 @@ from utils.delayed_memory_file_store import ( build_delayed_memory_file_payload, + delete_delayed_memory_report_files, load_delayed_memory_reports_from_files, merge_delayed_memory_reports, persist_delayed_memory_report, @@ -171,6 +172,38 @@ def test_save_replaces_old_filename_for_same_id(self): ], ) + def test_delete_removes_current_and_legacy_report_files(self): + + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + + persist_delayed_memory_report( + "48ggds", + self.build_report( + title="Current title", + ), + root=root, + ) + legacy_path = root / "48ggds.json" + legacy_path.write_text( + "{}", + encoding="utf-8", + ) + + errors = delete_delayed_memory_report_files( + "48ggds", + root=root, + ) + + self.assertEqual( + errors, + [], + ) + self.assertEqual( + list(root.glob("48ggds*.json")), + [], + ) + def test_invalid_files_are_skipped_without_crashing(self): with tempfile.TemporaryDirectory() as temporary_directory: @@ -353,6 +386,66 @@ def test_websocket_sync_treats_incoming_report_refs_as_authoritative(self): "File only", ) + def test_websocket_sync_deletes_report_by_id(self): + + context = SimpleNamespace( + delayed_memory_reports={ + "48ggds": self.build_report( + title="Deleted report", + ), + "a1b2c3": self.build_report( + title="Kept report", + ), + }, + runtime_appended_delayed_memory={ + "48ggds": { + **self.build_report( + title="Deleted report", + ), + "id": "48ggds", + }, + }, + runtime_appended_delayed_memory_ids=[ + "48ggds", + "a1b2c3", + ], + ) + + deleted_ids = apply_delayed_memory_reports( + context, + { + "delayed_memory_reports": { + "a1b2c3": self.build_report( + title="Kept report", + ), + }, + "deleted_delayed_memory_report_ids": [ + "48ggds", + ], + }, + ) + + self.assertEqual( + deleted_ids, + [ + "48ggds", + ], + ) + self.assertNotIn( + "48ggds", + context.delayed_memory_reports, + ) + self.assertNotIn( + "48ggds", + context.runtime_appended_delayed_memory, + ) + self.assertEqual( + context.runtime_appended_delayed_memory_ids, + [ + "a1b2c3", + ], + ) + def test_file_payload_accepts_example_aliases(self): payload = build_delayed_memory_file_payload( diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index b8c83163..b4c7fc6b 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,8 +47,8 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) - self.assertIn("/static/js/runtime/runtime.js?v=runtime-facade-22", source) + self.assertIn("/static/css/runtime-memory.css?v=delayed-delete-1", source) + self.assertIn("/static/js/runtime/runtime.js?v=delayed-delete-1", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l2_memory.py b/tests/test_l2_memory.py index 822d00bd..c7c1aaf6 100644 --- a/tests/test_l2_memory.py +++ b/tests/test_l2_memory.py @@ -29,6 +29,10 @@ maybe_summarize_runtime_l2_memory, record_runtime_l1_diff, ) +from runtime.memory_common import ( + build_runtime_summarizer_response_details, + extract_runtime_memory_text, +) from config_loader import ( config, ) @@ -41,6 +45,53 @@ class L2MemoryTests( unittest.IsolatedAsyncioTestCase ): + def test_runtime_memory_extractor_does_not_fallback_to_reasoning(self): + + response = { + "model": "fake-service", + "choices": [ + { + "index": 0, + "message": { + "content": "", + "reasoning_content": ( + "PRIVATE SUMMARIZER REASONING" + ), + }, + "finish_reason": "stop", + } + ], + } + + self.assertEqual( + extract_runtime_memory_text( + response + ), + "", + ) + + details = build_runtime_summarizer_response_details( + response, + extracted_memory="", + ) + + self.assertIn( + '"reasoning_generated": true', + details, + ) + self.assertIn( + '"reasoning_length": 28', + details, + ) + self.assertNotIn( + "PRIVATE SUMMARIZER REASONING", + details, + ) + self.assertNotIn( + "reasoning_content", + details, + ) + def test_runtime_l2_memory_prompt_defines_pattern_layer(self): prompt = build_runtime_l2_memory_system_prompt() @@ -626,3 +677,204 @@ async def emit(event): 0, ) + async def test_l2_memory_ignores_reasoning_only_summarizer_response(self): + + private_reasoning = ( + "Analysis of Evidence:\n" + "PRIVATE L2 CHAIN OF THOUGHT" + ) + + class ReasoningOnlyServiceClient: + model_uid = "fake-service" + + def __init__(self): + self.calls = [] + + async def ask( + self, + *, + system_prompt: str, + user_prompt: str, + temperature: float, + max_tokens: int, + timeout: float | None = None, + ): + + self.calls.append({ + "system_prompt": system_prompt, + "user_prompt": user_prompt, + "temperature": temperature, + "max_tokens": max_tokens, + "timeout": timeout, + }) + + return { + "model": self.model_uid, + "choices": [ + { + "message": { + "content": "", + "reasoning": private_reasoning, + }, + "finish_reason": "stop", + } + ], + } + + service_client = ReasoningOnlyServiceClient() + logger = FakeLogger() + context = SimpleNamespace( + clients={ + "service": service_client, + }, + emitter=SimpleNamespace( + events=[], + emit=None, + ), + logger=logger, + runtime_memory="", + runtime_memory_snapshots=[], + runtime_memory_snapshot_index=0, + runtime_l2_memory=( + "possible pattern: previous safe memory" + ), + runtime_l1_diff_history=[ + { + "turn_number": 1, + "snapshot_index": 1, + "total_diff": 110, + "changes": { + "added": [ + { + "key": "diagnostic_signal", + "value": "early broad update", + }, + ], + }, + }, + { + "turn_number": 2, + "snapshot_index": 2, + "total_diff": 254, + "changes": { + "changed": [ + { + "previous_key": "topic", + "previous_value": "early broad update", + "current_key": "topic", + "current_value": "large rewrite", + }, + ], + }, + }, + { + "turn_number": 3, + "snapshot_index": 3, + "total_diff": 199.05, + "changes": { + "changed": [ + { + "previous_key": "diagnostic_signal", + "previous_value": "large rewrite", + "current_key": "diagnostic_signal", + "current_value": "memory mechanics", + }, + ], + }, + }, + { + "turn_number": 4, + "snapshot_index": 4, + "total_diff": 151, + "changes": { + "changed": [ + { + "previous_key": "diagnostic_signal", + "previous_value": "memory mechanics", + "current_key": "diagnostic_signal", + "current_value": "pattern trigger", + }, + ], + }, + }, + { + "turn_number": 5, + "snapshot_index": 5, + "total_diff": 144.9, + "changes": { + "changed": [ + { + "previous_key": "diagnostic_signal", + "previous_value": "pattern trigger", + "current_key": "diagnostic_signal", + "current_value": "L2 window", + }, + ], + }, + }, + { + "turn_number": 6, + "snapshot_index": 6, + "total_diff": 77.6, + "changes": { + "changed": [ + { + "previous_key": "intent", + "previous_value": "inspect diff", + "current_key": "intent", + "current_value": "adjust trigger", + }, + ], + }, + }, + { + "turn_number": 7, + "snapshot_index": 7, + "total_diff": 104.69, + "changes": { + "changed": [ + { + "previous_key": "diagnostic_signal", + "previous_value": "L2 window", + "current_key": "diagnostic_signal", + "current_value": "repeated keys", + }, + ], + }, + }, + ], + runtime_l2_last_turn=0, + user_message_count=7, + session_id="test-session", + ) + + updated_memory = await maybe_summarize_runtime_l2_memory( + context=context, + ) + + self.assertEqual( + len(service_client.calls), + 1, + ) + self.assertEqual( + updated_memory, + "possible pattern: previous safe memory", + ) + self.assertEqual( + context.runtime_l2_memory, + "possible pattern: previous safe memory", + ) + + log_text = "\n".join( + str(item) + for item in logger.summarizer_logs + ) + + self.assertNotIn( + "PRIVATE L2 CHAIN OF THOUGHT", + log_text, + ) + self.assertNotIn( + "Analysis of Evidence", + context.runtime_l2_memory, + ) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index c1567c63..c3dd316f 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -23,6 +23,16 @@ def test_deleted_fact_card_exposes_full_payload_and_restore(self): self.assertIn('window.handleL4LoggerMemoryRestoreResult', source) self.assertIn('window.handleL4MemoryRestoreResult', source) + def test_deleted_delayed_memory_card_exposes_payload_and_restore(self): + source = LOG_ENTRIES_JS.read_text(encoding="utf-8") + + self.assertIn('"[MEMORY:DELAYED:DELETED]"', source) + self.assertIn('kind: "delayed_memory_report"', source) + self.assertIn('deleted_delayed_memory_report', source) + self.assertIn('"payload"', source) + self.assertIn('"restore"', source) + self.assertIn('api.restoreDelayedMemoryReport(', source) + def test_extraction_and_merge_request_response_cards_are_paired(self): source = LOG_ENTRIES_JS.read_text(encoding="utf-8") @@ -76,11 +86,11 @@ def test_delete_marks_local_l4_store_deleted_before_sync(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=memory-hover-sync-9', source) - self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=l4-delete-local-2', source) + self.assertIn('/static/css/runtime-memory.css?v=delayed-delete-1', source) + self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=delayed-loaded-facts-1', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) - self.assertIn('/static/js/logger/log-entries.js?v=l4-restore-meta-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=delayed-delete-restore-1', source) self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-2', source) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index 2684dffe..b1800e84 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1184,6 +1184,64 @@ def test_delayed_report_anchor_stays_visible_with_report_suffix(self): {"F2"}, ) + def test_loaded_delayed_report_fact_ids_are_visible_with_report_suffix(self): + context = RuntimeContext( + websocket=None, + emitter=None, + logger=None, + clients={}, + ) + context.runtime_long_term_memory_store = normalize_l4_store({ + "facts": [ + { + "id": "F1", + "key": "user.name", + "value": "Sergey", + }, + { + "id": "F2", + "key": "social.friend", + "value": "Taras is a personal friend.", + }, + ], + }) + context.delayed_memory_reports = { + "abc123": { + "title": "Social context", + "anchor_fact_ids": [ + "F1", + ], + "facts_ids": [ + "F1", + "F2", + ], + }, + } + context.runtime_appended_delayed_memory = { + "abc123": { + **context.delayed_memory_reports["abc123"], + "id": "abc123", + }, + } + + context_block = build_runtime_l4_memory_context( + context=context + ) + + self.assertIn( + "user.name: Sergey [ id: F1 ] [ delayed_memory_id: abc123 ]", + context_block, + ) + self.assertIn( + "social.friend: Taras is a personal friend. [ id: F2 ] " + "[ delayed_memory_id: abc123 ]", + context_block, + ) + self.assertEqual( + context.runtime_l4_archived_fact_ids, + set(), + ) + def test_anchor_visibility_wins_over_absorbed_reference_in_other_report(self): context = RuntimeContext( websocket=None, diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index c0ba8616..e9b5e489 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -111,9 +111,9 @@ def test_win95_keeps_reference_and_citation_hits_consistent(self): def test_cache_versions_are_bumped_for_citation_sync_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=memory-hover-sync-9", source) - self.assertIn("/static/css/theme-win95.css?v=memory-citations-2", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26", source) + self.assertIn("/static/css/runtime-memory.css?v=delayed-delete-1", source) + self.assertIn("/static/css/theme-win95.css?v=rating-persist-1", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-delete-1", source) self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-18", source) self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index fee565f4..3664c697 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -312,6 +312,48 @@ def test_delayed_memory_fact_ids_render_inline_with_fact_titles(self): css_source, ) + def test_delayed_memory_modal_delete_uses_hold_and_runtime_restore_payload(self): + source = MEMORY_VIEW_JS.read_text(encoding="utf-8") + runtime_source = RUNTIME_JS.read_text(encoding="utf-8") + css_source = RUNTIME_MEMORY_CSS.read_text(encoding="utf-8") + + self.assertIn( + "delayedMemoryModalDeleteButton", + source, + ) + self.assertIn( + '"delayed-memory-modal-icon-button delayed-memory-modal-delete"', + source, + ) + self.assertIn( + "deleteDelayedMemoryModalReport", + source, + ) + self.assertIn( + "configureRuntimeMemoryDeleteHold(\n delayedMemoryModalDeleteButton", + source, + ) + self.assertIn( + "deleteDelayedMemoryReport =\n options.deleteDelayedMemoryReport", + source, + ) + self.assertIn( + "function deleteDelayedMemoryReportAndRender(", + runtime_source, + ) + self.assertIn( + "function restoreDelayedMemoryReportAndRender(", + runtime_source, + ) + self.assertIn( + "deletedReportIds", + runtime_source, + ) + self.assertIn( + ".delayed-memory-modal-delete svg", + css_source, + ) + @unittest.skipUnless( shutil.which("node"), "node is required for the browser-side reference matcher test", @@ -368,19 +410,19 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=memory-hover-sync-9', + '/static/css/runtime-memory.css?v=delayed-delete-1', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=runtime-memory-view-26', + '/static/js/runtime/runtime-memory-view.js?v=delayed-delete-1', source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=runtime-facade-22', + '/static/js/runtime/runtime.js?v=delayed-delete-1', source, ) self.assertIn( - '/static/js/chat.js?v=memory-reference-sync-2', + '/static/js/chat.js?v=reasoning-gap-1', source, ) diff --git a/ui/static/css/base.css b/ui/static/css/base.css index 0597bc45..bc676ae7 100644 --- a/ui/static/css/base.css +++ b/ui/static/css/base.css @@ -37,9 +37,9 @@ --scene-jin-tint-alpha: 0.048; --scene-layer-base: 0; - --scene-layer-tint: 1; - --scene-layer-context: 2; - --scene-layer-search: 3; + --scene-layer-context: 1; + --scene-layer-search: 2; + --scene-layer-tint: 3; --scene-layer-shade: 4; /* Вертикальный ритм чата: добавляет немного воздуха между ходами. */ diff --git a/ui/static/css/chat-rating.css b/ui/static/css/chat-rating.css index 8e22f284..d707c3be 100644 --- a/ui/static/css/chat-rating.css +++ b/ui/static/css/chat-rating.css @@ -310,7 +310,7 @@ cursor: pointer; } -.jin-chat-bubble-rateable.jin-rating-selected-minus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) { +.jin-chat-bubble-rateable.jin-rating-selected-minus { box-shadow: 0 14px 42px rgba(0, 0, 0, 0.25), 0 0 28px rgba(248, 113, 113, var(--jin-rating-glow-alpha, 0.085)), @@ -323,15 +323,15 @@ brightness(var(--jin-rating-brightness, 1.01)); } -.jin-chat-bubble-rateable.jin-rating-selected-minus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked)::before { +.jin-chat-bubble-rateable.jin-rating-selected-minus::before { opacity: var(--jin-rating-edge-opacity, 0.42); } -.jin-chat-bubble-rateable.jin-rating-selected-minus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) .jin-chat-pre { +.jin-chat-bubble-rateable.jin-rating-selected-minus .jin-chat-pre { transform: translateX(-2px) skewY(-0.18deg); } -.jin-chat-bubble-rateable.jin-rating-selected-neutral:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) { +.jin-chat-bubble-rateable.jin-rating-selected-neutral { box-shadow: 0 14px 42px rgba(0, 0, 0, 0.24), 0 0 34px rgba(226, 232, 240, var(--jin-rating-glow-alpha, 0.095)), @@ -344,14 +344,14 @@ brightness(var(--jin-rating-brightness, 1.04)); } -.jin-chat-bubble-rateable.jin-rating-selected-neutral:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) .jin-chat-pre { +.jin-chat-bubble-rateable.jin-rating-selected-neutral .jin-chat-pre { transform: none; text-shadow: 0 0 10px rgba(226, 232, 240, var(--jin-rating-text-alpha, 0.070)), 0 0 14px rgba(103, 232, 249, 0.018); } -.jin-chat-bubble-rateable.jin-rating-selected-plus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) { +.jin-chat-bubble-rateable.jin-rating-selected-plus { box-shadow: 0 14px 42px rgba(0, 0, 0, 0.25), 0 0 28px rgba(74, 222, 128, var(--jin-rating-glow-alpha, 0.092)), @@ -364,10 +364,10 @@ brightness(var(--jin-rating-brightness, 1.01)); } -.jin-chat-bubble-rateable.jin-rating-selected-plus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked)::after { +.jin-chat-bubble-rateable.jin-rating-selected-plus::after { opacity: var(--jin-rating-edge-opacity, 0.42); } -.jin-chat-bubble-rateable.jin-rating-selected-plus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) .jin-chat-pre { +.jin-chat-bubble-rateable.jin-rating-selected-plus .jin-chat-pre { transform: translateX(2px) skewY(0.18deg); } diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 456fe206..7cd5dbe0 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -439,6 +439,17 @@ transition: filter 0.18s ease; } +.delayed-memory-modal-delete svg { + width: 17px; + height: 17px; + fill: currentColor; +} + +.delayed-memory-modal-delete:hover, +.delayed-memory-modal-delete:focus-visible { + color: rgba(248, 113, 113, 0.98); +} + .delayed-memory-modal-pin-active { color: rgba(240, 253, 250, 0.98); } diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index aff54377..c9224527 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -805,9 +805,9 @@ body.theme-win95 .jin-chat-bubble-rateable:not(.jin-rating-selected-active):not( body.theme-win95 .jin-chat-bubble-rateable.jin-rating-press-minus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked), body.theme-win95 .jin-chat-bubble-rateable.jin-rating-press-neutral:not(.jin-rating-committed):not(.jin-rating-interaction-blocked), body.theme-win95 .jin-chat-bubble-rateable.jin-rating-press-plus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked), -body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-minus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked), -body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-neutral:not(.jin-rating-committed):not(.jin-rating-interaction-blocked), -body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-plus:not(.jin-rating-committed):not(.jin-rating-interaction-blocked) { +body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-minus, +body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-neutral, +body.theme-win95 .jin-chat-bubble-rateable.jin-rating-selected-plus { box-shadow: none; } diff --git a/ui/static/js/answer-rating.js b/ui/static/js/answer-rating.js index 3cf7e5db..f9ba7160 100644 --- a/ui/static/js/answer-rating.js +++ b/ui/static/js/answer-rating.js @@ -9,6 +9,11 @@ "jin-rating-press-neutral", "jin-rating-press-plus", ]; + const ratingPressClasses = [ + "jin-rating-press-minus", + "jin-rating-press-neutral", + "jin-rating-press-plus", + ]; const ratingVisualClasses = ratingSelectionClasses.filter( (className) => className !== "jin-rating-committed" ); @@ -131,17 +136,20 @@ }); } - bubble.classList.remove(...ratingVisualClasses); + bubble.classList.remove(...ratingPressClasses); bubble.classList.add("jin-rating-committed"); bubble.dataset.ratingPending = "false"; bubble.dataset.ratingCommitted = "true"; bubble.dataset.ratingPastTurn = "true"; - delete bubble.dataset.ratingSelected; - clearBubbleRatingIntensity(bubble); - setBubbleRatingClickAlt(bubble, 0); + + if (!previousRating) { + bubble.classList.remove(...ratingVisualClasses); + clearBubbleRatingIntensity(bubble); + setBubbleRatingClickAlt(bubble, 0); + } const zones = bubble.querySelector(":scope > .jin-rating-hover-zones"); - if (zones) { + if (zones && !previousRating) { zones.title = ""; } } @@ -208,15 +216,17 @@ const blocked = isRatingInteractionBlocked(); const pastTurn = bubble.dataset.ratingPastTurn === "true"; - const ready = !blocked && !pastTurn && isBubbleRatingL1Ready(bubble); + const l1Ready = isBubbleRatingL1Ready(bubble); + const ready = !blocked && !pastTurn && l1Ready; + const waitingForL1 = !blocked && !pastTurn && !l1Ready; bubble.dataset.ratingL1Ready = ready ? "true" : "false"; - bubble.classList.toggle("jin-rating-l1-waiting", !ready); + bubble.classList.toggle("jin-rating-l1-waiting", waitingForL1); bubble.classList.toggle("jin-rating-interaction-blocked", blocked); const zones = bubble.querySelector(":scope > .jin-rating-hover-zones"); if (zones && blocked) { zones.title = "rating is locked while JIN is generating"; - } else if (zones && !ready && !bubble.dataset.ratingSelected) { + } else if (zones && waitingForL1 && !bubble.dataset.ratingSelected) { zones.title = "waiting for L1 snapshot before rating"; } else if (zones && !bubble.dataset.ratingSelected) { zones.title = ""; @@ -489,16 +499,20 @@ const committedClickCount = Number(bubble.dataset.ratingClickCount || 0); - bubble.classList.remove(...ratingVisualClasses); + bubble.classList.remove(...ratingPressClasses); bubble.classList.add("jin-rating-committed"); bubble.dataset.ratingPending = "false"; bubble.dataset.ratingCommitted = "true"; - delete bubble.dataset.ratingSelected; - clearBubbleRatingIntensity(bubble); - setBubbleRatingClickAlt(bubble, 0); + bubble.dataset.ratingPastTurn = "true"; const zones = bubble.querySelector(":scope > .jin-rating-hover-zones"); - if (zones) { + if (!committedRating) { + bubble.classList.remove(...ratingVisualClasses); + clearBubbleRatingIntensity(bubble); + setBubbleRatingClickAlt(bubble, 0); + } + + if (zones && !committedRating) { zones.title = ""; } diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 05add8f8..c4543cd5 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -879,6 +879,9 @@ const l4SummarizerCards = { const l4DeletedFactCards = new Map(); +const delayedDeletedReportCards = + new Map(); + function parseL4JsonPayload(details) { const text = String(details || "").trim(); @@ -1672,6 +1675,193 @@ function handleL4MemoryRestoreResult( ); } +function resolveDeletedDelayedMemoryReport( + details, + meta, +) { + if ( + meta + && meta.deleted_delayed_memory_report + && typeof meta.deleted_delayed_memory_report === "object" + ) { + return meta.deleted_delayed_memory_report; + } + + const payload = + parseL4JsonPayload(details); + + if ( + payload + && payload.report + && typeof payload.report === "object" + ) { + return payload.report; + } + + return null; +} + +function getDelayedMemoryReportCardId(report) { + return String( + report + && ( + report.id + || report._storage_key + ) + || "" + ).trim().toLowerCase(); +} + +function handleDelayedMemoryDeletedReportLog( + tag, + details, + meta, +) { + const isDeleted = + String(meta && meta.memory_event || "").toLowerCase() + === "delayed_memory_deleted" + || String(tag || "").toUpperCase() + === "[MEMORY:DELAYED:DELETED]"; + + if (!isDeleted) { + return null; + } + + const report = + resolveDeletedDelayedMemoryReport( + details, + meta + ); + const reportId = + getDelayedMemoryReportCardId( + report + ); + + if (!report || !reportId) { + return null; + } + + const logDiv = + createL4LoggerCard( + "[MEMORY:DELAYED:DELETED]" + ); + + const title = + document.createElement("span"); + + title.className = + "block mt-2 text-zinc-200 font-semibold"; + + title.textContent = + String(report.title || reportId || "Delayed memory"); + + const summary = + document.createElement("span"); + + summary.className = + "block mt-1 text-zinc-400"; + + summary.textContent = + String(report.summary || ""); + + logDiv.appendChild(title); + + if (summary.textContent) { + logDiv.appendChild(summary); + } + + const actions = + document.createElement("div"); + + actions.className = + "mt-2 flex flex-wrap items-center gap-2"; + + const payloadButton = + createL4LoggerButton( + "payload" + ); + + const restoreButton = + createL4LoggerButton( + "restore" + ); + + payloadButton.addEventListener( + "click", + function () { + showTrace( + JSON.stringify({ + kind: "delayed_memory_report", + report, + }), + "Delayed memory deleted" + ); + } + ); + + restoreButton.addEventListener( + "click", + function () { + const api = + window.JinRuntime + && window.JinRuntime.runtime; + + if (!api || typeof api.restoreDelayedMemoryReport !== "function") { + return; + } + + const restored = + api.restoreDelayedMemoryReport( + reportId, + report + ); + + if (!restored) { + restoreButton.textContent = + "restore failed"; + + window.setTimeout( + function () { + restoreButton.textContent = + "restore"; + }, + 1400 + ); + return; + } + + restoreButton.disabled = + true; + restoreButton.textContent = + "restored"; + restoreButton.classList.add( + "opacity-50" + ); + + delayedDeletedReportCards.delete( + reportId + ); + dismissLogAfterClear( + logDiv + ); + } + ); + + delayedDeletedReportCards.set( + reportId, + { + logDiv, + restoreButton, + } + ); + + actions.appendChild(payloadButton); + actions.appendChild(restoreButton); + logDiv.appendChild(actions); + + return logDiv; +} + function appendLog( tag, message, @@ -1726,6 +1916,17 @@ function appendLog( return l4DeletedFactLog; } + const delayedMemoryDeletedLog = + handleDelayedMemoryDeletedReportLog( + tag, + normalized.details, + meta + ); + + if (delayedMemoryDeletedLog) { + return delayedMemoryDeletedLog; + } + const flowId = meta?.flow_id; diff --git a/ui/static/js/runtime/runtime-feedback.js b/ui/static/js/runtime/runtime-feedback.js index d1ab0e6a..c61d98c2 100644 --- a/ui/static/js/runtime/runtime-feedback.js +++ b/ui/static/js/runtime/runtime-feedback.js @@ -25,11 +25,7 @@ ".jin-chat-bubble-rateable[data-rating-gate-generation], " + ".jin-chat-bubble-service[data-rating-gate-generation], " + ".jin-chat-bubble-brain[data-rating-gate-generation]"; - const ratingLockedVisualClasses = [ - "jin-rating-selected-active", - "jin-rating-selected-minus", - "jin-rating-selected-neutral", - "jin-rating-selected-plus", + const ratingLockedTransientClasses = [ "jin-rating-press-minus", "jin-rating-press-neutral", "jin-rating-press-plus", @@ -233,33 +229,12 @@ ); if (bubbleGen < jinAnswerRatingL1Gate.lockedBelowGeneration) { bubble.classList.remove( - ...ratingLockedVisualClasses + ...ratingLockedTransientClasses ); bubble.classList.add("jin-rating-committed"); bubble.dataset.ratingCommitted = "true"; bubble.dataset.ratingPastTurn = "true"; bubble.dataset.ratingPending = "false"; - delete bubble.dataset.ratingSelected; - delete bubble.dataset.ratingClickAlt; - bubble.removeAttribute("alt"); - bubble.removeAttribute("aria-label"); - bubble.removeAttribute("title"); - - [ - "--jin-rating-glow-alpha", - "--jin-rating-inner-alpha", - "--jin-rating-text-alpha", - "--jin-rating-edge-strong-alpha", - "--jin-rating-edge-mid-alpha", - "--jin-rating-edge-soft-alpha", - "--jin-rating-edge-opacity", - "--jin-rating-edge-flash-opacity", - "--jin-rating-edge-mid-opacity", - "--jin-rating-saturation", - "--jin-rating-brightness", - ].forEach((property) => { - bubble.style.removeProperty(property); - }); const zones = bubble.querySelector( ":scope > .jin-rating-hover-zones" diff --git a/ui/static/js/runtime/runtime-l4-memory.js b/ui/static/js/runtime/runtime-l4-memory.js index a5f696df..f5b40f6a 100644 --- a/ui/static/js/runtime/runtime-l4-memory.js +++ b/ui/static/js/runtime/runtime-l4-memory.js @@ -303,6 +303,22 @@ return readStore().facts; } + function isDelayedMemoryReportLoaded(reportId, report) { + if (report && Boolean(report.pinned)) { + return true; + } + + const runtime = + window.JinRuntime + && window.JinRuntime.runtime; + + return Boolean( + runtime + && typeof runtime.isDelayedMemoryReportLoaded === "function" + && runtime.isDelayedMemoryReportLoaded(reportId) + ); + } + function getArchivedFactIdSet() { const reports = storage && typeof storage.readDelayedMemoryReports === "function" @@ -319,8 +335,9 @@ const archivedIds = new Set(); const anchorIds = new Set(); + const loadedIds = new Set(); - Object.values(reports).forEach((report) => { + Object.entries(reports).forEach(([reportId, report]) => { if ( !report || typeof report !== "object" @@ -348,11 +365,23 @@ } }); }); + + if (isDelayedMemoryReportLoaded(reportId, report)) { + normalizeList(report.facts_ids).forEach((rawId) => { + const factId = normalizeFactId(rawId, false); + if (factId) { + loadedIds.add(factId); + } + }); + } }); anchorIds.forEach((factId) => { archivedIds.delete(factId); }); + loadedIds.forEach((factId) => { + archivedIds.delete(factId); + }); return archivedIds; } diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 6677ff03..d9b87493 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -13,6 +13,7 @@ let getDelayedMemoryReports = null; let setDelayedMemoryReportPinned = null; let setDelayedMemoryReportAnchorFactIds = null; + let deleteDelayedMemoryReport = null; let getFactsMemoryFields = null; let deleteFactsMemoryField = null; let getLongTermMemoryFacts = null; @@ -57,6 +58,7 @@ let delayedMemoryModalTitle = null; let delayedMemoryModalContent = null; let delayedMemoryModalPinButton = null; + let delayedMemoryModalDeleteButton = null; let delayedMemoryModalReport = null; let activeDelayedMemoryReportId = ""; @@ -2924,6 +2926,29 @@ pinned ? "Unpin delayed memory" : "Pin delayed memory"; } + function deleteDelayedMemoryModalReport() { + if ( + !delayedMemoryModalReport + || typeof deleteDelayedMemoryReport !== "function" + ) { + return; + } + + const reportId = + getDelayedMemoryReportId( + delayedMemoryModalReport + ); + + const deleted = + deleteDelayedMemoryReport( + reportId + ); + + if (deleted !== false) { + closeDelayedMemoryReportModal(); + } + } + function setActiveDelayedMemoryReportRow( reportId ) { @@ -3037,6 +3062,26 @@ delayedMemoryModalPinButton.innerHTML = ''; + delayedMemoryModalDeleteButton = + document.createElement("button"); + + delayedMemoryModalDeleteButton.type = + "button"; + + delayedMemoryModalDeleteButton.className = + "delayed-memory-modal-icon-button delayed-memory-modal-delete"; + + delayedMemoryModalDeleteButton.setAttribute( + "aria-label", + "Delete delayed memory" + ); + + delayedMemoryModalDeleteButton.title = + "Hold to delete delayed memory"; + + delayedMemoryModalDeleteButton.innerHTML = + ''; + const closeButton = document.createElement("button"); @@ -3068,6 +3113,10 @@ delayedMemoryModalPinButton ); + headerActions.appendChild( + delayedMemoryModalDeleteButton + ); + headerActions.appendChild( closeButton ); @@ -3124,6 +3173,19 @@ } ); + delayedMemoryModalDeleteButton.addEventListener( + "click", + (event) => { + event.preventDefault(); + event.stopPropagation(); + } + ); + + configureRuntimeMemoryDeleteHold( + delayedMemoryModalDeleteButton, + deleteDelayedMemoryModalReport + ); + closeButton.addEventListener( "click", closeDelayedMemoryReportModal @@ -4056,6 +4118,8 @@ setDelayedMemoryReportPinned = options.setDelayedMemoryReportPinned || null; setDelayedMemoryReportAnchorFactIds = options.setDelayedMemoryReportAnchorFactIds || null; + deleteDelayedMemoryReport = + options.deleteDelayedMemoryReport || null; getFactsMemoryFields = options.getFactsMemoryFields || null; deleteFactsMemoryField = options.deleteFactsMemoryField || null; getLongTermMemoryFacts = options.getLongTermMemoryFacts || null; diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index 2689e609..ada98cb0 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -151,6 +151,7 @@ const runtimeMemoryHistory = { let runtimeMemoryDisplayMode = "runtime"; let restoredSessionMemorySnapshot = null; +const loadedDelayedMemoryReportIds = new Set(); feedback.init({ memoryModel, @@ -569,6 +570,7 @@ memoryView.init({ getDelayedMemoryReports: readDelayedMemoryReports, setDelayedMemoryReportPinned, setDelayedMemoryReportAnchorFactIds, + deleteDelayedMemoryReport: deleteDelayedMemoryReportAndRender, removeLongTermFactIdFromDelayedMemoryReports, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, @@ -646,6 +648,140 @@ function syncDelayedMemoryStateToAvatar() { return false; } +function normalizeRuntimeDelayedMemoryReportId(value) { + const reportId = + String(value || "").trim().toLowerCase(); + + return /^[a-z0-9]{6}$/.test(reportId) + ? reportId + : ""; +} + +function syncDelayedMemoryReportsToServer( + options = {} +) { + if (typeof window.syncDelayedMemoryReportsToRuntime !== "function") { + return false; + } + + return window.syncDelayedMemoryReportsToRuntime( + options + ); +} + +function isDelayedMemoryReportLoaded(reportId) { + const normalizedId = + normalizeRuntimeDelayedMemoryReportId(reportId); + + return Boolean( + normalizedId + && loadedDelayedMemoryReportIds.has(normalizedId) + ); +} + +function markDelayedMemoryReportLoaded( + reportId, + loaded = true +) { + const normalizedId = + normalizeRuntimeDelayedMemoryReportId(reportId); + + if (!normalizedId) { + return false; + } + + const wasLoaded = + loadedDelayedMemoryReportIds.has(normalizedId); + + if (loaded) { + loadedDelayedMemoryReportIds.add( + normalizedId + ); + } else { + loadedDelayedMemoryReportIds.delete( + normalizedId + ); + } + + if ( + wasLoaded + === loadedDelayedMemoryReportIds.has(normalizedId) + ) { + return true; + } + + renderRuntimeMemorySnapshot(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } + + return true; +} + +function buildDeletedDelayedMemoryReportPayload( + reportId, + report +) { + const normalizedId = + normalizeRuntimeDelayedMemoryReportId(reportId); + + if ( + !normalizedId + || !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return null; + } + + return { + ...report, + id: normalizedId, + _storage_key: normalizedId, + _restore_meta: { + was_loaded: + isDelayedMemoryReportLoaded(normalizedId) + || Boolean(report.pinned), + }, + }; +} + +function logDeletedDelayedMemoryReport( + reportId, + report +) { + const deletedReport = + buildDeletedDelayedMemoryReportPayload( + reportId, + report + ); + + if ( + !deletedReport + || typeof window.appendLog !== "function" + ) { + return; + } + + window.appendLog( + "[MEMORY:DELAYED:DELETED]", + "Delayed memory deleted", + JSON.stringify( + { + kind: "delayed_memory_report", + report: deletedReport, + }, + null, + 2 + ), + { + memory_event: "delayed_memory_deleted", + deleted_delayed_memory_report: deletedReport, + } + ); +} + function syncActiveMemoryStateToAvatar() { const avatar = window.JinRuntime @@ -1003,6 +1139,137 @@ function setDelayedMemoryReportAnchorFactIds( } +function deleteDelayedMemoryReportAndRender( + reportId +) { + + const normalizedId = + normalizeRuntimeDelayedMemoryReportId( + reportId + ); + const reports = + readDelayedMemoryReports(); + const report = + reports[normalizedId]; + + if ( + !normalizedId + || !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return false; + } + + const deletedReport = + buildDeletedDelayedMemoryReportPayload( + normalizedId, + report + ); + + delete reports[normalizedId]; + + writeDelayedMemoryReports( + reports + ); + + loadedDelayedMemoryReportIds.delete( + normalizedId + ); + + renderRuntimeMemorySnapshot(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } + + syncDelayedMemoryReportsToServer({ + deletedReportIds: [ + normalizedId, + ], + }); + + logDeletedDelayedMemoryReport( + normalizedId, + report + ); + + return { + id: normalizedId, + report: deletedReport, + }; + +} + +function restoreDelayedMemoryReportAndRender( + reportId, + report +) { + + const normalizedId = + normalizeRuntimeDelayedMemoryReportId( + reportId + || ( + report + && typeof report === "object" + && !Array.isArray(report) + ? report.id || report._storage_key + : "" + ) + ); + + if ( + !normalizedId + || !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return false; + } + + const restoreMeta = + report._restore_meta + && typeof report._restore_meta === "object" + && !Array.isArray(report._restore_meta) + ? report._restore_meta + : {}; + const cleanReport = { + ...report, + }; + + delete cleanReport.id; + delete cleanReport._storage_key; + delete cleanReport._restore_meta; + + writeDelayedMemoryReports({ + ...readDelayedMemoryReports(), + [normalizedId]: cleanReport, + }); + + if ( + restoreMeta.was_loaded + || Boolean(cleanReport.pinned) + ) { + loadedDelayedMemoryReportIds.add( + normalizedId + ); + } + + renderRuntimeMemorySnapshot(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } + + syncDelayedMemoryReportsToServer(); + + return { + id: normalizedId, + report: readDelayedMemoryReports()[normalizedId], + }; + +} + function normalizeLongTermFactId( value @@ -1428,8 +1695,12 @@ window.JinRuntime.runtime = { appendActiveMemoryRecords: appendActiveMemoryRecordsAndRender, removeActiveMemoryRecordById: removeActiveMemoryRecordByIdAndRender, getDelayedMemoryReports: readDelayedMemoryReports, + isDelayedMemoryReportLoaded, + markDelayedMemoryReportLoaded, setDelayedMemoryReportPinned, setDelayedMemoryReportAnchorFactIds, + deleteDelayedMemoryReport: deleteDelayedMemoryReportAndRender, + restoreDelayedMemoryReport: restoreDelayedMemoryReportAndRender, getFactsMemoryFields, deleteFactsMemoryField: deleteFactsMemoryFieldAndRender, getLongTermMemoryFacts() { diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index 53e32ed0..b549ddb0 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -423,7 +423,34 @@ function clearDelayedMemoryContentFilter( } -function syncDelayedMemoryReportsToRuntime() { +function normalizeDelayedMemoryReportIds(value) { + const source = + Array.isArray(value) + ? value + : [value]; + const reportIds = []; + const seen = new Set(); + + source.forEach((item) => { + const reportId = + String(item || "").trim().toLowerCase(); + + if ( + !/^[a-z0-9]{6}$/.test(reportId) + || seen.has(reportId) + ) { + return; + } + + seen.add(reportId); + reportIds.push(reportId); + }); + + return reportIds; +} + + +function syncDelayedMemoryReportsToRuntime(options = {}) { if ( !ws || ws.readyState !== WebSocket.OPEN @@ -436,19 +463,29 @@ function syncDelayedMemoryReportsToRuntime() { const delayedMemoryReports = window.JinRuntime.runtime.getDelayedMemoryReports(); + const deletedReportIds = + normalizeDelayedMemoryReportIds( + options.deletedReportIds + || options.deleted_delayed_memory_report_ids + || [] + ); if ( !delayedMemoryReports || typeof delayedMemoryReports !== "object" || Array.isArray(delayedMemoryReports) - || !Object.keys(delayedMemoryReports).length + || ( + !Object.keys(delayedMemoryReports).length + && !deletedReportIds.length + ) ) { - return; + return false; } - sendSocketMessage({ + return sendSocketMessage({ type: "delayed_memory_store_sync", delayed_memory_reports: delayedMemoryReports, + deleted_delayed_memory_report_ids: deletedReportIds, }); } diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index bf85cf0f..3f2e5597 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -1173,6 +1173,33 @@ function handleRuntimeAction( }); } + if ( + ( + status === "completed" + || status === "complete" + || status === "done" + ) + && delayedMemoryPreview.reportId + && window.JinRuntime + && window.JinRuntime.runtime + && typeof window.JinRuntime.runtime.markDelayedMemoryReportLoaded + === "function" + ) { + if (action === "append_delayed_memory") { + window.JinRuntime.runtime.markDelayedMemoryReportLoaded( + delayedMemoryPreview.reportId, + true + ); + } + + if (action === "remove_delayed_memory") { + window.JinRuntime.runtime.markDelayedMemoryReportLoaded( + delayedMemoryPreview.reportId, + false + ); + } + } + if ( status === "completed" || status === "complete" diff --git a/ui/templates/index.html b/ui/templates/index.html index 83541a87..bf570b47 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -10,13 +10,13 @@ - + - + - + - + @@ -486,7 +486,7 @@ - + @@ -503,12 +503,12 @@ - - + + - - + + @@ -517,7 +517,7 @@ - + @@ -525,8 +525,8 @@ - - + + diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index 85f647c3..9d78f195 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -449,6 +449,62 @@ def persist_delayed_memory_reports( return errors +def delete_delayed_memory_report_files( + report_id: str, + *, + root: Path | str = DELAYED_MEMORY_ROOT, +) -> list[str]: + + normalized_id = str( + report_id + or "" + ).strip().casefold() + + if not is_delayed_memory_report_id(normalized_id): + return [ + f"{normalized_id or report_id}: invalid delayed memory id" + ] + + root_path = Path(root) + + if not root_path.exists(): + return [] + + candidates = [ + *root_path.glob( + f"{normalized_id}_*.json" + ), + root_path / f"{normalized_id}.json", + ] + errors = [] + seen = set() + + for candidate in candidates: + try: + resolved_candidate = candidate.resolve() + except OSError: + resolved_candidate = candidate + + if resolved_candidate in seen: + continue + + seen.add( + resolved_candidate + ) + + if not candidate.exists(): + continue + + try: + candidate.unlink() + except OSError as error: + errors.append( + f"{normalized_id}: {error}" + ) + + return errors + + def load_delayed_memory_reports_from_files( *, root: Path | str = DELAYED_MEMORY_ROOT, diff --git a/websocket/__init__.py b/websocket/__init__.py index 6284a8c1..2b1803a2 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -62,6 +62,7 @@ ) from utils.delayed_memory_file_store import ( + delete_delayed_memory_report_files, persist_delayed_memory_reports, ) @@ -267,10 +268,19 @@ async def process_pending_requests(): continue if message_type == "delayed_memory_store_sync": - apply_delayed_memory_reports( + deleted_report_ids = apply_delayed_memory_reports( context, message_data, ) + for report_id in deleted_report_ids: + delete_errors = delete_delayed_memory_report_files( + report_id + ) + for delete_error in delete_errors: + await logger.log_system( + "[DELAYED MEMORY] local file delete failed: " + + delete_error + ) reports = getattr( context, "delayed_memory_reports", diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 032a7181..074fd035 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -22,6 +22,7 @@ from runtime.telemetry import send_telemetry from utils.actions import ( is_active_memory_key, + is_delayed_memory_report_id, refresh_active_memory_runtime_metadata, remove_active_memory_entries, ) @@ -109,13 +110,54 @@ def clean_delayed_memory_reports(value) -> dict: ) +def clean_deleted_delayed_memory_report_ids(value) -> list[str]: + + source = value if isinstance(value, list) else [value] + report_ids = [] + seen = set() + + for item in source: + report_id = str( + item + or "" + ).strip().casefold() + + if ( + not report_id + or report_id in seen + or not is_delayed_memory_report_id( + report_id + ) + ): + continue + + seen.add( + report_id + ) + report_ids.append( + report_id + ) + + return report_ids + + def apply_delayed_memory_reports( context, message_data: dict, -) -> None: +) -> list[str]: - if "delayed_memory_reports" not in message_data: - return + deleted_report_ids = clean_deleted_delayed_memory_report_ids( + message_data.get( + "deleted_delayed_memory_report_ids", + [], + ) + ) + + if ( + "delayed_memory_reports" not in message_data + and not deleted_report_ids + ): + return [] incoming_reports = clean_delayed_memory_reports( message_data.get( @@ -131,11 +173,53 @@ def apply_delayed_memory_reports( ) ) + for report_id in deleted_report_ids: + existing_reports.pop( + report_id, + None, + ) + context.delayed_memory_reports = { **existing_reports, **incoming_reports, } + appended_reports = getattr( + context, + "runtime_appended_delayed_memory", + None, + ) + + if isinstance( + appended_reports, + dict, + ): + for report_id in deleted_report_ids: + appended_reports.pop( + report_id, + None, + ) + + appended_ids = getattr( + context, + "runtime_appended_delayed_memory_ids", + None, + ) + + if isinstance( + appended_ids, + list, + ): + deleted_report_id_set = set( + deleted_report_ids + ) + appended_ids[:] = [ + report_id + for report_id in appended_ids + if str(report_id or "").strip().casefold() + not in deleted_report_id_set + ] + from runtime.L4_memory import ( refresh_runtime_l4_archived_fact_ids, ) @@ -144,6 +228,8 @@ def apply_delayed_memory_reports( context ) + return deleted_report_ids + def hydrate_delayed_memory_reports_from_files( context, From 68978e067b9e1afc4bcf69759ced41a95b39af65 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:59:25 +0300 Subject: [PATCH 029/168] chore: cleanup - add delayed memory delete/restore and loaded-context highlighting - rename delayed memory and skill flows to load/unload - keep skill state visible in runtime context - simplify reasoning loop validator output - persist answer rating state and autonomous active memory --- agent/nodes/brain.py | 16 +- assets/skills/chunk_reader/JIN_SKILL.md | 4 +- assets/skills/file_manager.txt | 2 +- assets/skills/wildcards.txt | 2 +- clients/brain_client.py | 19 +- contracts/append_skill.json | 24 --- contracts/asset_action.json | 2 +- contracts/list_skills.json | 19 -- contracts/load_delayed_memory.json | 4 +- contracts/load_skill.json | 24 +++ contracts/remove_skill.json | 24 --- contracts/rules_assembler.py | 48 +---- contracts/save_delayed_memory.json | 2 +- contracts/unload_delayed_memory.json | 2 +- contracts/unload_skill.json | 24 +++ contracts/update_delayed_memory.json | 2 +- contracts/update_l4_facts.json | 2 +- rules/brain_context_builder.py | 138 ++++--------- rules/runtime.py | 20 +- runtime/L4_memory.py | 20 +- runtime/L4_memory_rules.py | 2 +- runtime/runtime_context.py | 6 +- runtime/stream.py | 29 ++- tests/runtime_actions/test_active_memory.py | 4 +- tests/runtime_actions/test_asset_actions.py | 4 +- tests/runtime_actions/test_delayed_memory.py | 118 +++++------ tests/runtime_actions/test_jin_color.py | 4 +- tests/runtime_actions/test_skill_actions.py | 4 +- tests/runtime_actions/test_stream_filter.py | 12 +- tests/runtime_actions/test_todo.py | 4 +- tests/test_answer_rating_client_contract.py | 2 +- tests/test_brain_asset_flow.py | 30 +-- tests/test_brain_runtime_actions.py | 14 +- tests/test_delayed_memory_client_contract.py | 57 ++++-- tests/test_delayed_memory_file_store.py | 12 +- ...ayed_memory_pinned_glow_client_contract.py | 4 +- tests/test_l4_logger_cards_client_contract.py | 2 +- tests/test_l4_memory.py | 2 +- ..._memory_citation_layers_client_contract.py | 11 +- ...t_memory_reference_sync_client_contract.py | 18 +- ..._runtime_avatar_l4_ring_client_contract.py | 6 +- tests/test_stream_validator.py | 2 +- ui/static/css/runtime-avatar.css | 3 + ui/static/css/runtime-memory.css | 14 +- ui/static/css/theme-win95.css | 7 +- ui/static/js/chat-runtime-actions.js | 12 +- ui/static/js/chat.js | 8 +- ui/static/js/logger/log-entries.js | 12 +- ui/static/js/runtime/runtime-avatar.js | 21 +- ui/static/js/runtime/runtime-memory-view.js | 112 ++++++++++- ui/static/js/runtime/runtime-session.js | 14 +- ui/static/js/runtime/runtime-storage.js | 100 ++++++---- ui/static/js/runtime/runtime.js | 56 +++++- ui/static/js/socket/delayed-memory.js | 24 ++- ui/static/js/socket/event-handlers.js | 10 +- ui/static/js/socket/runtime-actions.js | 34 ++-- ui/templates/index.html | 22 +- utils/actions/action_counter_utils.py | 8 +- utils/actions/asset_actions.py | 6 +- utils/actions/common_action_utils.py | 41 ++-- utils/actions/delayed_memory_actions.py | 54 ++--- utils/actions/dispatcher.py | 88 +++----- utils/actions/regexp_utils.py | 2 +- utils/actions/skill_actions.py | 90 +++------ ...end_skill_utils.py => skill_load_utils.py} | 14 +- utils/brain_client_utils.py | 188 +++++++++--------- utils/context/assets.py | 49 +---- utils/context/context_exports.py | 8 +- utils/context/delayed_memory.py | 6 +- utils/context/runtime_state.py | 5 +- utils/context/skills.py | 118 +++++------ utils/context/tool_results.py | 52 ++--- utils/delayed_memory_file_store.py | 53 +++-- utils/python_skill_asset_utils.py | 16 +- utils/session_actions_history.py | 29 ++- utils/skills_asset_utils.py | 5 +- utils/stream_handler.py | 81 +++++--- utils/stream_validator.py | 24 ++- websocket/__init__.py | 9 + websocket/bootstrap.py | 135 ++++++++++++- websocket/logger.py | 9 + 81 files changed, 1238 insertions(+), 1046 deletions(-) delete mode 100644 contracts/append_skill.json delete mode 100644 contracts/list_skills.json create mode 100644 contracts/load_skill.json delete mode 100644 contracts/remove_skill.json create mode 100644 contracts/unload_skill.json rename utils/actions/{append_skill_utils.py => skill_load_utils.py} (72%) diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index aa028a53..51d1908f 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -879,18 +879,18 @@ def build_followup_system_prompt( if context is not None: from rules.brain_context_builder import ( - build_appended_delayed_memory_context, + build_loaded_delayed_memory_context, ) - appended_delayed_memory_context = ( - build_appended_delayed_memory_context( + loaded_delayed_memory_context = ( + build_loaded_delayed_memory_context( context ) ) - if appended_delayed_memory_context: + if loaded_delayed_memory_context: sections.append( - appended_delayed_memory_context + loaded_delayed_memory_context ) sections.append( @@ -1645,9 +1645,9 @@ def collect_pending_asset_tool_results(): runtime_action_event_offset ) skill_state_followup_event_names = { - "append_skill", - "remove_skill", - "append_delayed_memory", + "load_skill", + "unload_skill", + "load_delayed_memory", } def collect_pending_action_events(): diff --git a/assets/skills/chunk_reader/JIN_SKILL.md b/assets/skills/chunk_reader/JIN_SKILL.md index 14081dc2..cc2b2378 100644 --- a/assets/skills/chunk_reader/JIN_SKILL.md +++ b/assets/skills/chunk_reader/JIN_SKILL.md @@ -15,7 +15,7 @@ Do not use: Mode files: - Every Markdown file beside `chunk_reader.py` whose name ends with `-mode.md` is an available reader mode. -- Use the exact filename shown in the appended skill context, for example `plain-mode.md`. +- Use the exact filename shown in the loaded skill context, for example `plain-mode.md`. - Adding another `*-mode.md` file automatically makes it available; no Python or runtime changes are required. Single-mode action: @@ -54,4 +54,4 @@ Rules for run_python_skill: - The script must stay inside `assets/skills//` and end in `.py`. - `$ATTACHMENT` is replaced with a temporary local path containing the selected attachment. - No shell is used. stdout and stderr are returned as the tool result. -- Use only when the appended skill documents the script contract or the user explicitly requests the test. +- Use only when the loaded skill documents the script contract or the user explicitly requests the test. diff --git a/assets/skills/file_manager.txt b/assets/skills/file_manager.txt index 657df663..94af8f5d 100644 --- a/assets/skills/file_manager.txt +++ b/assets/skills/file_manager.txt @@ -27,7 +27,7 @@ File behavior: - Keep generated lists in files; report only concise examples in chat. Action workflow: -1. Use LIST_SKILLS before an operational file workflow if the relevant project skills are not already appended. +1. Check before an operational file workflow and LOAD_SKILL if the relevant project skill is available but not loaded. 2. Use ASSET_ACTION with JSON payload for create_asset_file, append_asset_file, preview_file, and any task-specific asset action provided by another loaded instruction block. 3. Emit ASSET_ACTION as a JSON block: diff --git a/assets/skills/wildcards.txt b/assets/skills/wildcards.txt index 3dfa6da5..e5ef6697 100644 --- a/assets/skills/wildcards.txt +++ b/assets/skills/wildcards.txt @@ -38,7 +38,7 @@ Path authority: - If the file is not listed and was not just created successfully, treat it as missing. Do not substitute a guessed path. Action workflow: -1. Use LIST_SKILLS to retrieve relevant project skills before a wildcard workflow. +1. Check for relevant project skills before a wildcard workflow and LOAD_SKILL if needed. 2. Before any action that references existing wildcard files, call list_wildcards. 3. After list_wildcards, use only the exact returned wildcard paths, without .txt, inside __category/file__ tokens. 4. Use ASSET_ACTION with JSON payload for list_wildcards, sample_wildcard, expand_template, generate_prompt_batch, check_duplicates, or preview_file. diff --git a/clients/brain_client.py b/clients/brain_client.py index 4f53ce25..57a53827 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -8,9 +8,8 @@ ) from contracts.rules_assembler import ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_APPEND_SKILL, + RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, @@ -609,7 +608,7 @@ async def ask_brain_stream( context=context, ) - appended_skill_marker_names = { + loaded_skill_marker_names = { normalize_skill_name( skill.get( "name", @@ -624,22 +623,22 @@ async def ask_brain_stream( for skill in ( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) } - appended_skill_marker_names.discard( + loaded_skill_marker_names.discard( "" ) - def preserve_duplicate_append_skill_marker( + def preserve_duplicate_load_skill_marker( _raw_marker, action, ) -> bool: - if action.name != RUNTIME_ACTION_APPEND_SKILL: + if action.name != RUNTIME_ACTION_LOAD_SKILL: return False requested_skill = normalize_skill_name( @@ -649,10 +648,10 @@ def preserve_duplicate_append_skill_marker( if not requested_skill: return False - if requested_skill in appended_skill_marker_names: + if requested_skill in loaded_skill_marker_names: return True - appended_skill_marker_names.add( + loaded_skill_marker_names.add( requested_skill ) @@ -660,7 +659,7 @@ def preserve_duplicate_append_skill_marker( content_filter = RuntimeActionStreamFilter( enabled_actions=enabled_actions, - preserve_action_marker=preserve_duplicate_append_skill_marker, + preserve_action_marker=preserve_duplicate_load_skill_marker, repetition_guard=RuntimeActionRepetitionGuard(), #preserve_action_text=True ) diff --git a/contracts/append_skill.json b/contracts/append_skill.json deleted file mode 100644 index b6dd2766..00000000 --- a/contracts/append_skill.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "append_skill": { - "version": 1, - "description": "Allows the model to append skills.", - "runtime_action": "APPEND_SKILL", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": true - }, - "rules": [ - "APPEND / REMOVE SKILLS:", - "Use APPEND_SKILL and REMOVE_SKILL only for single skill append or remove.", - "", - "", - "For multiple appending you can also use following markers:", - "", - "You may append multiple skills at once.", - "Never append a skill that is already listed in ! Continue or notify user!" - ], - "close_tag": false - } -} diff --git a/contracts/asset_action.json b/contracts/asset_action.json index f9b19af9..d94790b6 100644 --- a/contracts/asset_action.json +++ b/contracts/asset_action.json @@ -11,7 +11,7 @@ }, "rules": [ "PROJECT ASSETS:", - "After the relevant skill appears in APPENDED_SKILLS, DO NOT follow its instructions, only by user request." + "Use only capabilities from skills already loaded in , and only when the user request requires them." ], "close_tag": true } diff --git a/contracts/list_skills.json b/contracts/list_skills.json deleted file mode 100644 index ffd3c41e..00000000 --- a/contracts/list_skills.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "list_skills": { - "version": 1, - "description": "Allows the model to list available skills.", - "runtime_action": "LIST_SKILLS", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": true - }, - "rules": [ - "PROJECT ASSETS:", - "Emit when you need to inspect available skills.", - "After the relevant skill appears in APPENDED_SKILLS, DO NOT follow its instructions, only by user request." - ], - "close_tag": false - } -} diff --git a/contracts/load_delayed_memory.json b/contracts/load_delayed_memory.json index b685a8c4..643feab2 100644 --- a/contracts/load_delayed_memory.json +++ b/contracts/load_delayed_memory.json @@ -7,13 +7,13 @@ "triggers": [], "blockers": [], "effects": { - "emit_followup": false + "emit_followup": true }, "rules": [ "Emit one marker per saved delayed memory. You may emit multiple markers to load multiple reports; already loaded ids are ignored.", "id - is a placeholder word; replace placeholder with the 6-character id prefix shown in .", "Available reports are listed in the block.", - "If a report is already loaded, use the content placed in ." + "If a report is already loaded, use the content placed in ." ], "close_tag": false } diff --git a/contracts/load_skill.json b/contracts/load_skill.json new file mode 100644 index 00000000..096e0f2b --- /dev/null +++ b/contracts/load_skill.json @@ -0,0 +1,24 @@ +{ + "load_skill": { + "version": 1, + "description": "Allows the model to load skills.", + "runtime_action": "LOAD_SKILL", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": true + }, + "rules": [ + "LOAD / UNLOAD SKILLS:", + "Use LOAD_SKILL and UNLOAD_SKILL only for a single skill load or unload.", + "", + "", + "For multiple skills you can also use: ", + "", + "You may load multiple skills at once.", + "Never load a skill already marked as loaded in ." + ], + "close_tag": false + } +} diff --git a/contracts/remove_skill.json b/contracts/remove_skill.json deleted file mode 100644 index 9f0fa920..00000000 --- a/contracts/remove_skill.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "remove_skill": { - "version": 1, - "description": "Allows the model to remove skills.", - "runtime_action": "REMOVE_SKILL", - "private_marker": "", - "triggers": [], - "blockers": [], - "effects": { - "emit_followup": false - }, - "rules": [ - "APPEND / REMOVE SKILLS:", - "Use APPEND_SKILL and REMOVE_SKILL only for single skill append or remove.", - "", - "", - "For multiple appending you can also use following markers:", - "", - "You may append multiple skills at once.", - "Never append a skill that is already listed in ! Continue or notify user!" - ], - "close_tag": false - } -} diff --git a/contracts/rules_assembler.py b/contracts/rules_assembler.py index ff41040d..c4613afd 100644 --- a/contracts/rules_assembler.py +++ b/contracts/rules_assembler.py @@ -19,13 +19,12 @@ ACTION_CONFIG_KEYS = ( ("WEB_SEARCH", "CAN_WEB_SEARCH"), ("SAVE_SESSION", "CAN_SAVE_SESSION"), - ("LIST_SKILLS", "CAN_USE_ASSETS"), ("CLEAN_TOOL_RESULTS", "CAN_CLEAN_TOOL_RESULTS"), ("IDLE", "CAN_IDLE"), ("JIN_COLOR", "CAN_JIN_COLOR"), ("UPDATE_L4_FACTS", "CAN_UPDATE_L4_FACTS"), - ("APPEND_SKILL", "CAN_USE_ASSETS"), - ("REMOVE_SKILL", "CAN_USE_ASSETS"), + ("LOAD_SKILL", "CAN_USE_ASSETS"), + ("UNLOAD_SKILL", "CAN_USE_ASSETS"), ("ASSET_ACTION", "CAN_USE_ASSETS"), ("CREATE_TODO_LIST", "CAN_RUNTIME_TODO"), ("RESOLVE_TODO", "CAN_RUNTIME_TODO"), @@ -305,9 +304,8 @@ def normalize_runtime_action_names(enabled_actions=None) -> tuple[str, ...]: if normalized_name == "ASSET_ACTION": normalized_names.extend(( - "LIST_SKILLS", - "APPEND_SKILL", - "REMOVE_SKILL", + "LOAD_SKILL", + "UNLOAD_SKILL", )) for normalized_name in normalized_names: @@ -443,18 +441,6 @@ def _action_enabled( ) -def _context_has_list_skills_tool_result(context=None) -> bool: - for entry in list(getattr(context, "runtime_tool_results", []) or []): - if not isinstance(entry, dict): - continue - - result = entry.get("result") - if isinstance(result, dict) and result.get("action") == "list_skills": - return True - - return False - - def _context_has_delayed_memory_reports(context=None) -> bool: reports = getattr(context, "delayed_memory_reports", None) return bool(isinstance(reports, dict) and reports) @@ -481,20 +467,9 @@ def build_allowed_markers( context=None, ) -> str: markers: list[str] = [] - has_list_skills_result = _context_has_list_skills_tool_result(context) - for action in enabled_actions: action_name = _normalize_action_name(action) - if action_name == "LIST_SKILLS" and has_list_skills_result: - continue - - if action_name in { - "APPEND_SKILL", - "REMOVE_SKILL", - } and not has_list_skills_result: - continue - marker = get_runtime_action_private_marker(action_name) if marker: markers.append(marker) @@ -516,8 +491,6 @@ def build_runtime_action_instructions( RUNTIME_ACTIONS_RULES, PROPOSAL_RULES, ] - has_list_skills_result = _context_has_list_skills_tool_result(context) - def append_rules(action_name: str) -> None: action_instructions = build_runtime_action_contract_instructions( action_name @@ -538,15 +511,9 @@ def append_rules(action_name: str) -> None: } and not _context_has_delayed_memory_reports(context): continue - if normalized_name in { - "APPEND_SKILL", - "REMOVE_SKILL", - } and not has_list_skills_result: - continue - append_rules(normalized_name) - if _action_enabled(enabled_actions, "LIST_SKILLS"): + if _action_enabled(enabled_actions, "LOAD_SKILL", "UNLOAD_SKILL"): instructions.append(SKILL_ROUTING_RULES) return "\n\n".join( @@ -576,12 +543,11 @@ def append_rules(action_name: str) -> None: RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY = get_runtime_action_name( "resolve_active_memory" ) -RUNTIME_ACTION_LIST_SKILLS = get_runtime_action_name("list_skills") RUNTIME_ACTION_CLEAN_TOOL_RESULTS = get_runtime_action_name( "clean_tool_results" ) -RUNTIME_ACTION_APPEND_SKILL = get_runtime_action_name("append_skill") -RUNTIME_ACTION_REMOVE_SKILL = get_runtime_action_name("remove_skill") +RUNTIME_ACTION_LOAD_SKILL = get_runtime_action_name("load_skill") +RUNTIME_ACTION_UNLOAD_SKILL = get_runtime_action_name("unload_skill") RUNTIME_ACTION_ASSET_ACTION = get_runtime_action_name("asset_action") RUNTIME_ACTION_CREATE_TODO_LIST = get_runtime_action_name("create_todo_list") RUNTIME_ACTION_RESOLVE_TODO = get_runtime_action_name("resolve_todo") diff --git a/contracts/save_delayed_memory.json b/contracts/save_delayed_memory.json index a77db058..481d2d6d 100644 --- a/contracts/save_delayed_memory.json +++ b/contracts/save_delayed_memory.json @@ -28,7 +28,7 @@ "", "You must fulfill title, summary, tags, and body so the report is valid for runtime processing.", "When the report consolidates facts from , list every referenced F id in facts_ids.", - "anchor_fact_ids are the most important/key facts among facts_ids. They remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", + "anchor_fact_ids are the most important/key facts among facts_ids. Choose from 1 to 3 anchor facts. They remain directly visible in ; while the report exists they receive a [ delayed_memory_id: ... ] suffix.", "facts_ids is the full set of L4 facts attached to the report, including every anchor_fact_ids entry. Non-anchor facts_ids are represented by the report and are hidden from while the report exists.", "Use exact ids copied from the [ id: ... ] suffixes. Do not list unrelated ids and never use PF pending ids.", "anchor_fact_ids must be a subset of facts_ids. Leave both arrays empty when the report does not consolidate L4 facts." diff --git a/contracts/unload_delayed_memory.json b/contracts/unload_delayed_memory.json index 88cf4604..6a6ef968 100644 --- a/contracts/unload_delayed_memory.json +++ b/contracts/unload_delayed_memory.json @@ -10,7 +10,7 @@ "emit_followup": false }, "rules": [ - "Emit to unload one currently loaded APPENDED_DELAYED_MEMORY from context in the next turn/message; the saved report is never deleted from storage.", + "Emit to unload one currently loaded LOADED_DELAYED_MEMORY from context in the next turn/message; the saved report is never deleted from storage.", "id - is a placeholder word; replace it with the 6-character id prefix shown in ." ], "close_tag": false diff --git a/contracts/unload_skill.json b/contracts/unload_skill.json new file mode 100644 index 00000000..5333f2c3 --- /dev/null +++ b/contracts/unload_skill.json @@ -0,0 +1,24 @@ +{ + "unload_skill": { + "version": 1, + "description": "Allows the model to unload skills.", + "runtime_action": "UNLOAD_SKILL", + "private_marker": "", + "triggers": [], + "blockers": [], + "effects": { + "emit_followup": false + }, + "rules": [ + "LOAD / UNLOAD SKILLS:", + "Use LOAD_SKILL and UNLOAD_SKILL only for a single skill load or unload.", + "", + "", + "For multiple skills you can also use: ", + "", + "You may load multiple skills at once.", + "Never load a skill already marked as loaded in ." + ], + "close_tag": false + } +} diff --git a/contracts/update_delayed_memory.json b/contracts/update_delayed_memory.json index a3110a80..de1df7eb 100644 --- a/contracts/update_delayed_memory.json +++ b/contracts/update_delayed_memory.json @@ -1,7 +1,7 @@ { "update_delayed_memory": { "version": 2, - "description": "Allows the model to append selected data to an existing delayed memory report without rewriting its existing content.", + "description": "Allows the model to add selected data to an existing delayed memory report without rewriting its existing content.", "runtime_action": "UPDATE_DELAYED_MEMORY", "private_marker": "", "triggers": [], diff --git a/contracts/update_l4_facts.json b/contracts/update_l4_facts.json index 4c38cc29..0af4c4a7 100644 --- a/contracts/update_l4_facts.json +++ b/contracts/update_l4_facts.json @@ -11,7 +11,7 @@ }, "rules": [ "Use this marker only for a meaningful L4 contradiction, ambiguous durable fact, inaccurate formulation, or semantic duplicate that matters to the current understanding.", - "Do not use it for harmless repetition between L4 and appended delayed memory, stylistic rewording with no change of meaning, temporary session details, weak suspicions, or issues unrelated to durable memory.", + "Do not use it for harmless repetition between L4 and loaded delayed memory, stylistic rewording with no change of meaning, temporary session details, weak suspicions, or issues unrelated to durable memory.", "Do not interrupt an unrelated conversation only to clean memory. Raise the issue when it affects the current answer or can be resolved naturally in the current topic.", "When the user's meaning is not already clear, ask one brief natural question in the visible conversation. You may name the conflicting remembered meanings, but do not expose the marker or internal action mechanics.", "Never emit the marker while ambiguity remains or on the same turn in which you ask for clarification. Emit it after the user resolves the issue, or immediately when the user's current message itself is an explicit correction.", diff --git a/rules/brain_context_builder.py b/rules/brain_context_builder.py index ec965634..09298d66 100644 --- a/rules/brain_context_builder.py +++ b/rules/brain_context_builder.py @@ -42,7 +42,7 @@ "CAN_UPDATE_L4_FACTS": True, } -APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS = ( +LOADED_DELAYED_MEMORY_CONTEXT_FIELDS = ( "title", "summary", "tags", @@ -448,84 +448,6 @@ def _append_zero_diff_alert( ) -def _build_current_appended_skills_context( - context=None, -) -> str: - - from utils.brain_client_utils import ( - indent_xml, - ) - - if context is None: - return "" - - appended_skills = list( - getattr( - context, - "runtime_appended_skills", - [], - ) - or [] - ) - skill_labels = [] - - for skill in appended_skills: - modes = [] - - if isinstance( - skill, - dict, - ): - name = str( - skill.get( - "name", - "", - ) - or "" - ).strip() - modes = [ - str(mode).strip() - for mode in skill.get( - "modes", - [], - ) - or [] - if str(mode).strip() - ] - else: - name = str( - skill - or "" - ).strip() - - if name: - mode_suffix = ( - f" (modes: {', '.join(modes)})" - if modes - else "" - ) - skill_labels.append( - f"{name}{mode_suffix}" - ) - - if not skill_labels: - return "" - - lines = [ - f"{index}. {label}" - for index, label in enumerate( - skill_labels, - start=1, - ) - ] - - return ( - "\n" - f"{indent_xml(escape(chr(10).join(lines)), spaces=4)}\n" - "" - ) - - def build_delayed_memory_inventory_context( context=None, ) -> str: @@ -581,7 +503,7 @@ def build_delayed_memory_inventory_context( ) -def build_appended_delayed_memory_context( +def build_loaded_delayed_memory_context( context=None, ) -> str: @@ -596,16 +518,16 @@ def build_appended_delayed_memory_context( if context is None: return "" - appended_reports = include_pinned_delayed_memory_reports( + loaded_reports = include_pinned_delayed_memory_reports( context ) - if not appended_reports: + if not loaded_reports: return "" blocks = [] - for report_id, report in appended_reports.items(): + for report_id, report in loaded_reports.items(): if not isinstance( report, dict, @@ -615,14 +537,14 @@ def build_appended_delayed_memory_context( payload = { "id": report_id, } - for field_name in APPENDED_DELAYED_MEMORY_CONTEXT_FIELDS: + for field_name in LOADED_DELAYED_MEMORY_CONTEXT_FIELDS: if field_name in report: payload[field_name] = report[field_name] blocks.append( - "\n" + "\n" f"{indent_xml(escape(format_tool_result_payload(payload)))}\n" - "" + "" ) return "\n".join( @@ -751,8 +673,12 @@ def build_brain_context( build_session_actions_history_context, ) from utils.context.tool_results import ( + build_loaded_skills_content_context, build_tool_results_context, ) + from utils.context.skills import ( + build_skills_inventory_context, + ) prompt_parts = [] runtime_context_parts = [] @@ -771,6 +697,24 @@ def build_brain_context( tool_results_context ) + # Skill inventory is always visible directly below tool results. + # The inventory is context state, not a runtime action. + prompt_parts.append( + build_skills_inventory_context( + context + ) + ) + + loaded_skills_content_context = ( + build_loaded_skills_content_context( + context + ) + ) + if loaded_skills_content_context: + prompt_parts.append( + loaded_skills_content_context + ) + # User feedback block: carries the latest explicit response feedback forward. _append_user_feedback( runtime_context_parts, @@ -817,28 +761,16 @@ def build_brain_context( context, ) - # Appended delayed memory block: pins the selected delayed memory report. - appended_delayed_memory_context = ( - build_appended_delayed_memory_context( - context - ) - ) - - if appended_delayed_memory_context: - runtime_context_parts.append( - appended_delayed_memory_context - ) - - # Current appended skills block: lists skills already loaded this turn. - current_appended_skills_context = ( - _build_current_appended_skills_context( + # Loaded delayed memory block: pins the selected delayed memory report. + loaded_delayed_memory_context = ( + build_loaded_delayed_memory_context( context ) ) - if current_appended_skills_context: + if loaded_delayed_memory_context: runtime_context_parts.append( - current_appended_skills_context + loaded_delayed_memory_context ) # L3 memory block: restores previous session state from prior turns. diff --git a/rules/runtime.py b/rules/runtime.py index 15667a8d..715265f7 100644 --- a/rules/runtime.py +++ b/rules/runtime.py @@ -67,22 +67,22 @@ "Propose saving the session when the conversation has reached a stable checkpoint worth restoring later, especially after a substantial task, decision, or coherent phase is complete.\n" "Propose a delayed memory report when a substantial reusable result, analysis, design, or report has crystallized and may be useful to append or continue in another context later.\n" "When contains a coherent cluster of detailed facts that no longer needs to remain always-on, propose consolidating that cluster into one delayed memory report. State which fact keys and ids would be grouped, what the report would preserve, and why moving them would reduce context noise.\n" - "When new long-term facts belong to an existing delayed memory report, propose extending that report instead of creating a duplicate. Name the report and list the exact fact keys and ids that would be appended.\n" + "When new long-term facts belong to an existing delayed memory report, propose extending that report instead of creating a duplicate. Name the report and list the exact fact keys and ids that would be added to the report.\n" "Do not propose moving identity anchors, broad user preferences, core project rules, or facts that remain useful across unrelated topics.\n" "Phrase save-session and delayed-memory proposals as one short natural sentence describing what would be preserved and why it may help. Ask for confirmation and never imply that anything has already been saved.\n" "Do not propose after trivial exchanges, while the idea is still unstable, or merely because the topic changed. Do not repeat a declined or ignored proposal unless meaningful new state has appeared.\n" ) SKILL_ROUTING_RULES = ("\n" - "\n" - "SKILL ROUTING RULES:\n" - "1. For extended tasks (e.g. file creation, console, and much more) determine whether the request requires a skill.\n" - "2. Check for a suitable skill.\n" - "3. Never append skill already presented inside .\n" - "4. If no skill is present, you must use the enabled LIST_SKILLS runtime action.\n" - "5. If no specific skills are listed in — you must use the enabled LIST_SKILLS runtime action.\n" - "\n" - "Do not derive skill capabilities from a skill name or filename, you must append it first!\n" + "\n" + "SKILL ROUTING RULES:\n" + "1. For extended tasks (e.g. file creation, console, and much more) determine whether the request requires a skill.\n" + "2. Check for available project skills and their loaded status.\n" + "3. If a relevant skill is available but not loaded, use LOAD_SKILL before using its capabilities.\n" + "4. Never load a skill already marked as loaded in .\n" + "5. Use UNLOAD_SKILL when a loaded skill is no longer needed in the current runtime context.\n" + "\n" + "Do not derive skill capabilities from a skill name or filename; load the skill first and use its loaded content.\n" "\n" "SEQUENCE RULES:\n" "1. Determine whether the CURRENT_SEQUENCE latest action or actions satisfies the original request at the top of CURRENT_SEQUENCE.\n" diff --git a/runtime/L4_memory.py b/runtime/L4_memory.py index ab5ff021..5ee2cd98 100644 --- a/runtime/L4_memory.py +++ b/runtime/L4_memory.py @@ -406,10 +406,10 @@ def get_loaded_delayed_memory_reports( ) -> dict: from utils.brain_client_utils import ( - get_appended_delayed_memory_report, + get_loaded_delayed_memory_reports, ) - return get_appended_delayed_memory_report( + return get_loaded_delayed_memory_reports( context ) @@ -688,9 +688,9 @@ def remap_delayed_memory_l4_fact_ids( "file_errors": [], } - from utils.brain_client_utils import get_appended_delayed_memory_report + from utils.brain_client_utils import get_loaded_delayed_memory_reports - appended_reports = get_appended_delayed_memory_report(context) + loaded_reports = get_loaded_delayed_memory_reports(context) changed_reports = {} removed_report_refs = [] @@ -760,8 +760,8 @@ def remap_delayed_memory_l4_fact_ids( reports[report_id] = updated_report changed_reports[report_id] = updated_report - if report_id in appended_reports: - appended_reports[report_id] = { + if report_id in loaded_reports: + loaded_reports[report_id] = { **updated_report, "id": report_id, } @@ -894,9 +894,9 @@ def restore_delayed_memory_l4_fact_refs( "file_errors": [], } - from utils.brain_client_utils import get_appended_delayed_memory_report + from utils.brain_client_utils import get_loaded_delayed_memory_reports - appended_reports = get_appended_delayed_memory_report(context) + loaded_reports = get_loaded_delayed_memory_reports(context) changed_reports = {} missing_report_ids = [] @@ -951,8 +951,8 @@ def restore_delayed_memory_l4_fact_refs( reports[report_id] = updated_report changed_reports[report_id] = updated_report - if report_id in appended_reports: - appended_reports[report_id] = { + if report_id in loaded_reports: + loaded_reports[report_id] = { **updated_report, "id": report_id, } diff --git a/runtime/L4_memory_rules.py b/runtime/L4_memory_rules.py index 394f5111..9a3b94f1 100644 --- a/runtime/L4_memory_rules.py +++ b/runtime/L4_memory_rules.py @@ -161,7 +161,7 @@ - Different wording or keys do not make a fact different. - A person may validly have several compatible roles; do not turn overlap into a contradiction. -- Harmless repetition between L4 and an appended report does not require an L4 +- Harmless repetition between L4 and a loaded report does not require an L4 change. - Keep separate durable facts separate when they express independent ideas. - Merge selected facts only when one canonical fact can represent their complete diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 48965a98..5f0b93ad 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -97,11 +97,11 @@ class RuntimeContext: runtime_delayed_memory_action_sequence: int = 0 - runtime_appended_delayed_memory: dict = field( + runtime_loaded_delayed_memory: dict = field( default_factory=dict ) - runtime_appended_delayed_memory_ids: list[str] = field( + runtime_loaded_delayed_memory_ids: list[str] = field( default_factory=list ) @@ -115,7 +115,7 @@ class RuntimeContext: delayed_memory_file_store_enabled: bool = True - runtime_appended_skills: list[dict] = field( + runtime_loaded_skills: list[dict] = field( default_factory=list ) diff --git a/runtime/stream.py b/runtime/stream.py index b262f3a0..cb40c12e 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -44,7 +44,7 @@ ) from contracts.rules_assembler import ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_APPEND_SKILL, + RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, @@ -149,7 +149,7 @@ def __init__( self.filter_runtime_actions_enabled = filter_runtime_actions if self.filter_runtime_actions_enabled: self.context.runtime_skill_state_barrier_active = False - self.append_skill_marker_names = self.build_appended_skill_name_set() + self.load_skill_marker_names = self.build_loaded_skill_name_set() self.repetition_guard = RuntimeActionRepetitionGuard() self.action_counter = RuntimeActionCounter() self.marker_repetition_aborted = False @@ -188,14 +188,14 @@ def __init__( ), ) - def build_appended_skill_name_set(self) -> set[str]: + def build_loaded_skill_name_set(self) -> set[str]: names = set() for skill in ( getattr( self.context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] @@ -228,7 +228,7 @@ def should_preserve_action_marker( action, ) -> bool: - if action.name != RUNTIME_ACTION_APPEND_SKILL: + if action.name != RUNTIME_ACTION_LOAD_SKILL: return False requested_skill = normalize_skill_name( @@ -238,10 +238,10 @@ def should_preserve_action_marker( if not requested_skill: return False - if requested_skill in self.append_skill_marker_names: + if requested_skill in self.load_skill_marker_names: return True - self.append_skill_marker_names.add( + self.load_skill_marker_names.add( requested_skill ) @@ -650,10 +650,17 @@ def mark_validator_interruption( or "Runtime stream validator interrupted generation." ) - quote = getattr( - validator, - "last_failure_preview", - "", + quote = ( + getattr( + validator, + "last_failure_loop_preview", + "", + ) + or getattr( + validator, + "last_failure_preview", + "", + ) ) self.context.runtime_turn_interruption_reason = reason diff --git a/tests/runtime_actions/test_active_memory.py b/tests/runtime_actions/test_active_memory.py index 6a7019cf..46967e5a 100644 --- a/tests/runtime_actions/test_active_memory.py +++ b/tests/runtime_actions/test_active_memory.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -36,7 +36,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context diff --git a/tests/runtime_actions/test_asset_actions.py b/tests/runtime_actions/test_asset_actions.py index 80819d3c..0d32c593 100644 --- a/tests/runtime_actions/test_asset_actions.py +++ b/tests/runtime_actions/test_asset_actions.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -36,7 +36,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context diff --git a/tests/runtime_actions/test_delayed_memory.py b/tests/runtime_actions/test_delayed_memory.py index 98dd425e..91bb3ab5 100644 --- a/tests/runtime_actions/test_delayed_memory.py +++ b/tests/runtime_actions/test_delayed_memory.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from runtime.stream import RuntimeStream from tests.helpers.runtime_actions import ( FakeContext, @@ -38,7 +38,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, build_delayed_memory_report, flush_pending_active_memory_resolve_failure_history, include_pinned_delayed_memory_reports, @@ -625,11 +625,11 @@ def test_apply_runtime_action_calls_saves_delayed_memory_report(self): "2026-06-29T12:00:00", ) self.assertEqual( - report["appended_times"], + report["loaded_times"], 0, ) self.assertEqual( - report["append_streak"], + report["load_streak"], 0, ) self.assertEqual( @@ -928,7 +928,7 @@ def test_apply_runtime_action_calls_suffixes_duplicate_delayed_memory_key(self): ) - def test_append_delayed_memory_uses_appended_context_block(self): + def test_load_delayed_memory_uses_loaded_context_block(self): Emitter = FakeEmitter @@ -954,11 +954,11 @@ def test_append_delayed_memory_uses_appended_context_block(self): "created_session_id": "session-a", "created_time": "2026-08-02T19:51:41.803270", "created_date": "2026-08-02T19:51:41.803270", - "appended_times": 1, - "append_streak": 1, - "last_appended_date": "2026-08-02T19:57:42.787241", - "last_appended_session_id": "session-a", - "all_appended_session_ids": [ + "loaded_times": 1, + "load_streak": 1, + "last_loaded_date": "2026-08-02T19:57:42.787241", + "last_loaded_session_id": "session-a", + "all_loaded_session_ids": [ "session-a", ], }, @@ -995,22 +995,22 @@ def test_append_delayed_memory_uses_appended_context_block(self): tool_results, "\n", ) - appended_context = build_appended_delayed_memory_context( + loaded_context = build_loaded_delayed_memory_context( context ) self.assertIn( - "", - appended_context, + "", + loaded_context, ) self.assertIn( '"id": "a1b2c3"', - appended_context, + loaded_context, ) self.assertLess( - appended_context.index( + loaded_context.index( '"id": "a1b2c3"', ), - appended_context.index( + loaded_context.index( '"title":', ), ) @@ -1019,15 +1019,15 @@ def test_append_delayed_memory_uses_appended_context_block(self): "created_session_id", "created_time", "created_date", - "appended_times", - "append_streak", - "last_appended_date", - "last_appended_session_id", - "all_appended_session_ids", + "loaded_times", + "load_streak", + "last_loaded_date", + "last_loaded_session_id", + "all_loaded_session_ids", ): self.assertNotIn( metadata_key, - appended_context, + loaded_context, ) self.assertEqual( context.emitter.events[0]["text"], @@ -1053,7 +1053,7 @@ def test_append_delayed_memory_uses_appended_context_block(self): ) - def test_started_append_delayed_memory_events_are_report_scoped(self): + def test_started_load_delayed_memory_events_are_report_scoped(self): context = SimpleNamespace( emitter=FakeEmitter(), @@ -1123,7 +1123,7 @@ def test_started_append_delayed_memory_events_are_report_scoped(self): ) - def test_append_delayed_memory_keeps_multiple_reports(self): + def test_load_delayed_memory_keeps_multiple_reports(self): Emitter = FakeEmitter @@ -1180,7 +1180,7 @@ def test_append_delayed_memory_keeps_multiple_reports(self): ) self.assertEqual( set( - context.runtime_appended_delayed_memory + context.runtime_loaded_delayed_memory ), { "a1b2c3", @@ -1188,22 +1188,22 @@ def test_append_delayed_memory_keeps_multiple_reports(self): }, ) - appended_context = build_appended_delayed_memory_context( + loaded_context = build_loaded_delayed_memory_context( context ) self.assertEqual( - appended_context.count( - "" + loaded_context.count( + "" ), 2, ) self.assertIn( '"title": "First report"', - appended_context, + loaded_context, ) self.assertIn( '"title": "Second report"', - appended_context, + loaded_context, ) tool_results = build_tool_results_context( @@ -1214,7 +1214,7 @@ def test_append_delayed_memory_keeps_multiple_reports(self): tool_results, ) self.assertNotIn( - "", + "", tool_results, ) self.assertEqual( @@ -1254,7 +1254,7 @@ def test_append_delayed_memory_keeps_multiple_reports(self): ) - def test_invalid_append_delayed_memory_id_returns_failure_tool_result(self): + def test_invalid_load_delayed_memory_id_returns_failure_tool_result(self): Emitter = FakeEmitter @@ -1327,7 +1327,7 @@ def test_invalid_append_delayed_memory_id_returns_failure_tool_result(self): ) - def test_append_delayed_memory_tracks_session_metadata(self): + def test_load_delayed_memory_tracks_session_metadata(self): Emitter = FakeEmitter @@ -1375,11 +1375,11 @@ def test_append_delayed_memory_tracks_session_metadata(self): ) report = context.delayed_memory_reports["a1b2c3"] self.assertEqual( - report["appended_times"], + report["loaded_times"], 1, ) self.assertEqual( - report["append_streak"], + report["load_streak"], 1, ) self.assertEqual( @@ -1387,21 +1387,21 @@ def test_append_delayed_memory_tracks_session_metadata(self): "2026-07-16T10:00:00+03:00", ) self.assertEqual( - report["last_appended_date"], + report["last_loaded_date"], "2026-07-17T19:40:00+03:00", ) self.assertEqual( - report["last_appended_session_id"], + report["last_loaded_session_id"], "session-a", ) self.assertEqual( - report["all_appended_session_ids"], + report["all_loaded_session_ids"], [ "session-a", ], ) self.assertEqual( - context.runtime_appended_delayed_memory_ids, + context.runtime_loaded_delayed_memory_ids, [ "a1b2c3", ], @@ -1433,19 +1433,19 @@ def test_append_delayed_memory_tracks_session_metadata(self): report = next_context.delayed_memory_reports["a1b2c3"] self.assertEqual( - report["appended_times"], + report["loaded_times"], 2, ) self.assertEqual( - report["append_streak"], + report["load_streak"], 2, ) self.assertEqual( - report["last_appended_session_id"], + report["last_loaded_session_id"], "session-b", ) self.assertEqual( - report["all_appended_session_ids"], + report["all_loaded_session_ids"], [ "session-a", "session-b", @@ -1453,7 +1453,7 @@ def test_append_delayed_memory_tracks_session_metadata(self): ) - def test_remove_delayed_memory_only_detaches_from_context(self): + def test_unload_delayed_memory_only_detaches_from_context(self): Emitter = FakeEmitter @@ -1465,7 +1465,7 @@ def test_remove_delayed_memory_only_detaches_from_context(self): context.runtime_search_calls = [] context.runtime_appended_skills = [] context.runtime_asset_results = [] - context.runtime_appended_delayed_memory = { + context.runtime_loaded_delayed_memory = { "id": "a1b2c3", "title": "Pinned report", "summary": "Summary", @@ -1498,7 +1498,7 @@ def test_remove_delayed_memory_only_detaches_from_context(self): 1, ) self.assertEqual( - context.runtime_appended_delayed_memory, + context.runtime_loaded_delayed_memory, {}, ) self.assertIn( @@ -1528,7 +1528,7 @@ def test_remove_delayed_memory_only_detaches_from_context(self): ) - def test_remove_delayed_memory_detaches_multiple_reports(self): + def test_unload_delayed_memory_detaches_multiple_reports(self): Emitter = FakeEmitter @@ -1540,7 +1540,7 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): context.runtime_search_calls = [] context.runtime_appended_skills = [] context.runtime_asset_results = [] - context.runtime_appended_delayed_memory = { + context.runtime_loaded_delayed_memory = { "a1b2c3": { "id": "a1b2c3", "title": "First report", @@ -1554,7 +1554,7 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): "title": "Third report", }, } - context.runtime_appended_delayed_memory_ids = [ + context.runtime_loaded_delayed_memory_ids = [ "a1b2c3", "b2c3d4", "c3d4e5", @@ -1593,14 +1593,14 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): ) self.assertEqual( set( - context.runtime_appended_delayed_memory + context.runtime_loaded_delayed_memory ), { "c3d4e5", }, ) self.assertEqual( - context.runtime_appended_delayed_memory_ids, + context.runtime_loaded_delayed_memory_ids, [ "c3d4e5", ], @@ -1628,7 +1628,7 @@ def test_remove_delayed_memory_detaches_multiple_reports(self): - def test_invalid_remove_delayed_memory_id_returns_failed_result(self): + def test_invalid_unload_delayed_memory_id_returns_failed_result(self): Emitter = FakeEmitter @@ -1712,7 +1712,7 @@ def test_invalid_remove_delayed_memory_id_returns_failed_result(self): ) - def test_missing_remove_delayed_memory_id_returns_failed_result(self): + def test_missing_unload_delayed_memory_id_returns_failed_result(self): Emitter = FakeEmitter @@ -2000,7 +2000,7 @@ def test_pinned_delayed_memory_is_included_once_per_turn(self): "pinned": True, }, }, - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, runtime_current_turn_id="turn-1", runtime_pinned_delayed_memory_turns={}, session_id="session-1", @@ -2014,7 +2014,7 @@ def test_pinned_delayed_memory_is_included_once_per_turn(self): self.assertIn("a1b2c3", first) self.assertIn("a1b2c3", second) self.assertEqual( - context.delayed_memory_reports["a1b2c3"]["appended_times"], + context.delayed_memory_reports["a1b2c3"]["loaded_times"], 1, ) @@ -2023,11 +2023,11 @@ def test_pinned_delayed_memory_is_included_once_per_turn(self): include_pinned_delayed_memory_reports(context) self.assertEqual( - context.delayed_memory_reports["a1b2c3"]["appended_times"], + context.delayed_memory_reports["a1b2c3"]["loaded_times"], 2, ) self.assertEqual( - context.delayed_memory_reports["a1b2c3"]["last_appended_date"], + context.delayed_memory_reports["a1b2c3"]["last_loaded_date"], "2026-08-02T20:01:00", ) @@ -2041,7 +2041,7 @@ def test_remove_pinned_delayed_memory_returns_failed_result(self): context.runtime_appended_skills = [] context.runtime_asset_results = [] context.runtime_delayed_memory_results = [] - context.runtime_appended_delayed_memory = { + context.runtime_loaded_delayed_memory = { "a1b2c3": { "id": "a1b2c3", "title": "Pinned report", @@ -2081,5 +2081,5 @@ def test_remove_pinned_delayed_memory_returns_failed_result(self): ) self.assertIn( "a1b2c3", - context.runtime_appended_delayed_memory, + context.runtime_loaded_delayed_memory, ) diff --git a/tests/runtime_actions/test_jin_color.py b/tests/runtime_actions/test_jin_color.py index 1da7739c..f008b28e 100644 --- a/tests/runtime_actions/test_jin_color.py +++ b/tests/runtime_actions/test_jin_color.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -36,7 +36,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context diff --git a/tests/runtime_actions/test_skill_actions.py b/tests/runtime_actions/test_skill_actions.py index 2797ed71..0c5d8507 100644 --- a/tests/runtime_actions/test_skill_actions.py +++ b/tests/runtime_actions/test_skill_actions.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -36,7 +36,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context diff --git a/tests/runtime_actions/test_stream_filter.py b/tests/runtime_actions/test_stream_filter.py index 2c14cc7d..9703dab6 100644 --- a/tests/runtime_actions/test_stream_filter.py +++ b/tests/runtime_actions/test_stream_filter.py @@ -16,7 +16,7 @@ get_runtime_action_private_marker, normalize_runtime_action_names, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -37,7 +37,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context @@ -1737,7 +1737,7 @@ def test_recorded_tool_results_persist_then_append_in_order(self): TOOL_RESULT_KIND_DELAYED_MEMORY, { "ok": False, - "action": "remove_delayed_memory", + "action": "unload_delayed_memory", "failure": "No entries found.", }, ) @@ -1774,7 +1774,7 @@ def test_failed_tool_results_dedupe_ignores_volatile_result_id(self): begin_runtime_tool_results_turn( context ) - append_delayed_memory_runtime_result( + record_delayed_memory_runtime_result( context, { "ok": False, @@ -1789,7 +1789,7 @@ def test_failed_tool_results_dedupe_ignores_volatile_result_id(self): "runtime_turn_id": "turn_000001", }, ) - append_delayed_memory_runtime_result( + record_delayed_memory_runtime_result( context, { "runtime_turn_id": "turn_000001", @@ -1872,7 +1872,7 @@ def test_clean_tool_results_action_clears_all_result_state(self): ] context.runtime_delayed_memory_results = [ { - "action": "append_delayed_memory", + "action": "load_delayed_memory", }, ] applied_count = asyncio.run( diff --git a/tests/runtime_actions/test_todo.py b/tests/runtime_actions/test_todo.py index a22a50b9..56f3f577 100644 --- a/tests/runtime_actions/test_todo.py +++ b/tests/runtime_actions/test_todo.py @@ -15,7 +15,7 @@ RUNTIME_ACTION_JIN_COLOR, get_runtime_action_private_marker, ) -from rules.brain_context_builder import build_appended_delayed_memory_context +from rules.brain_context_builder import build_loaded_delayed_memory_context from tests.helpers.runtime_actions import ( FakeContext, FakeEmitter, @@ -36,7 +36,7 @@ ) from utils.assets_utils import run_asset_action from utils.brain_client_utils import ( - append_delayed_memory_runtime_result, + record_delayed_memory_runtime_result, flush_pending_active_memory_resolve_failure_history, ) from utils.context.context_exports import build_tool_results_context diff --git a/tests/test_answer_rating_client_contract.py b/tests/test_answer_rating_client_contract.py index 59dedf5c..ebf9de7c 100644 --- a/tests/test_answer_rating_client_contract.py +++ b/tests/test_answer_rating_client_contract.py @@ -75,7 +75,7 @@ def test_cache_versions_are_bumped_for_rating_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn("/static/css/chat-rating.css?v=rating-persist-1", source) - self.assertIn("/static/css/theme-win95.css?v=rating-persist-1", source) + self.assertIn("/static/css/theme-win95.css?v=delayed-context-loaded-1", source) self.assertIn("/static/js/answer-rating.js?v=answer-rating-persist-1", source) self.assertIn( "/static/js/runtime/runtime-feedback.js?v=runtime-feedback-persist-1", diff --git a/tests/test_brain_asset_flow.py b/tests/test_brain_asset_flow.py index e1b70e10..18867217 100644 --- a/tests/test_brain_asset_flow.py +++ b/tests/test_brain_asset_flow.py @@ -20,7 +20,7 @@ prepare_asset_results_for_turn, ) from rules.brain_context_builder import ( - build_appended_delayed_memory_context, + build_loaded_delayed_memory_context, ) from utils.context.context_exports import ( build_tool_results_context, @@ -286,7 +286,7 @@ async def test_followup_places_confirm_result_inside_tool_results(self): context = SimpleNamespace( runtime_action_failure_followup_messages=[message], runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) prompt = BrainNode.build_followup_system_prompt( @@ -341,7 +341,7 @@ async def test_current_sequence_starts_with_original_user_message(self): }, ], runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) with patch( @@ -463,7 +463,7 @@ async def test_followup_consumes_reasoning_recovery_block(self): ), runtime_turn_interruption_quote="looped sentence", runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) prompt = BrainNode.build_followup_system_prompt( @@ -505,7 +505,7 @@ async def test_followup_consumes_context_limit_recovery_block(self): ), runtime_turn_interruption_quote="", runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) prompt = BrainNode.build_followup_system_prompt( @@ -599,7 +599,7 @@ async def test_followup_runtime_memory_tag_uses_sequence_started_at(self): context.runtime_current_turn_id = "turn_000001" context.runtime_action_sequence_turn_ids = [] context.runtime_session_action_history = [] - context.runtime_appended_delayed_memory = {} + context.runtime_loaded_delayed_memory = {} with patch( "agent.nodes.brain.time.time", @@ -649,11 +649,11 @@ async def test_followup_renames_runtime_memory_block(self): prompt, ) - async def test_appended_delayed_memory_is_under_latest_request(self): + async def test_loaded_delayed_memory_is_under_latest_request(self): context = SimpleNamespace( runtime_recent_turns=[], - runtime_appended_delayed_memory={ + runtime_loaded_delayed_memory={ "id": "a1b2c3", "title": "Pinned delayed report", "summary": "Summary", @@ -666,8 +666,8 @@ async def test_appended_delayed_memory_is_under_latest_request(self): context=context, ) - appended_delayed_memory = ( - build_appended_delayed_memory_context( + loaded_delayed_memory = ( + build_loaded_delayed_memory_context( context ) ) @@ -690,7 +690,7 @@ async def test_appended_delayed_memory_is_under_latest_request(self): prompt, ) self.assertIn( - appended_delayed_memory, + loaded_delayed_memory, prompt, ) self.assertNotIn( @@ -702,7 +702,7 @@ async def test_appended_delayed_memory_is_under_latest_request(self): "" ), prompt.index( - appended_delayed_memory + loaded_delayed_memory ), ) @@ -730,7 +730,7 @@ async def test_followup_places_current_sequence_under_latest_request(self): }, ], runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) base_prompt = ( "\nstate\n\n\n" @@ -804,7 +804,7 @@ async def test_idle_followup_keeps_original_sequence_action_history(self): }, ], runtime_recent_turns=[], - runtime_appended_delayed_memory={}, + runtime_loaded_delayed_memory={}, ) with patch( @@ -2668,7 +2668,7 @@ async def fake_run_brain_stream(**kwargs): }, ] context.runtime_recent_turns = [] - context.runtime_appended_delayed_memory = {} + context.runtime_loaded_delayed_memory = {} state = AgentState( user_input=origin_request, diff --git a/tests/test_brain_runtime_actions.py b/tests/test_brain_runtime_actions.py index 3a7bff79..2c93ab8b 100644 --- a/tests/test_brain_runtime_actions.py +++ b/tests/test_brain_runtime_actions.py @@ -1802,7 +1802,7 @@ def test_payload_distinct_save_delayed_history_uses_separate_parts(self): ], ) - def test_append_delayed_history_splits_by_raw_id(self): + def test_load_delayed_history_splits_by_raw_id(self): context = SimpleNamespace( runtime_session_action_history=[], @@ -1842,7 +1842,7 @@ def test_append_delayed_history_splits_by_raw_id(self): ], ) - def test_remove_delayed_history_splits_by_raw_id(self): + def test_unload_delayed_history_splits_by_raw_id(self): context = SimpleNamespace( runtime_session_action_history=[], @@ -3496,14 +3496,14 @@ def test_prompt_places_tool_results_at_context_top(self): prompt, ) - def test_prompt_keeps_appended_delayed_memory_in_normal_turns(self): + def test_prompt_keeps_loaded_delayed_memory_in_normal_turns(self): context = SimpleNamespace( runtime_memory="session_status: active", runtime_memory_stable="session_status: active", runtime_l2_memory="", active_memory_records=[], - runtime_appended_delayed_memory={ + runtime_loaded_delayed_memory={ "id": "a1b2c3", "title": "Pinned task plan", "summary": "Use this plan for the next task.", @@ -3519,7 +3519,7 @@ def test_prompt_keeps_appended_delayed_memory_in_normal_turns(self): ) self.assertIn( - "", + "", prompt, ) self.assertIn( @@ -3528,13 +3528,13 @@ def test_prompt_keeps_appended_delayed_memory_in_normal_turns(self): ) self.assertEqual( prompt.count( - "" + "" ), 1, ) self.assertLess( prompt.index( - "" + "" ), prompt.index( "I identify myself as JIN" diff --git a/tests/test_delayed_memory_client_contract.py b/tests/test_delayed_memory_client_contract.py index 84a57543..5605d8ba 100644 --- a/tests/test_delayed_memory_client_contract.py +++ b/tests/test_delayed_memory_client_contract.py @@ -7,7 +7,7 @@ class DelayedMemoryClientContractTests(unittest.TestCase): - def test_remove_delayed_memory_does_not_rewrite_saved_reports(self): + def test_unload_delayed_memory_does_not_rewrite_saved_reports(self): source = ( ROOT @@ -29,7 +29,7 @@ def test_remove_delayed_memory_does_not_rewrite_saved_reports(self): r"delete\s+reports\s*\[", ) - def test_delayed_memory_append_metadata_is_persisted_client_side(self): + def test_delayed_memory_load_metadata_is_persisted_client_side(self): storage_source = ( ROOT @@ -53,19 +53,19 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): ) self.assertIn( - "appended_times", + "loaded_times", storage_source, ) self.assertIn( - "append_streak", + "load_streak", storage_source, ) self.assertIn( - "last_appended_session_id", + "last_loaded_session_id", storage_source, ) self.assertIn( - "all_appended_session_ids", + "all_loaded_session_ids", storage_source, ) self.assertIn( @@ -73,11 +73,7 @@ def test_delayed_memory_append_metadata_is_persisted_client_side(self): storage_source, ) self.assertIn( - "collectCurrentSessionAppendedMemoryIds", - storage_source, - ) - self.assertIn( - 'action === "append_delayed_memory"', + 'action === "load_delayed_memory"', runtime_actions_source, ) self.assertIn( @@ -161,11 +157,11 @@ def test_delayed_memory_delete_sync_and_loaded_state_are_registered(self): socket_delayed_source, ) self.assertIn( - 'action === "append_delayed_memory"', + 'action === "load_delayed_memory"', runtime_actions_source, ) self.assertIn( - 'action === "remove_delayed_memory"', + 'action === "unload_delayed_memory"', runtime_actions_source, ) self.assertIn( @@ -173,7 +169,32 @@ def test_delayed_memory_delete_sync_and_loaded_state_are_registered(self): runtime_actions_source, ) - def test_appended_delayed_memory_bubbles_open_their_own_reports(self): + def test_context_loaded_delayed_memory_highlights_report_and_linked_l4(self): + + runtime_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime.js" + ).read_text(encoding="utf-8") + memory_view_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-memory-view.js" + ).read_text(encoding="utf-8") + avatar_source = ( + ROOT / "ui" / "static" / "js" / "runtime" / "runtime-avatar.js" + ).read_text(encoding="utf-8") + socket_source = ( + ROOT / "ui" / "static" / "js" / "socket" / "delayed-memory.js" + ).read_text(encoding="utf-8") + + self.assertIn("getLoadedDelayedMemoryReportIds", runtime_source) + self.assertIn("replaceLoadedDelayedMemoryReportIds", runtime_source) + self.assertIn("runtime-memory-context-loaded-hit", memory_view_source) + self.assertIn("getContextLoadedDelayedMemoryFactIds", memory_view_source) + self.assertIn("isDelayedMemoryReportInContext", memory_view_source) + self.assertIn("is-context-loaded", avatar_source) + self.assertIn("collectDelayedMemoryLinkedL4FactIds", avatar_source) + self.assertIn("loaded_delayed_memory_ids", socket_source) + self.assertIn("replaceLoadedDelayedMemoryReportIds", socket_source) + + def test_loaded_delayed_memory_bubbles_open_their_own_reports(self): runtime_actions_source = ( ROOT @@ -237,7 +258,7 @@ def test_appended_delayed_memory_bubbles_open_their_own_reports(self): runtime_actions_source, ) self.assertIn( - '"append_delayed_memory",', + '"load_delayed_memory",', chat_actions_source, ) self.assertGreaterEqual( @@ -484,7 +505,7 @@ def test_deferred_runtime_actions_fade_with_message_scope(self): flush_block, ) - def test_session_snapshot_history_and_appended_ids_are_persisted(self): + def test_session_snapshot_history_and_loaded_ids_are_persisted(self): storage_source = ( ROOT @@ -520,11 +541,11 @@ def test_session_snapshot_history_and_appended_ids_are_persisted(self): storage_source, ) self.assertIn( - "appended_memory_ids", + "loaded_memory_ids", session_source, ) self.assertIn( - "collectCurrentSessionAppendedMemoryIds()", + "getLoadedDelayedMemoryReportIds()", session_source, ) diff --git a/tests/test_delayed_memory_file_store.py b/tests/test_delayed_memory_file_store.py index 1fbef9cd..b7d13b3c 100644 --- a/tests/test_delayed_memory_file_store.py +++ b/tests/test_delayed_memory_file_store.py @@ -42,7 +42,7 @@ def build_report( "created_session_id": "session-a", "created_time": "2026-07-19T16:14:14.628194", "created_date": "2026-07-19T16:14:14.628194", - "all_appended_session_ids": [ + "all_loaded_session_ids": [ "session-a", ], } @@ -397,7 +397,7 @@ def test_websocket_sync_deletes_report_by_id(self): title="Kept report", ), }, - runtime_appended_delayed_memory={ + runtime_loaded_delayed_memory={ "48ggds": { **self.build_report( title="Deleted report", @@ -405,7 +405,7 @@ def test_websocket_sync_deletes_report_by_id(self): "id": "48ggds", }, }, - runtime_appended_delayed_memory_ids=[ + runtime_loaded_delayed_memory_ids=[ "48ggds", "a1b2c3", ], @@ -437,10 +437,10 @@ def test_websocket_sync_deletes_report_by_id(self): ) self.assertNotIn( "48ggds", - context.runtime_appended_delayed_memory, + context.runtime_loaded_delayed_memory, ) self.assertEqual( - context.runtime_appended_delayed_memory_ids, + context.runtime_loaded_delayed_memory_ids, [ "a1b2c3", ], @@ -460,7 +460,7 @@ def test_file_payload_accepts_example_aliases(self): "id": "48ggds", "session": "session-a", "created_date": "2026-07-19T16:14:14.628194", - "all_appended_session_ids": [], + "all_loaded_session_ids": [], "body": "Body", }, ) diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index b4c7fc6b..d2262fd5 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -47,8 +47,8 @@ def test_modal_icon_sizes_are_bumped_for_close_and_pin_controls(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=delayed-delete-1", source) - self.assertIn("/static/js/runtime/runtime.js?v=delayed-delete-1", source) + self.assertIn("/static/css/runtime-memory.css?v=delayed-context-loaded-1", source) + self.assertIn("/static/js/runtime/runtime.js?v=delayed-context-loaded-1", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index c3dd316f..9698f7c0 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -86,7 +86,7 @@ def test_delete_marks_local_l4_store_deleted_before_sync(self): def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn('/static/css/runtime-memory.css?v=delayed-delete-1', source) + self.assertIn('/static/css/runtime-memory.css?v=delayed-context-loaded-1', source) self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=delayed-loaded-facts-1', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) diff --git a/tests/test_l4_memory.py b/tests/test_l4_memory.py index b1800e84..09e4d183 100644 --- a/tests/test_l4_memory.py +++ b/tests/test_l4_memory.py @@ -1217,7 +1217,7 @@ def test_loaded_delayed_report_fact_ids_are_visible_with_report_suffix(self): ], }, } - context.runtime_appended_delayed_memory = { + context.runtime_loaded_delayed_memory = { "abc123": { **context.delayed_memory_reports["abc123"], "id": "abc123", diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index e9b5e489..741657fa 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -105,16 +105,17 @@ def test_win95_keeps_reference_and_citation_hits_consistent(self): css = WIN95_CSS.read_text(encoding="utf-8") self.assertIn(".runtime-memory-reference-hit,", css) - self.assertIn(".runtime-memory-citation-hit {", css) + self.assertIn(".runtime-memory-citation-hit,", css) + self.assertIn(".runtime-memory-context-loaded-hit {", css) self.assertIn(".runtime-memory-citation-hit .runtime-memory-key", css) def test_cache_versions_are_bumped_for_citation_sync_assets(self): source = INDEX_HTML.read_text(encoding="utf-8") - self.assertIn("/static/css/runtime-memory.css?v=delayed-delete-1", source) - self.assertIn("/static/css/theme-win95.css?v=rating-persist-1", source) - self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-delete-1", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-18", source) + self.assertIn("/static/css/runtime-memory.css?v=delayed-context-loaded-1", source) + self.assertIn("/static/css/theme-win95.css?v=delayed-context-loaded-1", source) + self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-19", source) self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) diff --git a/tests/test_memory_reference_sync_client_contract.py b/tests/test_memory_reference_sync_client_contract.py index 3664c697..4765c413 100644 --- a/tests/test_memory_reference_sync_client_contract.py +++ b/tests/test_memory_reference_sync_client_contract.py @@ -353,6 +353,18 @@ def test_delayed_memory_modal_delete_uses_hold_and_runtime_restore_payload(self) ".delayed-memory-modal-delete svg", css_source, ) + self.assertIn( + "deleteTimer = null;", + source, + ) + self.assertIn( + "setRuntimeMemoryRowPressVisual(\n row,\n false\n );", + source, + ) + self.assertIn( + 'delayedMemoryModalDeleteButton.style.removeProperty(\n "opacity"', + source, + ) @unittest.skipUnless( shutil.which("node"), @@ -410,15 +422,15 @@ def test_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - '/static/css/runtime-memory.css?v=delayed-delete-1', + '/static/css/runtime-memory.css?v=delayed-context-loaded-1', source, ) self.assertIn( - '/static/js/runtime/runtime-memory-view.js?v=delayed-delete-1', + '/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1', source, ) self.assertIn( - '/static/js/runtime/runtime.js?v=delayed-delete-1', + '/static/js/runtime/runtime.js?v=delayed-context-loaded-1', source, ) self.assertIn( diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 1478dd8c..20e4f796 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -72,7 +72,7 @@ def test_l4_facts_are_rendered_as_reference_aware_memory_dashes(self): self.assertIn("@keyframes jin-avatar-memory-absorb-dot", css_source) self.assertIn("rgba(147, 197, 253, 0.30)", css_source) - def test_l4_ring_is_appended_inside_delayed_and_active_rings(self): + def test_l4_ring_is_rendered_inside_delayed_and_active_rings(self): source = AVATAR_JS.read_text(encoding="utf-8") start = source.index("function appendMemorySignalRings(") end = source.index("\n function appendDefs(", start) @@ -178,11 +178,11 @@ def test_avatar_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/css/runtime-avatar.css?v=memory-rings-7", + "/static/css/runtime-avatar.css?v=memory-rings-8", source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-18", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-19", source, ) self.assertIn( diff --git a/tests/test_stream_validator.py b/tests/test_stream_validator.py index 659b926d..785c0394 100644 --- a/tests/test_stream_validator.py +++ b/tests/test_stream_validator.py @@ -246,7 +246,7 @@ def test_stream_validator_stops_repeated_sentence_sequence_with_markers(): "* *Wait*, I'll just do the search." ) assert validator.last_failure_loop_preview == ( - validator.last_failure_preview + "* *Wait*, I'll just do the search." ) diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index df4a7e4a..91383bf1 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -291,6 +291,7 @@ .jin-avatar-memory-dash.is-runtime-cited, .jin-avatar-memory-dash.is-memory-reference-hit, +.jin-avatar-memory-dash.is-context-loaded, .jin-avatar-memory-dash.is-delayed-memory-linked-hit { filter: brightness(1.72) @@ -303,6 +304,7 @@ .jin-avatar-memory-dash:not(.is-memory-dot).is-runtime-cited path, .jin-avatar-memory-dash:not(.is-memory-dot).is-memory-reference-hit path, +.jin-avatar-memory-dash:not(.is-memory-dot).is-context-loaded path, .jin-avatar-memory-dash:not(.is-memory-dot).is-delayed-memory-linked-hit path { stroke-opacity: 1 !important; stroke-width: var(--jin-avatar-memory-hover-width, 2px); @@ -310,6 +312,7 @@ .jin-avatar-memory-dash.is-memory-dot.is-runtime-cited circle, .jin-avatar-memory-dash.is-memory-dot.is-memory-reference-hit circle, +.jin-avatar-memory-dash.is-memory-dot.is-context-loaded circle, .jin-avatar-memory-dash.is-memory-dot.is-delayed-memory-linked-hit circle { fill-opacity: 1 !important; r: var(--jin-avatar-memory-hover-dot-radius, 2.45px); diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index 7cd5dbe0..de47404e 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -22,7 +22,9 @@ .runtime-memory-reference-hit .runtime-memory-key, .runtime-memory-reference-hit .runtime-memory-value, .runtime-memory-citation-hit .runtime-memory-key, -.runtime-memory-citation-hit .runtime-memory-value { +.runtime-memory-citation-hit .runtime-memory-value, +.runtime-memory-context-loaded-hit .runtime-memory-key, +.runtime-memory-context-loaded-hit .runtime-memory-value { color: rgba(244, 244, 245, 0.92); text-shadow: 0 0 7px rgba(248, 250, 252, 0.30), @@ -364,7 +366,9 @@ .runtime-memory-reference-hit .runtime-memory-key.flash-new, .runtime-memory-reference-hit .runtime-memory-value.flash-new, .runtime-memory-citation-hit .runtime-memory-key.flash-new, -.runtime-memory-citation-hit .runtime-memory-value.flash-new { +.runtime-memory-citation-hit .runtime-memory-value.flash-new, +.runtime-memory-context-loaded-hit .runtime-memory-key.flash-new, +.runtime-memory-context-loaded-hit .runtime-memory-value.flash-new { color: rgba(134,239,172,0.96); text-shadow: 0 0 10px rgba(34,197,94,0.35), @@ -372,7 +376,8 @@ } .runtime-memory-reference-hit .runtime-memory-key.flash-changed, -.runtime-memory-citation-hit .runtime-memory-key.flash-changed { +.runtime-memory-citation-hit .runtime-memory-key.flash-changed, +.runtime-memory-context-loaded-hit .runtime-memory-key.flash-changed { color: rgba(216,180,254,0.96); text-shadow: 0 0 10px rgba(168,85,247,0.35), @@ -380,7 +385,8 @@ } .runtime-memory-reference-hit .runtime-memory-value.flash-changed, -.runtime-memory-citation-hit .runtime-memory-value.flash-changed { +.runtime-memory-citation-hit .runtime-memory-value.flash-changed, +.runtime-memory-context-loaded-hit .runtime-memory-value.flash-changed { color: rgba( 134, 239, diff --git a/ui/static/css/theme-win95.css b/ui/static/css/theme-win95.css index c9224527..826d2d4b 100644 --- a/ui/static/css/theme-win95.css +++ b/ui/static/css/theme-win95.css @@ -562,7 +562,8 @@ body.theme-win95 #summarizer-window-bar::after { } body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit, -body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit { +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-context-loaded-hit { background: #d7f0ed; box-shadow: inset 0 0 0 1px #6b9e99; } @@ -570,7 +571,9 @@ body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-h body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-key, body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-reference-hit .runtime-memory-value, body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit .runtime-memory-key, -body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit .runtime-memory-value { +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-citation-hit .runtime-memory-value, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-context-loaded-hit .runtime-memory-key, +body.theme-win95 #settings-panel #runtime-memory-text .runtime-memory-context-loaded-hit .runtime-memory-value { color: #005650 !important; text-shadow: none; } diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 75a44804..26f4a9d6 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -1749,7 +1749,7 @@ function updateRuntimeActionRow( if ( [ "save_delayed_memory_content", - "append_delayed_memory", + "load_delayed_memory", ].includes(action) ) { if ( @@ -2304,11 +2304,9 @@ function appendRuntimeAction( icon.textContent = action === "web_search" ? "🔍" - : action === "list_skills" - ? "📘" - : action === "asset_action" - ? "▣" - : "●"; + : action === "asset_action" + ? "▣" + : "●"; if (options.contextSnapshot) { icon.className += @@ -2382,7 +2380,7 @@ function appendRuntimeAction( if ( [ "save_delayed_memory_content", - "append_delayed_memory", + "load_delayed_memory", ].includes(action) ) { if ( diff --git a/ui/static/js/chat.js b/ui/static/js/chat.js index f142de43..97d97d17 100644 --- a/ui/static/js/chat.js +++ b/ui/static/js/chat.js @@ -1231,15 +1231,11 @@ function stripInternalActionMarkers( "$1" ) .replace( - /(^|\n)[^\S\r\n]*\n]*)?>[^\S\r\n]*(?=\n|$)/gi, + /(^|\n)[^\S\r\n]*\n]*>[^\S\r\n]*(?=\n|$)/gi, "$1" ) .replace( - /(^|\n)[^\S\r\n]*\n]*>[^\S\r\n]*(?=\n|$)/gi, - "$1" - ) - .replace( - /(^|\n)[^\S\r\n]*\n]*>[^\S\r\n]*(?=\n|$)/gi, + /(^|\n)[^\S\r\n]*\n]*>[^\S\r\n]*(?=\n|$)/gi, "$1" ) .replace( diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index c4543cd5..6b5e14fb 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -372,10 +372,10 @@ function getInternalActionLogKey( actionName ); const keepSkillMarkerSeparate = [ - "APPEND_SKILL", - "APPEND_SKILLS", - "REMOVE_SKILL", - "REMOVE_SKILLS", + "LOAD_SKILL", + "LOAD_SKILLS", + "UNLOAD_SKILL", + "UNLOAD_SKILLS", ].includes(normalizedActionName); const instanceKey = keepSkillMarkerSeparate ? String( @@ -601,8 +601,8 @@ function log_internal_action( ) || 0 ); const suppressMarkerCount = [ - "APPEND_SKILL", - "APPEND_SKILLS", + "LOAD_SKILL", + "LOAD_SKILLS", ].includes(actionName); const cancelledByUser = String(data.status || "").toLowerCase() === "failed" diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index a79fafa7..dff9a7ca 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -779,6 +779,13 @@ normalizeL4FactIds(report.anchor_fact_ids); const factIds = normalizeL4FactIds(report.facts_ids); + const loaded = + Boolean(report.pinned) + || Boolean( + runtime + && typeof runtime.isDelayedMemoryReportLoaded === "function" + && runtime.isDelayedMemoryReportLoaded(id) + ); if (!id || !title) { return null; @@ -789,6 +796,7 @@ title, summary, pinned: Boolean(report.pinned), + loaded, anchorFactIds, factIds, avatarMemoryHoverId: @@ -911,6 +919,10 @@ classNames.push("is-memory-pinned"); } + if (options.contextLoaded) { + classNames.push("is-context-loaded"); + } + if (options.archived) { classNames.push("is-memory-archived"); } @@ -1163,6 +1175,7 @@ : color, opacity: pinned ? 0.82 : 0.36, pinned, + contextLoaded: Boolean(record.loaded), avatarMemoryHoverId: record.avatarMemoryHoverId, citationKey: normalizeRuntimeCitationIdentity(record.id), @@ -1505,6 +1518,10 @@ serializeL4FactIds(record.factIds) || ""; dashGroup.dataset.delayedMemoryAnchorFactIds = serializeL4FactIds(record.anchorFactIds) || ""; + dashGroup.classList.toggle( + "is-context-loaded", + Boolean(record.loaded) + ); if ( !setDelayedMemoryDashPinned( @@ -2125,7 +2142,8 @@ Array.from( svg.querySelectorAll( - ".jin-avatar-memory-dash-delayed.is-memory-pinned" + ".jin-avatar-memory-dash-delayed.is-memory-pinned, " + + ".jin-avatar-memory-dash-delayed.is-context-loaded" ) ).forEach((node) => { addL4FactIds( @@ -2437,6 +2455,7 @@ record.title, record.summary, record.pinned, + record.loaded, record.anchorFactIds.join(","), record.factIds.join(","), ].join("␟")).join("␞"), diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index d9b87493..9f9f3576 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -11,6 +11,7 @@ let setActiveMemoryRecords = null; let deleteRuntimeMemoryLine = null; let getDelayedMemoryReports = null; + let isDelayedMemoryReportLoaded = null; let setDelayedMemoryReportPinned = null; let setDelayedMemoryReportAnchorFactIds = null; let deleteDelayedMemoryReport = null; @@ -426,10 +427,12 @@ .sort((left, right) => { const leftHighlighted = left.classList.contains("runtime-memory-reference-hit") - || left.classList.contains("runtime-memory-citation-hit"); + || left.classList.contains("runtime-memory-citation-hit") + || left.classList.contains("runtime-memory-context-loaded-hit"); const rightHighlighted = right.classList.contains("runtime-memory-reference-hit") - || right.classList.contains("runtime-memory-citation-hit"); + || right.classList.contains("runtime-memory-citation-hit") + || right.classList.contains("runtime-memory-context-loaded-hit"); if (leftHighlighted !== rightHighlighted) { return leftHighlighted ? -1 : 1; @@ -656,14 +659,14 @@ const leftDate = Date.parse( - left.last_appended_date + left.last_loaded_date || left.created_date || left.created_time || "" ) || 0; const rightDate = Date.parse( - right.last_appended_date + right.last_loaded_date || right.created_date || right.created_time || "" @@ -673,6 +676,47 @@ }); } + function isDelayedMemoryReportInContext(report) { + if ( + !report + || typeof report !== "object" + || Array.isArray(report) + ) { + return false; + } + + if (Boolean(report.pinned)) { + return true; + } + + const reportId = + normalizeDelayedMemoryReportId( + report._storage_key || report.id + ); + + return Boolean( + reportId + && typeof isDelayedMemoryReportLoaded === "function" + && isDelayedMemoryReportLoaded(reportId) + ); + } + + function getContextLoadedDelayedMemoryFactIds() { + const factIds = new Set(); + + getDelayedMemoryReportRecords() + .filter(isDelayedMemoryReportInContext) + .forEach((report) => { + normalizeDelayedMemoryFactIds([ + report.anchor_fact_ids, + report.facts_ids, + report.absorbed_fact_ids, + report.long_term_facts_ids, + ]).forEach(factId => factIds.add(factId)); + }); + + return factIds; + } function setActiveMemoryRecordTexts(records) { if (typeof setActiveMemoryRecords === "function") { setActiveMemoryRecords( @@ -1796,6 +1840,13 @@ normalizeRuntimeCitationIdentity( `${line.key || "note"}: ${line.value || ""}` ); + + if (line && line.context_loaded === true) { + row.classList.add( + "runtime-memory-context-loaded-hit" + ); + } + setMemoryReferenceAliases( row, collectMemoryRecordReferenceAliases(line) @@ -2302,12 +2353,23 @@ clearDeleteTimer(); deleteTimer = setTimeout(() => { + deleteTimer = null; + if (!pointerDown) { return; } deleteCompleted = true; pointerDown = false; + pointerId = null; + + // The same control can be reused after deletion (for example the + // delayed-memory report modal delete button). Do not leave the + // hold-to-delete opacity at zero after the timer completes. + setRuntimeMemoryRowPressVisual( + row, + false + ); if (typeof onDelete === "function") { onDelete(); @@ -2385,6 +2447,12 @@ ); } + if (isDelayedMemoryReportInContext(report)) { + row.classList.add( + "runtime-memory-context-loaded-hit" + ); + } + if (Boolean(report.pinned)) { row.classList.add( "runtime-memory-delayed-row-pinned" @@ -3531,6 +3599,25 @@ delayedMemoryModalReport = { ...resolvedReport, }; + + // This button is a single persistent DOM node reused for every report. + // A completed hold from the previous report must never keep it faded out + // when the next report is opened. + if (delayedMemoryModalDeleteButton) { + delayedMemoryModalDeleteButton.style.removeProperty( + "transition-property" + ); + delayedMemoryModalDeleteButton.style.removeProperty( + "transition-timing-function" + ); + delayedMemoryModalDeleteButton.style.removeProperty( + "transition-duration" + ); + delayedMemoryModalDeleteButton.style.removeProperty( + "opacity" + ); + } + setActiveDelayedMemoryReportRow( delayedMemoryModalReport._storage_key ); @@ -3741,7 +3828,8 @@ function buildLongTermMemoryLine( - fact + fact, + contextLoadedFactIds = new Set() ) { const id = @@ -3769,6 +3857,10 @@ key, value ), + context_loaded: + contextLoadedFactIds.has( + normalizeDelayedMemoryFactId(id) + ), status: "same", key_status: "same", value_status: "same", @@ -3783,9 +3875,15 @@ const records = getLongTermMemoryFactRecords(); + const contextLoadedFactIds = + getContextLoadedDelayedMemoryFactIds(); + const lines = records.map( - buildLongTermMemoryLine + fact => buildLongTermMemoryLine( + fact, + contextLoadedFactIds + ) ); if (runtimeMemoryText) { @@ -4115,6 +4213,8 @@ setActiveMemoryRecords = options.setActiveMemoryRecords || null; deleteRuntimeMemoryLine = options.deleteRuntimeMemoryLine || null; getDelayedMemoryReports = options.getDelayedMemoryReports || null; + isDelayedMemoryReportLoaded = + options.isDelayedMemoryReportLoaded || null; setDelayedMemoryReportPinned = options.setDelayedMemoryReportPinned || null; setDelayedMemoryReportAnchorFactIds = options.setDelayedMemoryReportAnchorFactIds || null; diff --git a/ui/static/js/runtime/runtime-session.js b/ui/static/js/runtime/runtime-session.js index 67eb58f6..ec18ba30 100644 --- a/ui/static/js/runtime/runtime-session.js +++ b/ui/static/js/runtime/runtime-session.js @@ -48,6 +48,7 @@ persistRuntimeMemorySnapshot, attachFirstUserIdleToInitialRuntimeSnapshot, rememberStableRuntimeSnapshot: rememberStableRuntimeSnapshotCallback, + getLoadedDelayedMemoryReportIds, } = deps; const { @@ -66,7 +67,6 @@ writeLatestSavedRuntimeMemory, readLatestSavedRuntimeMemory, buildPersistedRuntimeSnapshot, - collectCurrentSessionAppendedMemoryIds, collectOtherLatestRuntimeMemorySnapshots, clearOtherLatestRuntimeMemorySnapshots, getSavedRuntimeMemoryFallback, @@ -614,8 +614,10 @@ session_id: getCurrentSavedSessionId(), saved_at: savedAt, - appended_memory_ids: - collectCurrentSessionAppendedMemoryIds(), + loaded_memory_ids: + typeof getLoadedDelayedMemoryReportIds === "function" + ? getLoadedDelayedMemoryReportIds() + : [], session_memory: sessionMemory, session_memory_updates: data.updates || 0, @@ -1189,12 +1191,12 @@ && sessionMemory.session_memory_updates ) || 0, - appended_memory_ids: + loaded_memory_ids: ( sessionMemory - && Array.isArray(sessionMemory.appended_memory_ids) + && Array.isArray(sessionMemory.loaded_memory_ids) ) - ? sessionMemory.appended_memory_ids + ? sessionMemory.loaded_memory_ids .map(item => String(item || "").trim()) .filter(Boolean) : [], diff --git a/ui/static/js/runtime/runtime-storage.js b/ui/static/js/runtime/runtime-storage.js index 5783a9a5..6eb7a27a 100644 --- a/ui/static/js/runtime/runtime-storage.js +++ b/ui/static/js/runtime/runtime-storage.js @@ -1176,6 +1176,33 @@ } + function readDelayedLoadMetadata( + report, + key, + fallbackValue + ) { + if ( + report + && Object.prototype.hasOwnProperty.call(report, key) + ) { + return report[key]; + } + + const legacyPrefix = "append" + "ed"; + const legacyKeys = { + loaded_times: `${legacyPrefix}_times`, + load_streak: "append_streak", + last_loaded_date: `last_${legacyPrefix}_date`, + last_loaded_session_id: `last_${legacyPrefix}_session_id`, + all_loaded_session_ids: `all_${legacyPrefix}_session_ids`, + }; + const legacyKey = legacyKeys[key]; + + return legacyKey && report + ? report[legacyKey] + : fallbackValue; + } + function normalizeDelayedMemoryReports( value ) { @@ -1271,21 +1298,45 @@ || createdDate, created_date: createdDate, - appended_times: + loaded_times: normalizeDelayedMemoryCounter( - report.appended_times + readDelayedLoadMetadata( + report, + "loaded_times", + 0 + ) ), - append_streak: + load_streak: normalizeDelayedMemoryCounter( - report.append_streak + readDelayedLoadMetadata( + report, + "load_streak", + 0 + ) ), - last_appended_date: - String(report.last_appended_date || "").trim(), - last_appended_session_id: - String(report.last_appended_session_id || "").trim(), - all_appended_session_ids: + last_loaded_date: + String( + readDelayedLoadMetadata( + report, + "last_loaded_date", + "" + ) || "" + ).trim(), + last_loaded_session_id: + String( + readDelayedLoadMetadata( + report, + "last_loaded_session_id", + "" + ) || "" + ).trim(), + all_loaded_session_ids: normalizeDelayedMemorySessionIds( - report.all_appended_session_ids + readDelayedLoadMetadata( + report, + "all_loaded_session_ids", + [] + ) ), }; } @@ -1344,30 +1395,6 @@ } - function collectCurrentSessionAppendedMemoryIds() { - - const sessionId = - getCurrentRuntimeSessionId(); - const reports = - readDelayedMemoryReports(); - - if (!sessionId) { - return []; - } - - return Object.entries(reports) - .filter(function ([, report]) { - return ( - report - && Array.isArray(report.all_appended_session_ids) - && report.all_appended_session_ids.includes(sessionId) - ); - }) - .map(([reportId]) => reportId); - - } - - function readDelayedMemoryReports() { const rawReports = @@ -1410,7 +1437,7 @@ } - function appendDelayedMemoryReports( + function mergeDelayedMemoryReports( reports ) { @@ -1913,7 +1940,6 @@ writeLatestSavedSessionMemory, readSavedSessionMemoryHistory, writeSavedSessionMemoryHistory, - collectCurrentSessionAppendedMemoryIds, readLatestSavedRuntimeMemory, writeLatestSavedRuntimeMemory, normalizeActiveMemoryRecords, @@ -1939,7 +1965,7 @@ normalizeDelayedMemoryReports, readDelayedMemoryReports, writeDelayedMemoryReports, - appendDelayedMemoryReports, + mergeDelayedMemoryReports, buildPersistedRuntimeSnapshot, cloneRuntimeMemoryToCurrentSession, cloneRuntimeMemoryFromSessionId, diff --git a/ui/static/js/runtime/runtime.js b/ui/static/js/runtime/runtime.js index ada98cb0..44235434 100644 --- a/ui/static/js/runtime/runtime.js +++ b/ui/static/js/runtime/runtime.js @@ -123,7 +123,7 @@ const { normalizeDelayedMemoryReports, readDelayedMemoryReports, writeDelayedMemoryReports, - appendDelayedMemoryReports: appendStoredDelayedMemoryReports, + mergeDelayedMemoryReports: mergeStoredDelayedMemoryReports, readFactsMemory, writeFactsMemory, removeFactsMemoryField, @@ -555,6 +555,7 @@ session.init({ renderRuntimeMemorySnapshot, persistRuntimeMemorySnapshot, attachFirstUserIdleToInitialRuntimeSnapshot, + getLoadedDelayedMemoryReportIds, }); panel.init(); @@ -568,6 +569,7 @@ memoryView.init({ setActiveMemoryRecords: writeActiveMemoryRecordsAndRefresh, deleteRuntimeMemoryLine: deleteRuntimeMemoryLineAndRender, getDelayedMemoryReports: readDelayedMemoryReports, + isDelayedMemoryReportLoaded, setDelayedMemoryReportPinned, setDelayedMemoryReportAnchorFactIds, deleteDelayedMemoryReport: deleteDelayedMemoryReportAndRender, @@ -669,6 +671,46 @@ function syncDelayedMemoryReportsToServer( ); } +function getLoadedDelayedMemoryReportIds() { + return Array.from(loadedDelayedMemoryReportIds) + .sort(); +} + +function replaceLoadedDelayedMemoryReportIds( + reportIds, + options = {} +) { + const nextIds = new Set( + (Array.isArray(reportIds) ? reportIds : []) + .map(normalizeRuntimeDelayedMemoryReportId) + .filter(Boolean) + ); + const changed = + nextIds.size !== loadedDelayedMemoryReportIds.size + || Array.from(nextIds).some( + reportId => !loadedDelayedMemoryReportIds.has(reportId) + ); + + if (!changed) { + return getLoadedDelayedMemoryReportIds(); + } + + loadedDelayedMemoryReportIds.clear(); + nextIds.forEach( + reportId => loadedDelayedMemoryReportIds.add(reportId) + ); + + if (options.render !== false) { + renderRuntimeMemorySnapshot(); + + if (!syncDelayedMemoryStateToAvatar()) { + refreshRuntimeAvatar(); + } + } + + return getLoadedDelayedMemoryReportIds(); +} + function isDelayedMemoryReportLoaded(reportId) { const normalizedId = normalizeRuntimeDelayedMemoryReportId(reportId); @@ -1042,10 +1084,10 @@ function setDelayedMemoryReportPinned( reports[normalizedId] = { ...report, pinned: Boolean(pinned), - last_appended_date: + last_loaded_date: Boolean(pinned) ? new Date().toISOString() - : String(report.last_appended_date || "").trim(), + : String(report.last_loaded_date || "").trim(), }; writeDelayedMemoryReports( @@ -1402,12 +1444,12 @@ function deleteLongTermMemoryFactAndRender( } -function appendDelayedMemoryReports( +function mergeDelayedMemoryReports( reports ) { const nextReports = - appendStoredDelayedMemoryReports( + mergeStoredDelayedMemoryReports( reports ); @@ -1695,6 +1737,8 @@ window.JinRuntime.runtime = { appendActiveMemoryRecords: appendActiveMemoryRecordsAndRender, removeActiveMemoryRecordById: removeActiveMemoryRecordByIdAndRender, getDelayedMemoryReports: readDelayedMemoryReports, + getLoadedDelayedMemoryReportIds, + replaceLoadedDelayedMemoryReportIds, isDelayedMemoryReportLoaded, markDelayedMemoryReportLoaded, setDelayedMemoryReportPinned, @@ -1710,7 +1754,7 @@ window.JinRuntime.runtime = { return deleteLongTermMemoryFactAndRender(factId); }, replaceDelayedMemoryReports: replaceDelayedMemoryReportsAndRender, - appendDelayedMemoryReports, + mergeDelayedMemoryReports, }; window.JinRuntime.init = function () { diff --git a/ui/static/js/socket/delayed-memory.js b/ui/static/js/socket/delayed-memory.js index b549ddb0..4ff0de78 100644 --- a/ui/static/js/socket/delayed-memory.js +++ b/ui/static/js/socket/delayed-memory.js @@ -284,7 +284,7 @@ function parseDelayedMemoryReportPayload( } -function appendDelayedMemoryReportFromClientFallback( +function mergeDelayedMemoryReportFromClientFallback( payload ) { @@ -297,12 +297,12 @@ function appendDelayedMemoryReportFromClientFallback( !Object.keys(report).length || !window.JinRuntime || !window.JinRuntime.runtime - || !window.JinRuntime.runtime.appendDelayedMemoryReports + || !window.JinRuntime.runtime.mergeDelayedMemoryReports ) { return false; } - window.JinRuntime.runtime.appendDelayedMemoryReports( + window.JinRuntime.runtime.mergeDelayedMemoryReports( report ); @@ -380,7 +380,7 @@ function filterDelayedMemoryContentFromChunk( return visible; } - appendDelayedMemoryReportFromClientFallback( + mergeDelayedMemoryReportFromClientFallback( source.slice( payloadStart, closeIndex @@ -482,10 +482,16 @@ function syncDelayedMemoryReportsToRuntime(options = {}) { return false; } + const loadedDelayedMemoryIds = + typeof window.JinRuntime.runtime.getLoadedDelayedMemoryReportIds === "function" + ? window.JinRuntime.runtime.getLoadedDelayedMemoryReportIds() + : []; + return sendSocketMessage({ type: "delayed_memory_store_sync", delayed_memory_reports: delayedMemoryReports, deleted_delayed_memory_report_ids: deletedReportIds, + loaded_delayed_memory_ids: loadedDelayedMemoryIds, }); } @@ -514,6 +520,16 @@ function handleDelayedMemoryStoreSnapshot( const localReports = window.JinRuntime.runtime.getDelayedMemoryReports(); + if ( + typeof window.JinRuntime.runtime.replaceLoadedDelayedMemoryReportIds + === "function" + ) { + window.JinRuntime.runtime.replaceLoadedDelayedMemoryReportIds( + data.loaded_delayed_memory_ids || [], + { render: false } + ); + } + window.JinRuntime.runtime.replaceDelayedMemoryReports({ ...localReports, ...data.delayed_memory_reports, diff --git a/ui/static/js/socket/event-handlers.js b/ui/static/js/socket/event-handlers.js index d5418fc2..c591bff8 100644 --- a/ui/static/js/socket/event-handlers.js +++ b/ui/static/js/socket/event-handlers.js @@ -227,10 +227,12 @@ function handleMessageError( false ); - appendLog( - "[VALIDATOR]", - data.text - ); + if (!data.suppress_log) { + appendLog( + data.log_tag || "[VALIDATOR]", + data.text + ); + } finishStreamMessage( data.message_id diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 3f2e5597..6a5b1695 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -550,8 +550,8 @@ const PAYLOAD_DISTINCT_RUNTIME_ACTIONS = new Set([ "save_active_memory", "resolve_active_memory", "save_delayed_memory_content", - "append_delayed_memory", - "remove_delayed_memory", + "load_delayed_memory", + "unload_delayed_memory", "update_delayed_memory", ]); @@ -687,8 +687,8 @@ function getDelayedMemoryRuntimeActionPreview( ) || ( [ - "append_delayed_memory", - "remove_delayed_memory", + "load_delayed_memory", + "unload_delayed_memory", "update_delayed_memory", ].includes(action) ? data.payload @@ -745,8 +745,8 @@ function handleRuntimeAction( ); const reportScopedDelayedAction = [ - "append_delayed_memory", - "remove_delayed_memory", + "load_delayed_memory", + "unload_delayed_memory", "update_delayed_memory", ].includes(action) && Boolean( @@ -869,8 +869,8 @@ function handleRuntimeAction( ); const suppressMarkerCount = [ - "append_skill", - "append_skills", + "load_skill", + "load_skills", ].includes(action); const markerCount = suppressMarkerCount @@ -1136,9 +1136,9 @@ function handleRuntimeAction( && data.delayed_memory_report && window.JinRuntime && window.JinRuntime.runtime - && window.JinRuntime.runtime.appendDelayedMemoryReports + && window.JinRuntime.runtime.mergeDelayedMemoryReports ) { - window.JinRuntime.runtime.appendDelayedMemoryReports( + window.JinRuntime.runtime.mergeDelayedMemoryReports( data.delayed_memory_report ); } @@ -1150,24 +1150,24 @@ function handleRuntimeAction( && data.delayed_memory_result.id && window.JinRuntime && window.JinRuntime.runtime - && window.JinRuntime.runtime.appendDelayedMemoryReports + && window.JinRuntime.runtime.mergeDelayedMemoryReports ) { - window.JinRuntime.runtime.appendDelayedMemoryReports({ + window.JinRuntime.runtime.mergeDelayedMemoryReports({ [data.delayed_memory_result.id]: data.delayed_memory_result.report, }); } if ( - action === "append_delayed_memory" + action === "load_delayed_memory" && data.delayed_memory_result && data.delayed_memory_result.report && data.delayed_memory_result.id && window.JinRuntime && window.JinRuntime.runtime - && window.JinRuntime.runtime.appendDelayedMemoryReports + && window.JinRuntime.runtime.mergeDelayedMemoryReports ) { - window.JinRuntime.runtime.appendDelayedMemoryReports({ + window.JinRuntime.runtime.mergeDelayedMemoryReports({ [data.delayed_memory_result.id]: data.delayed_memory_result.report, }); @@ -1185,14 +1185,14 @@ function handleRuntimeAction( && typeof window.JinRuntime.runtime.markDelayedMemoryReportLoaded === "function" ) { - if (action === "append_delayed_memory") { + if (action === "load_delayed_memory") { window.JinRuntime.runtime.markDelayedMemoryReportLoaded( delayedMemoryPreview.reportId, true ); } - if (action === "remove_delayed_memory") { + if (action === "unload_delayed_memory") { window.JinRuntime.runtime.markDelayedMemoryReportLoaded( delayedMemoryPreview.reportId, false diff --git a/ui/templates/index.html b/ui/templates/index.html index bf570b47..160ff00a 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -14,10 +14,10 @@ - + - - + + @@ -500,16 +500,16 @@ } | tojson }} - + - + - - - + + + @@ -522,11 +522,11 @@ - + - - + + diff --git a/utils/actions/action_counter_utils.py b/utils/actions/action_counter_utils.py index 404fb8d3..46c1af77 100644 --- a/utils/actions/action_counter_utils.py +++ b/utils/actions/action_counter_utils.py @@ -34,10 +34,10 @@ class RuntimeActionCounter: """Count identical parsed markers once, before execution dedupe.""" _EXCLUDED_ACTIONS = frozenset({ - "APPEND_SKILL", - "APPEND_SKILLS", - "REMOVE_SKILL", - "REMOVE_SKILLS", + "LOAD_SKILL", + "LOAD_SKILLS", + "UNLOAD_SKILL", + "UNLOAD_SKILLS", }) def __init__(self): diff --git a/utils/actions/asset_actions.py b/utils/actions/asset_actions.py index fb16bb59..87b224d1 100644 --- a/utils/actions/asset_actions.py +++ b/utils/actions/asset_actions.py @@ -244,11 +244,7 @@ async def emit_saved_asset_results( ) or "assets" ) - action_name = ( - "list_skills" - if result_action == "list_skills" - else "asset_action" - ) + action_name = "asset_action" text = ( saved_asset_result_texts[ result_index - 1 diff --git a/utils/actions/common_action_utils.py b/utils/actions/common_action_utils.py index 98b8457e..84fcefcb 100644 --- a/utils/actions/common_action_utils.py +++ b/utils/actions/common_action_utils.py @@ -4,19 +4,18 @@ from functools import lru_cache from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_SKILL, + RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, RUNTIME_ACTION_RESOLVE_TODO, @@ -35,8 +34,8 @@ _get_internal_action_placeholder_payloads, ) from .load_delayed_memory_utils import build_load_delayed_memory_payload -from .append_skill_utils import ( - build_append_skill_payload, +from .skill_load_utils import ( + build_load_skill_payload, plural_skill_marker_action_name as _plural_skill_marker_action_name, split_internal_skill_marker_list as _split_internal_skill_marker_list, ) @@ -330,10 +329,9 @@ def normalize_runtime_action_name( "SAVE_ACTIVE_MEMORY": RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, "RESOLVE_ACTIVE_MEMORY": RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, "USE_ASSETS": RUNTIME_ACTION_ASSET_ACTION, - "LIST_SKILLS": RUNTIME_ACTION_LIST_SKILLS, "CLEAN_TOOL_RESULTS": RUNTIME_ACTION_CLEAN_TOOL_RESULTS, - "APPEND_SKILL": RUNTIME_ACTION_APPEND_SKILL, - "REMOVE_SKILL": RUNTIME_ACTION_REMOVE_SKILL, + "LOAD_SKILL": RUNTIME_ACTION_LOAD_SKILL, + "UNLOAD_SKILL": RUNTIME_ACTION_UNLOAD_SKILL, "ASSET_ACTION": RUNTIME_ACTION_ASSET_ACTION, "TODO_LIST": RUNTIME_ACTION_CREATE_TODO_LIST, "INTERNAL_ACTION_TODO_LIST": RUNTIME_ACTION_CREATE_TODO_LIST, @@ -403,13 +401,10 @@ def normalize_runtime_action_names( if normalized_name == RUNTIME_ACTION_ASSET_ACTION: normalized_names.append( - RUNTIME_ACTION_LIST_SKILLS + RUNTIME_ACTION_LOAD_SKILL ) normalized_names.append( - RUNTIME_ACTION_APPEND_SKILL - ) - normalized_names.append( - RUNTIME_ACTION_REMOVE_SKILL + RUNTIME_ACTION_UNLOAD_SKILL ) if ( @@ -443,8 +438,8 @@ def normalize_runtime_action_names( RUNTIME_ACTION_LOAD_DELAYED_MEMORY: build_load_delayed_memory_payload, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY: build_resolve_action_payload, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY: build_update_delayed_memory_payload, - RUNTIME_ACTION_APPEND_SKILL: build_append_skill_payload, - RUNTIME_ACTION_REMOVE_SKILL: build_resolve_action_payload, + RUNTIME_ACTION_LOAD_SKILL: build_load_skill_payload, + RUNTIME_ACTION_UNLOAD_SKILL: build_resolve_action_payload, RUNTIME_ACTION_ASSET_ACTION: build_asset_action_payload, } @@ -461,14 +456,6 @@ def _build_internal_action_call( if normalized_name not in KNOWN_RUNTIME_ACTIONS: return None - if normalized_name == RUNTIME_ACTION_LIST_SKILLS: - return RuntimeActionCall( - name=normalized_name, - payload=_clean_internal_action_query( - query - ), - ) - payload_builder = _ACTION_PAYLOAD_BUILDERS.get( normalized_name ) @@ -921,9 +908,9 @@ def handle_plural_skill_marker( query ) plural_marker_name = ( - "APPEND_SKILLS" - if action_name == RUNTIME_ACTION_APPEND_SKILL - else "REMOVE_SKILLS" + "LOAD_SKILLS" + if action_name == RUNTIME_ACTION_LOAD_SKILL + else "UNLOAD_SKILLS" ) plural_marker_payload = ", ".join( skill_names @@ -1236,7 +1223,7 @@ def _action_text_may_contain_marker( ) # No opening angle bracket means no runtime action. Names such as - # ``LIST_SKILLS`` or ``SAVE_SESSION`` in prose, Markdown/code spans, or + # ``SAVE_SESSION`` in prose, Markdown/code spans, or # standalone lines must pass through unchanged. if "<" not in upper_text: return False diff --git a/utils/actions/delayed_memory_actions.py b/utils/actions/delayed_memory_actions.py index 93de367c..9f6bcda7 100644 --- a/utils/actions/delayed_memory_actions.py +++ b/utils/actions/delayed_memory_actions.py @@ -16,51 +16,51 @@ async def apply_delayed_memory_actions( context, *, - append_delayed_memory_actions, - remove_delayed_memory_actions, + load_delayed_memory_actions, + unload_delayed_memory_actions, update_delayed_memory_actions, log_runtime, ): from utils.brain_client_utils import ( - append_delayed_memory_report, - append_delayed_memory_runtime_result, + load_delayed_memory_report, + record_delayed_memory_runtime_result, build_delayed_memory_failure_result, build_delayed_memory_history_text, - clear_appended_delayed_memory_report, + clear_loaded_delayed_memory_report, clear_delayed_memory_runtime_results, get_delayed_memory_reports, - remove_delayed_memory_report, - set_appended_delayed_memory_report, + unload_delayed_memory_report, + set_loaded_delayed_memory_report, update_delayed_memory_report, ) delayed_memory_results = [] - if append_delayed_memory_actions: + if load_delayed_memory_actions: if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] append_delayed_memory requested" + "[RUNTIME ACTION] load_delayed_memory requested" ) clear_delayed_memory_runtime_results( context ) - for action in append_delayed_memory_actions: - result = append_delayed_memory_report( + for action in load_delayed_memory_actions: + result = load_delayed_memory_report( context, action.payload, ) - did_append_delayed_memory = set_appended_delayed_memory_report( + did_load_delayed_memory = set_loaded_delayed_memory_report( context, result, ) if result.get("ok") is False: - append_delayed_memory_runtime_result( + record_delayed_memory_runtime_result( context, result, ) - if did_append_delayed_memory: + if did_load_delayed_memory: history_text = build_delayed_memory_history_text( result ) @@ -88,7 +88,7 @@ async def apply_delayed_memory_actions( context, action.payload, ) - append_delayed_memory_runtime_result( + record_delayed_memory_runtime_result( context, result, ) @@ -114,10 +114,10 @@ async def apply_delayed_memory_actions( result ) - if remove_delayed_memory_actions: + if unload_delayed_memory_actions: if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] remove_delayed_memory requested" + "[RUNTIME ACTION] unload_delayed_memory requested" ) clear_delayed_memory_runtime_results( @@ -130,37 +130,37 @@ async def apply_delayed_memory_actions( ) ) - for action in remove_delayed_memory_actions: - result = remove_delayed_memory_report( + for action in unload_delayed_memory_actions: + result = unload_delayed_memory_report( context, action.payload, ) - did_remove_delayed_memory = clear_appended_delayed_memory_report( + did_unload_delayed_memory = clear_loaded_delayed_memory_report( context, result.get( "id", "", ), ) - result["detached"] = did_remove_delayed_memory + result["unloaded"] = did_unload_delayed_memory if ( result.get("ok") is not False - and not did_remove_delayed_memory + and not did_unload_delayed_memory ): result = build_delayed_memory_failure_result( - action="remove_delayed_memory", + action="unload_delayed_memory", requested=result.get( "id", "", ), - error="delayed_memory_not_appended", + error="delayed_memory_not_loaded", ) - result["detached"] = False - append_delayed_memory_runtime_result( + result["unloaded"] = False + record_delayed_memory_runtime_result( context, result, ) - if did_remove_delayed_memory: + if did_unload_delayed_memory: history_text = build_delayed_memory_history_text( result ) diff --git a/utils/actions/dispatcher.py b/utils/actions/dispatcher.py index 45081d19..cbe93e53 100644 --- a/utils/actions/dispatcher.py +++ b/utils/actions/dispatcher.py @@ -1,18 +1,17 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_APPEND_SKILL, + RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_UPDATE_L4_FACTS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_UPDATE_DELAYED_MEMORY, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, @@ -118,9 +117,9 @@ async def apply_runtime_action_calls( if not hasattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", ): - context.runtime_appended_skills = [] + context.runtime_loaded_skills = [] action_context_snapshot = ( dict(context_snapshot) @@ -227,18 +226,16 @@ def with_action_context(payload: dict) -> dict: resolve_active_memory_ids_seen = set() resolve_active_memory_failures_seen = set() save_delayed_memory_seen = set() - list_skills_seen = False resolved_user_message = resolve_runtime_action_user_message( context, user_message, ) skill_state_action_names = { - RUNTIME_ACTION_APPEND_SKILL, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_LOAD_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, } skill_workflow_action_names = { *skill_state_action_names, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_WEB_SEARCH, @@ -250,7 +247,7 @@ def with_action_context(payload: dict) -> dict: RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_CHECK_TODO, } - appended_skill_names = { + loaded_skill_names = { normalize_skill_name( skill.get( "name", @@ -260,7 +257,7 @@ def with_action_context(payload: dict) -> dict: for skill in ( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] @@ -350,8 +347,8 @@ def build_runtime_action_dedup_key( action.payload ) elif action_name in { - RUNTIME_ACTION_APPEND_SKILL, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_LOAD_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, }: payload_identity = normalize_skill_name( action.payload @@ -881,18 +878,7 @@ def accept_runtime_action_once_per_message( ): continue - if action.name == RUNTIME_ACTION_LIST_SKILLS: - if list_skills_seen: - continue - - if not accept_runtime_action_once_per_message( - action - ): - continue - - list_skills_seen = True - - if action.name == RUNTIME_ACTION_APPEND_SKILL: + if action.name == RUNTIME_ACTION_LOAD_SKILL: requested_skill = normalize_skill_name( action.payload ) @@ -905,14 +891,14 @@ def accept_runtime_action_once_per_message( ): continue - if requested_skill in appended_skill_names: + if requested_skill in loaded_skill_names: continue - appended_skill_names.add( + loaded_skill_names.add( requested_skill ) - if action.name == RUNTIME_ACTION_REMOVE_SKILL: + if action.name == RUNTIME_ACTION_UNLOAD_SKILL: requested_skill = normalize_skill_name( action.payload ) @@ -925,15 +911,14 @@ def accept_runtime_action_once_per_message( ): continue - appended_skill_names.discard( + loaded_skill_names.discard( requested_skill ) if action.name not in { RUNTIME_ACTION_WEB_SEARCH, - RUNTIME_ACTION_LIST_SKILLS, - RUNTIME_ACTION_APPEND_SKILL, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_LOAD_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, }: if not accept_runtime_action_once_per_message( action @@ -1275,13 +1260,13 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT ] - append_delayed_memory_actions = [ + load_delayed_memory_actions = [ action for action in filtered_actions if action.name == RUNTIME_ACTION_LOAD_DELAYED_MEMORY ] - remove_delayed_memory_actions = [ + unload_delayed_memory_actions = [ action for action in filtered_actions if action.name == RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY @@ -1299,12 +1284,6 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_UPDATE_L4_FACTS ] - list_skill_actions = [ - action - for action in filtered_actions - if action.name == RUNTIME_ACTION_LIST_SKILLS - ] - clean_tool_result_actions = [ action for action in filtered_actions @@ -1323,16 +1302,16 @@ def accept_runtime_action_once_per_message( if action.name == RUNTIME_ACTION_JIN_COLOR ] - append_skill_actions = [ + load_skill_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_APPEND_SKILL + if action.name == RUNTIME_ACTION_LOAD_SKILL ] - remove_skill_actions = [ + unload_skill_actions = [ action for action in filtered_actions - if action.name == RUNTIME_ACTION_REMOVE_SKILL + if action.name == RUNTIME_ACTION_UNLOAD_SKILL ] asset_actions = [ @@ -1458,17 +1437,16 @@ def accept_runtime_action_once_per_message( skill_results = await apply_skill_actions( context, - list_skill_actions=list_skill_actions, - append_skill_actions=append_skill_actions, - remove_skill_actions=remove_skill_actions, + load_skill_actions=load_skill_actions, + unload_skill_actions=unload_skill_actions, runtime_todo_action_items=runtime_todo_action_items, log_runtime=log_runtime, ) saved_asset_results = list( skill_results["saved_asset_results"] ) - appended_skill_results = skill_results["appended_skill_results"] - removed_skill_results = skill_results["removed_skill_results"] + loaded_skill_results = skill_results["loaded_skill_results"] + unloaded_skill_results = skill_results["unloaded_skill_results"] saved_asset_results.extend( await apply_asset_actions( @@ -1482,8 +1460,8 @@ def accept_runtime_action_once_per_message( delayed_memory_results = await apply_delayed_memory_actions( context, - append_delayed_memory_actions=append_delayed_memory_actions, - remove_delayed_memory_actions=remove_delayed_memory_actions, + load_delayed_memory_actions=load_delayed_memory_actions, + unload_delayed_memory_actions=unload_delayed_memory_actions, update_delayed_memory_actions=update_delayed_memory_actions, log_runtime=log_runtime, ) @@ -1509,8 +1487,8 @@ def accept_runtime_action_once_per_message( ) skill_state_results = ( - appended_skill_results - + removed_skill_results + loaded_skill_results + + unloaded_skill_results ) await emit_skill_state_results( @@ -1598,10 +1576,10 @@ def accept_runtime_action_once_per_message( saved_asset_results ) + len( - appended_skill_results + loaded_skill_results ) + len( - removed_skill_results + unloaded_skill_results ) + len( clean_tool_result_actions diff --git a/utils/actions/regexp_utils.py b/utils/actions/regexp_utils.py index 8f22280e..37ddf63c 100644 --- a/utils/actions/regexp_utils.py +++ b/utils/actions/regexp_utils.py @@ -99,7 +99,7 @@ def _runtime_action_aliases( # Keep compatibility with plural skill markers without putting aliases in # every contract. - if action_name in {"APPEND_SKILL", "REMOVE_SKILL"}: + if action_name in {"LOAD_SKILL", "UNLOAD_SKILL"}: plural_name = f"{action_name}S" if plural_name not in names: names.append(plural_name) diff --git a/utils/actions/skill_actions.py b/utils/actions/skill_actions.py index 985567b6..4ef11a9b 100644 --- a/utils/actions/skill_actions.py +++ b/utils/actions/skill_actions.py @@ -6,7 +6,6 @@ from utils.actions.todo_actions import attach_todo_result from utils.session_actions_history import record_session_action_history from utils.skills_asset_utils import ( - list_skills, load_skill, normalize_skill_name, ) @@ -117,8 +116,8 @@ def _group_skill_state_results( or "" ).strip() is_plural = marker_name in { - "APPEND_SKILLS", - "REMOVE_SKILLS", + "LOAD_SKILLS", + "UNLOAD_SKILLS", } if not is_plural or not marker_group: @@ -174,18 +173,18 @@ def _build_skill_state_group_payload( if not marker_name: marker_name = ( - "APPEND_SKILL" - if first_action == "append_skill" + "LOAD_SKILL" + if first_action == "load_skill" else ( - "REMOVE_SKILL" - if first_action == "remove_skill" + "UNLOAD_SKILL" + if first_action == "unload_skill" else first_action.upper() ) ) is_plural = marker_name in { - "APPEND_SKILLS", - "REMOVE_SKILLS", + "LOAD_SKILLS", + "UNLOAD_SKILLS", } if marker_payload: @@ -284,9 +283,8 @@ def _build_skill_state_group_payload( async def apply_skill_actions( context, *, - list_skill_actions, - append_skill_actions, - remove_skill_actions, + load_skill_actions, + unload_skill_actions, runtime_todo_action_items, log_runtime, ): @@ -294,48 +292,24 @@ async def apply_skill_actions( saved_asset_results = [] - if list_skill_actions: - if log_runtime is not None: - await log_runtime( - "[RUNTIME ACTION] list_skills requested" - ) - - for action in list_skill_actions: - result = list_skills( - action.payload - ) - result = attach_todo_result( - context, - runtime_todo_action_items, - action, - result, - ) - append_asset_runtime_result( - context, - result, - ) - saved_asset_results.append( - result - ) - - appended_skill_results = [] + loaded_skill_results = [] - if append_skill_actions: + if load_skill_actions: if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] append_skill requested" + "[RUNTIME ACTION] load_skill requested" ) current_skills = list( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) - for action in append_skill_actions: + for action in load_skill_actions: result = load_skill( action.payload ) @@ -363,7 +337,7 @@ async def apply_skill_actions( current_skills.append( skill ) - context.runtime_appended_skills = current_skills + context.runtime_loaded_skills = current_skills result = attach_todo_result( context, @@ -382,31 +356,31 @@ async def apply_skill_actions( ), ) - appended_skill_results.append( + loaded_skill_results.append( _attach_skill_marker_metadata( result, action, ) ) - removed_skill_results = [] + unloaded_skill_results = [] - if remove_skill_actions: + if unload_skill_actions: if log_runtime is not None: await log_runtime( - "[RUNTIME ACTION] remove_skill requested" + "[RUNTIME ACTION] unload_skill requested" ) current_skills = list( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) - for action in remove_skill_actions: + for action in unload_skill_actions: requested = normalize_skill_name( action.payload ) @@ -423,14 +397,14 @@ async def apply_skill_actions( ) ) != requested ] - context.runtime_appended_skills = current_skills + context.runtime_loaded_skills = current_skills result = { "ok": True, - "action": "remove_skill", + "action": "unload_skill", "requested": requested, - "removed": len(current_skills) < before_count, + "unloaded": len(current_skills) < before_count, } - removed_skill_results.append( + unloaded_skill_results.append( _attach_skill_marker_metadata( result, action, @@ -438,15 +412,15 @@ async def apply_skill_actions( ) if ( - appended_skill_results - or removed_skill_results + loaded_skill_results + or unloaded_skill_results ): context.runtime_skill_state_barrier_active = True return { "saved_asset_results": saved_asset_results, - "appended_skill_results": appended_skill_results, - "removed_skill_results": removed_skill_results, + "loaded_skill_results": loaded_skill_results, + "unloaded_skill_results": unloaded_skill_results, } @@ -492,8 +466,8 @@ async def emit_skill_state_results( preserve_separate=( marker_name not in { - "APPEND_SKILLS", - "REMOVE_SKILLS", + "LOAD_SKILLS", + "UNLOAD_SKILLS", } ), ) diff --git a/utils/actions/append_skill_utils.py b/utils/actions/skill_load_utils.py similarity index 72% rename from utils/actions/append_skill_utils.py rename to utils/actions/skill_load_utils.py index 01b1c041..37a21505 100644 --- a/utils/actions/append_skill_utils.py +++ b/utils/actions/skill_load_utils.py @@ -1,6 +1,6 @@ from contracts.rules_assembler import ( - RUNTIME_ACTION_APPEND_SKILL, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_LOAD_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, ) from .action_payload_utils import _clean_internal_action_query @@ -16,11 +16,11 @@ def plural_skill_marker_action_name( .upper() ) - if normalized_name == "APPEND_SKILLS": - return RUNTIME_ACTION_APPEND_SKILL + if normalized_name == "LOAD_SKILLS": + return RUNTIME_ACTION_LOAD_SKILL - if normalized_name == "REMOVE_SKILLS": - return RUNTIME_ACTION_REMOVE_SKILL + if normalized_name == "UNLOAD_SKILLS": + return RUNTIME_ACTION_UNLOAD_SKILL return None @@ -41,7 +41,7 @@ def split_internal_skill_marker_list( ) -def build_append_skill_payload( +def build_load_skill_payload( query: str, placeholder_payloads=(), ) -> str: diff --git a/utils/brain_client_utils.py b/utils/brain_client_utils.py index e14844bd..38ec5803 100644 --- a/utils/brain_client_utils.py +++ b/utils/brain_client_utils.py @@ -62,17 +62,16 @@ def get_brain_runtime_config(): from contracts.rules_assembler import ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, - RUNTIME_ACTION_APPEND_SKILL, + RUNTIME_ACTION_LOAD_SKILL, RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_CHECK_TODO, RUNTIME_ACTION_CREATE_TODO_LIST, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_IDLE, RUNTIME_ACTION_JIN_COLOR, RUNTIME_ACTION_CLEAN_TOOL_RESULTS, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, - RUNTIME_ACTION_REMOVE_SKILL, + RUNTIME_ACTION_UNLOAD_SKILL, RUNTIME_ACTION_RESOLVE_TODO, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, @@ -94,7 +93,6 @@ def get_brain_runtime_config(): run_context_asset_action, ) from utils.skills_asset_utils import ( - list_skills, load_skill, normalize_skill_name, ) @@ -396,40 +394,40 @@ def build_delayed_memory_report( ).strip() or created_time ), - "appended_times": int( + "loaded_times": int( normalize_delayed_memory_counter( value.get( - "appended_times", + "loaded_times", 0, ) ) ), - "append_streak": int( + "load_streak": int( normalize_delayed_memory_counter( value.get( - "append_streak", + "load_streak", 0, ) ) ), - "last_appended_date": str( + "last_loaded_date": str( value.get( - "last_appended_date", + "last_loaded_date", "", ) or "" ).strip(), - "last_appended_session_id": str( + "last_loaded_session_id": str( value.get( - "last_appended_session_id", + "last_loaded_session_id", "", ) or "" ).strip(), - "all_appended_session_ids": ( + "all_loaded_session_ids": ( normalize_delayed_memory_session_ids( value.get( - "all_appended_session_ids", + "all_loaded_session_ids", [], ) ) @@ -494,7 +492,7 @@ def normalize_delayed_memory_session_ids( return session_ids -def update_delayed_memory_append_metadata( +def update_delayed_memory_load_metadata( context, report: dict, ) -> dict: @@ -531,23 +529,23 @@ def update_delayed_memory_append_metadata( ).strip() previous_last_session_id = str( updated_report.get( - "last_appended_session_id", + "last_loaded_session_id", "", ) or "" ).strip() - appended_session_ids = normalize_delayed_memory_session_ids( + loaded_session_ids = normalize_delayed_memory_session_ids( updated_report.get( - "all_appended_session_ids", + "all_loaded_session_ids", [], ) ) if ( session_id - and session_id not in appended_session_ids + and session_id not in loaded_session_ids ): - appended_session_ids.append( + loaded_session_ids.append( session_id ) @@ -575,18 +573,18 @@ def update_delayed_memory_append_metadata( ).strip() or updated_report["created_date"] ) - updated_report["appended_times"] = ( + updated_report["loaded_times"] = ( normalize_delayed_memory_counter( updated_report.get( - "appended_times", + "loaded_times", 0, ) ) + 1 ) - updated_report["append_streak"] = normalize_delayed_memory_counter( + updated_report["load_streak"] = normalize_delayed_memory_counter( updated_report.get( - "append_streak", + "load_streak", 0, ) ) @@ -598,16 +596,16 @@ def update_delayed_memory_append_metadata( or previous_last_session_id != session_id ) ): - updated_report["append_streak"] += 1 + updated_report["load_streak"] += 1 - updated_report["last_appended_date"] = now - updated_report["last_appended_session_id"] = session_id - updated_report["all_appended_session_ids"] = appended_session_ids + updated_report["last_loaded_date"] = now + updated_report["last_loaded_session_id"] = session_id + updated_report["all_loaded_session_ids"] = loaded_session_ids return updated_report -def record_appended_delayed_memory_id( +def record_loaded_delayed_memory_id( context, report_id: str, ) -> None: @@ -620,25 +618,25 @@ def record_appended_delayed_memory_id( if not normalized_report_id: return - appended_ids = getattr( + loaded_ids = getattr( context, - "runtime_appended_delayed_memory_ids", + "runtime_loaded_delayed_memory_ids", None, ) if not isinstance( - appended_ids, + loaded_ids, list, ): - appended_ids = [] + loaded_ids = [] setattr( context, - "runtime_appended_delayed_memory_ids", - appended_ids, + "runtime_loaded_delayed_memory_ids", + loaded_ids, ) - if normalized_report_id not in appended_ids: - appended_ids.append( + if normalized_report_id not in loaded_ids: + loaded_ids.append( normalized_report_id ) @@ -1612,7 +1610,7 @@ def append_asset_runtime_result( ) -def append_delayed_memory_runtime_result( +def record_delayed_memory_runtime_result( context, result: dict, ) -> None: @@ -1688,24 +1686,24 @@ def clear_delayed_memory_runtime_results( ) -def get_appended_delayed_memory_report( +def get_loaded_delayed_memory_reports( context, ) -> dict: - appended_reports = getattr( + loaded_reports = getattr( context, - "runtime_appended_delayed_memory", + "runtime_loaded_delayed_memory", None, ) if not isinstance( - appended_reports, + loaded_reports, dict, ): - appended_reports = {} + loaded_reports = {} legacy_report_id = str( - appended_reports.get( + loaded_reports.get( "id", "", ) @@ -1718,21 +1716,21 @@ def get_appended_delayed_memory_report( legacy_report_id ) and ( - "title" in appended_reports - or "body" in appended_reports - or "summary" in appended_reports + "title" in loaded_reports + or "body" in loaded_reports + or "summary" in loaded_reports ) ): - appended_reports = { + loaded_reports = { legacy_report_id: { - **appended_reports, + **loaded_reports, "id": legacy_report_id, }, } else: normalized_reports = {} - for report_id, report in appended_reports.items(): + for report_id, report in loaded_reports.items(): normalized_report_id = str( report_id or "" @@ -1754,18 +1752,18 @@ def get_appended_delayed_memory_report( "id": normalized_report_id, } - appended_reports = normalized_reports + loaded_reports = normalized_reports setattr( context, - "runtime_appended_delayed_memory", - appended_reports, + "runtime_loaded_delayed_memory", + loaded_reports, ) - return appended_reports + return loaded_reports -def set_appended_delayed_memory_report( +def set_loaded_delayed_memory_report( context, result: dict, ) -> bool: @@ -1806,14 +1804,14 @@ def set_appended_delayed_memory_report( ): return False - appended_reports = get_appended_delayed_memory_report( + loaded_reports = get_loaded_delayed_memory_reports( context ) - if report_id in appended_reports: + if report_id in loaded_reports: return False - appended_reports[report_id] = { + loaded_reports[report_id] = { **report, "id": report_id, } @@ -1821,7 +1819,7 @@ def set_appended_delayed_memory_report( return True -def clear_appended_delayed_memory_report( +def clear_loaded_delayed_memory_report( context, report_id: str = "", ) -> bool: @@ -1843,28 +1841,28 @@ def clear_appended_delayed_memory_report( if isinstance(saved_report, dict) and bool(saved_report.get("pinned", False)): return False - appended_reports = get_appended_delayed_memory_report( + loaded_reports = get_loaded_delayed_memory_reports( context ) - if normalized_report_id not in appended_reports: + if normalized_report_id not in loaded_reports: return False - del appended_reports[normalized_report_id] + del loaded_reports[normalized_report_id] - appended_ids = getattr( + loaded_ids = getattr( context, - "runtime_appended_delayed_memory_ids", + "runtime_loaded_delayed_memory_ids", None, ) if isinstance( - appended_ids, + loaded_ids, list, ): - appended_ids[:] = [ + loaded_ids[:] = [ item - for item in appended_ids + for item in loaded_ids if str(item or "").strip().casefold() != normalized_report_id ] @@ -1931,7 +1929,7 @@ def build_delayed_memory_failure_result( } -def append_delayed_memory_report( +def load_delayed_memory_report( context, payload: str, ) -> dict: @@ -1951,7 +1949,7 @@ def append_delayed_memory_report( dict, ): return build_delayed_memory_failure_result( - action="append_delayed_memory", + action="load_delayed_memory", requested=report_id or payload, error=( @@ -1961,12 +1959,12 @@ def append_delayed_memory_report( ), ) - updated_report = update_delayed_memory_append_metadata( + updated_report = update_delayed_memory_load_metadata( context, report, ) reports[report_id] = updated_report - record_appended_delayed_memory_id( + record_loaded_delayed_memory_id( context, report_id, ) @@ -1990,7 +1988,7 @@ def append_delayed_memory_report( return { "ok": True, - "action": "append_delayed_memory", + "action": "load_delayed_memory", "id": report_id, "title": str( updated_report.get( @@ -2008,7 +2006,7 @@ def append_delayed_memory_report( } -def _append_unique_delayed_memory_values( +def _merge_unique_delayed_memory_values( existing, incoming, *, @@ -2101,19 +2099,19 @@ def update_delayed_memory_report( ], ) ) - next_tags = _append_unique_delayed_memory_values( + next_tags = _merge_unique_delayed_memory_values( report.get("tags", []), update.get("tags", []), casefold=True, ) - appended_body = str(update.get("body", "") or "").strip() + added_body = str(update.get("body", "") or "").strip() current_body = str(report.get("body", "") or "").rstrip() next_body = current_body - if appended_body: + if added_body: next_body = "\n\n".join( part - for part in (current_body, appended_body) + for part in (current_body, added_body) if part ) @@ -2140,10 +2138,10 @@ def update_delayed_memory_report( updated_report.pop("long_term_facts_ids", None) reports[report_id] = updated_report - appended_reports = get_appended_delayed_memory_report(context) + loaded_reports = get_loaded_delayed_memory_reports(context) - if report_id in appended_reports: - appended_reports[report_id] = { + if report_id in loaded_reports: + loaded_reports[report_id] = { **updated_report, "id": report_id, } @@ -2187,10 +2185,10 @@ def include_pinned_delayed_memory_reports( "delayed_memory_reports", None, ) - appended_reports = get_appended_delayed_memory_report(context) + loaded_reports = get_loaded_delayed_memory_reports(context) if not isinstance(reports, dict): - return appended_reports + return loaded_reports turn_id = str( getattr(context, "runtime_current_turn_id", "") or getattr(context, "runtime_message_id", "") @@ -2215,7 +2213,7 @@ def include_pinned_delayed_memory_reports( updated_report = report if turn_id and touched_by_report.get(report_id) != turn_id: - updated_report = update_delayed_memory_append_metadata( + updated_report = update_delayed_memory_load_metadata( context, report, ) @@ -2224,7 +2222,7 @@ def include_pinned_delayed_memory_reports( touched_by_report[report_id] = turn_id reports_to_persist[report_id] = updated_report - appended_reports[report_id] = { + loaded_reports[report_id] = { **updated_report, "id": report_id, } @@ -2242,10 +2240,10 @@ def include_pinned_delayed_memory_reports( persist_delayed_memory_reports(reports_to_persist) - return appended_reports + return loaded_reports -def remove_delayed_memory_report( +def unload_delayed_memory_report( context, payload: str, ) -> dict: @@ -2256,7 +2254,7 @@ def remove_delayed_memory_report( if not report_id: return build_delayed_memory_failure_result( - action="remove_delayed_memory", + action="unload_delayed_memory", requested=payload, error="invalid_delayed_memory_id", ) @@ -2277,23 +2275,23 @@ def remove_delayed_memory_report( dict, ): return build_delayed_memory_failure_result( - action="remove_delayed_memory", + action="unload_delayed_memory", requested=report_id, error="delayed_memory_not_found", ) if bool(report.get("pinned", False)): return build_delayed_memory_failure_result( - action="remove_delayed_memory", + action="unload_delayed_memory", requested=report_id, error="delayed_memory_pinned", ) return { "ok": True, - "action": "remove_delayed_memory", + "action": "unload_delayed_memory", "id": report_id, - "detached": bool( + "unloaded": bool( report_id ), "title": ( @@ -2373,14 +2371,14 @@ def build_delayed_memory_action_text( failed = result.get("ok") is False - if action == "append_delayed_memory": + if action == "load_delayed_memory": return ( f"Load failed: {title}" if failed else f"Loading: {title}" ) - if action == "remove_delayed_memory": + if action == "unload_delayed_memory": return ( f"Unload failed: {title}" if failed @@ -2464,10 +2462,10 @@ def build_delayed_memory_history_text( if action == "save_delayed_memory_content": return f"Delayed memory saved: {title}" - if action == "append_delayed_memory": + if action == "load_delayed_memory": return f"Delayed memory loaded: {title}" - if action == "remove_delayed_memory": + if action == "unload_delayed_memory": return f"Delayed memory unloaded from context: {title}" if action == "update_delayed_memory": diff --git a/utils/context/assets.py b/utils/context/assets.py index 63ed718f..e812afd9 100644 --- a/utils/context/assets.py +++ b/utils/context/assets.py @@ -1,11 +1,10 @@ -# Formats asset action and skill listing results for runtime context output. +# Formats asset action results for runtime context output. import re from .formatting import ( format_tool_result_payload, ) from .skills import ( - format_list_skills_result, format_missing_skill_result, ) @@ -71,23 +70,6 @@ def format_asset_result_sections( sections = [] pending_results = [] - latest_list_skills_index = None - - for index, result in enumerate( - payload, - ): - if ( - isinstance( - result, - dict, - ) - and result.get( - "action" - ) - == "list_skills" - ): - latest_list_skills_index = index - def flush_pending_results() -> None: if not pending_results: return @@ -115,7 +97,7 @@ def flush_pending_results() -> None: and result.get( "action" ) - == "append_skill" + == "load_skill" and result.get("ok") is False and result.get("error") == "skill_not_found" ): @@ -130,33 +112,6 @@ def flush_pending_results() -> None: ) continue - if ( - isinstance( - result, - dict, - ) - and result.get( - "action" - ) - == "list_skills" - ): - if index != latest_list_skills_index: - continue - - flush_pending_results() - sections.append( - ( - _format_action_result_name( - result, - ), - format_list_skills_result( - result, - context, - ), - ) - ) - continue - pending_results.append( result ) diff --git a/utils/context/context_exports.py b/utils/context/context_exports.py index c326f735..b4396977 100644 --- a/utils/context/context_exports.py +++ b/utils/context/context_exports.py @@ -12,9 +12,10 @@ get_visible_turn_count, ) from .skills import ( - _appended_skill_names, + _loaded_skill_names, _normalize_skill_status_name, - format_list_skills_result, + build_skills_inventory_context, + format_skills_inventory, format_missing_skill_result, ) from .session_actions import ( @@ -62,7 +63,8 @@ "format_context_message_age_suffix", "format_delayed_memory_report_result", "format_delayed_memory_result_sections", - "format_list_skills_result", + "build_skills_inventory_context", + "format_skills_inventory", "format_missing_skill_result", "format_session_action_age", "format_session_result_sections", diff --git a/utils/context/delayed_memory.py b/utils/context/delayed_memory.py index 4e368c25..356d2c91 100644 --- a/utils/context/delayed_memory.py +++ b/utils/context/delayed_memory.py @@ -1,4 +1,4 @@ -# Formats delayed memory tool results and appended delayed memory context blocks. +# Formats delayed memory tool results and loaded delayed memory context blocks. from .formatting import ( format_tool_result_payload, ) @@ -90,7 +90,7 @@ def format_delayed_memory_result_sections( or "" ) - if action == "append_delayed_memory": + if action == "load_delayed_memory": if result.get("ok") is False: sections.append( ( @@ -102,7 +102,7 @@ def format_delayed_memory_result_sections( ) continue - if action == "remove_delayed_memory": + if action == "unload_delayed_memory": sections.append( ( "UNLOAD_DELAYED_MEMORY", diff --git a/utils/context/runtime_state.py b/utils/context/runtime_state.py index a5e4c0ea..8a2d38e5 100644 --- a/utils/context/runtime_state.py +++ b/utils/context/runtime_state.py @@ -14,7 +14,6 @@ from contracts.rules_assembler import ( RUNTIME_ACTION_ASSET_ACTION, RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - RUNTIME_ACTION_LIST_SKILLS, RUNTIME_ACTION_SAVE_SESSION, RUNTIME_ACTION_WEB_SEARCH, ) @@ -119,9 +118,7 @@ def build_runtime_xml( in enabled_actions ), can_use_assets=( - RUNTIME_ACTION_LIST_SKILLS - in enabled_actions - or RUNTIME_ACTION_ASSET_ACTION + RUNTIME_ACTION_ASSET_ACTION in enabled_actions ), can_save_session=( diff --git a/utils/context/skills.py b/utils/context/skills.py index 0334a516..91f0fae4 100644 --- a/utils/context/skills.py +++ b/utils/context/skills.py @@ -1,9 +1,9 @@ -# Formats appended skill state and skill listing results for runtime context output. +# Formats the always-visible skill inventory and loaded skill state. import re +from xml.sax.saxutils import escape -from rules.runtime import ( - NO_ENTRIES_FOUND_MESSAGE, -) +from utils.brain_client_utils import indent_xml +from utils.skills_asset_utils import list_skills def _normalize_skill_status_name( @@ -35,21 +35,21 @@ def _normalize_skill_status_name( ) -def _appended_skill_names( +def _loaded_skill_names( context=None, ) -> set[str]: - appended_skills = list( + loaded_skills = list( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) names = set() - for skill in appended_skills: + for skill in loaded_skills: if isinstance( skill, dict, @@ -72,40 +72,22 @@ def _appended_skill_names( return names -def format_list_skills_result( - result: dict, +def format_skills_inventory( + skills, context=None, ) -> str: - lines = [] - - skills = [ - skill - for skill in result.get( - "skills", - [], - ) - or [] - if isinstance( - skill, - dict, - ) - ] - - if not skills: - lines.append( - NO_ENTRIES_FOUND_MESSAGE - ) - return "\n".join( - lines - ) - - appended_names = _appended_skill_names( + loaded_names = _loaded_skill_names( context ) + lines = [] for index, skill in enumerate( - skills, + ( + skill + for skill in (skills or []) + if isinstance(skill, dict) + ), start=1, ): name = str( @@ -114,32 +96,35 @@ def format_list_skills_result( "", ) or "" - ).strip() - - if not name: - name = "(unnamed skill)" + ).strip() or "(unnamed skill)" - status = "" - if _normalize_skill_status_name( - name - ) in appended_names: - status = " (appended)" - - path = str( - skill.get( - "path", - "", + status = ( + " (loaded)" + if _normalize_skill_status_name(name) in loaded_names + else "" + ) + modes = [ + str(mode).strip() + for mode in skill.get( + "modes", + [], ) - or "" - ).strip() - path_suffix = ( - f" - {path}" - if path + or [] + if str(mode).strip() + ] + modes_suffix = ( + f" [modes: {', '.join(modes)}]" + if modes else "" ) lines.append( - f"{index}. {name}{status}{path_suffix}" + f"{index}. {name}{status}{modes_suffix}" + ) + + if not lines: + lines.append( + "No project skills available." ) return "\n".join( @@ -147,6 +132,26 @@ def format_list_skills_result( ) +def build_skills_inventory_context( + context=None, +) -> str: + + result = list_skills() + body = format_skills_inventory( + result.get( + "skills", + [], + ), + context, + ) + + return ( + "\n" + f"{indent_xml(escape(body), spaces=4)}\n" + "" + ) + + def format_missing_skill_result( result: dict, ) -> str: @@ -163,7 +168,6 @@ def format_missing_skill_result( requested = "unknown" return ( - "You attempted to append a skill that does not exist: " + "You attempted to load a skill that does not exist: " f"{requested}" ) - diff --git a/utils/context/tool_results.py b/utils/context/tool_results.py index 8961017d..7dc8ccd1 100644 --- a/utils/context/tool_results.py +++ b/utils/context/tool_results.py @@ -216,30 +216,29 @@ def _append_recorded_tool_results( return appended -def _append_appended_skills( - parts: list[str], +def build_loaded_skills_content_context( context=None, -) -> None: +) -> str: if context is None: - return + return "" - appended_skills = list( + loaded_skills = list( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) - if not appended_skills: - return + if not loaded_skills: + return "" - parts.append( - "\n" - f"{indent_xml(escape(format_tool_result_payload(appended_skills)))}\n" - "" + return ( + "\n" + f"{indent_xml(escape(format_tool_result_payload(loaded_skills)))}\n" + "" ) @@ -291,7 +290,7 @@ def _append_asset_results( ) -def _append_delayed_memory_results( +def _load_delayed_memory_results( parts: list[str], context=None, ) -> None: @@ -335,17 +334,11 @@ def build_tool_results_context( ) -> str: tool_result_blocks = [] - extra_parts = [] - if _append_recorded_tool_results( + if not _append_recorded_tool_results( tool_result_blocks, context, ): - _append_appended_skills( - extra_parts, - context, - ) - else: _append_tool_results( tool_result_blocks, context, @@ -354,24 +347,11 @@ def build_tool_results_context( tool_result_blocks, context, ) - _append_delayed_memory_results( + _load_delayed_memory_results( tool_result_blocks, context, ) - _append_appended_skills( - extra_parts, - context, - ) - parts = [ - build_tools_results_context( - tool_result_blocks - ) - ] - parts.extend( - extra_parts - ) - - return "\n".join( - parts + return build_tools_results_context( + tool_result_blocks ) diff --git a/utils/delayed_memory_file_store.py b/utils/delayed_memory_file_store.py index 9d78f195..bc211c96 100644 --- a/utils/delayed_memory_file_store.py +++ b/utils/delayed_memory_file_store.py @@ -103,6 +103,23 @@ def _clean_tags(value) -> list[str]: return tags + + +def _read_delayed_load_metadata(report: dict, key: str, default=None): + if key in report: + return report.get(key, default) + + legacy_prefix = "append" + "ed" + legacy_keys = { + "loaded_times": f"{legacy_prefix}_times", + "load_streak": "append_streak", + "last_loaded_date": f"last_{legacy_prefix}_date", + "last_loaded_session_id": f"last_{legacy_prefix}_session_id", + "all_loaded_session_ids": f"all_{legacy_prefix}_session_ids", + } + legacy_key = legacy_keys.get(key, "") + + return report.get(legacy_key, default) if legacy_key else default def normalize_delayed_memory_report( report_id: str, report, @@ -168,22 +185,22 @@ def normalize_delayed_memory_report( ), "created_time": created_time, "created_date": created_date, - "appended_times": _clean_counter( - report.get("appended_times", 0), + "loaded_times": _clean_counter( + _read_delayed_load_metadata(report, "loaded_times", 0), ), - "append_streak": _clean_counter( - report.get("append_streak", 0), + "load_streak": _clean_counter( + _read_delayed_load_metadata(report, "load_streak", 0), ), - "last_appended_date": _clean_text( - report.get("last_appended_date", ""), + "last_loaded_date": _clean_text( + _read_delayed_load_metadata(report, "last_loaded_date", ""), limit=MAX_DELAYED_MEMORY_TIME_CHARS, ), - "last_appended_session_id": _clean_text( - report.get("last_appended_session_id", ""), + "last_loaded_session_id": _clean_text( + _read_delayed_load_metadata(report, "last_loaded_session_id", ""), limit=MAX_DELAYED_MEMORY_SESSION_ID_CHARS, ), - "all_appended_session_ids": _clean_session_ids( - report.get("all_appended_session_ids", []), + "all_loaded_session_ids": _clean_session_ids( + _read_delayed_load_metadata(report, "all_loaded_session_ids", []), ), } @@ -323,8 +340,8 @@ def build_delayed_memory_file_payload( "id": normalized_id, "session": clean_report["created_session_id"], "created_date": clean_report["created_date"], - "all_appended_session_ids": clean_report[ - "all_appended_session_ids" + "all_loaded_session_ids": clean_report[ + "all_loaded_session_ids" ], "body": clean_report["body"], "pinned": clean_report["pinned"], @@ -334,13 +351,13 @@ def build_delayed_memory_file_payload( "facts_ids": clean_report[ "facts_ids" ], - "appended_times": clean_report["appended_times"], - "append_streak": clean_report["append_streak"], - "last_appended_date": clean_report[ - "last_appended_date" + "loaded_times": clean_report["loaded_times"], + "load_streak": clean_report["load_streak"], + "last_loaded_date": clean_report[ + "last_loaded_date" ], - "last_appended_session_id": clean_report[ - "last_appended_session_id" + "last_loaded_session_id": clean_report[ + "last_loaded_session_id" ], } diff --git a/utils/python_skill_asset_utils.py b/utils/python_skill_asset_utils.py index ca7806ee..6c582302 100644 --- a/utils/python_skill_asset_utils.py +++ b/utils/python_skill_asset_utils.py @@ -1061,7 +1061,7 @@ def _materialize_attachment( ) -def _require_appended_skill( +def _require_loaded_skill( context, skill: str, ) -> str: @@ -1069,7 +1069,7 @@ def _require_appended_skill( requested = normalize_skill_name( skill ) - appended_names = { + loaded_names = { normalize_skill_name( item.get( "name", @@ -1084,19 +1084,19 @@ def _require_appended_skill( for item in ( getattr( context, - "runtime_appended_skills", + "runtime_loaded_skills", [], ) or [] ) } - appended_names.discard( + loaded_names.discard( "" ) - if requested not in appended_names: + if requested not in loaded_names: raise PermissionError( - f"skill must be appended before execution: {requested}" + f"skill must be loaded before execution: {requested}" ) return requested @@ -1270,7 +1270,7 @@ async def run_python_skill_action( "args must be a list" ) - _require_appended_skill( + _require_loaded_skill( context, skill_name, ) @@ -2652,7 +2652,7 @@ async def run_document_reader_action( or "chunk_reader" ).strip() - _require_appended_skill( + _require_loaded_skill( context, skill_name, ) diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index ddead0fa..dfbbce94 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -123,8 +123,8 @@ def get_current_action_sequence_started_at( ACTION_DISPLAY_ALIASES = { "append_asset_file": "Appended asset file", - "append_delayed_memory": "Loaded delayed memory", - "append_skill": "Appended skill", + "load_delayed_memory": "Loaded delayed memory", + "load_skill": "Loaded skill", "append_wildcard_file": "Appended wildcard file", "asset_action": "Asset action", "check_duplicates": "Checked duplicates", @@ -134,15 +134,12 @@ def get_current_action_sequence_started_at( "create_wildcard_library": "Created wildcard library", "expand_template": "Expanded template", "generate_prompt_batch": "Generated prompt batch", - "list_skills": "Listed skills", "list_wildcards": "Listed wildcards", - "load_delayed_memory": "Loaded delayed memory", "preview_file": "Previewed file", "read_asset_file": "Read asset file", "read_asset_text": "Read asset text", - "remove_delayed_memory": "Unloaded delayed memory", - "remove_skill": "Removed skill", "unload_delayed_memory": "Unloaded delayed memory", + "unload_skill": "Unloaded skill", "resolve_active_memory": "Resolved active memory", "run_document_reader": "Read document iteratively", "run_python_skill": "Ran Python skill", @@ -1179,10 +1176,10 @@ def _build_payload_distinct_session_action_parts( ) skill_marker_action = action_name in { - "APPEND_SKILL", - "APPEND_SKILLS", - "REMOVE_SKILL", - "REMOVE_SKILLS", + "LOAD_SKILL", + "LOAD_SKILLS", + "UNLOAD_SKILL", + "UNLOAD_SKILLS", } if ( @@ -1483,10 +1480,10 @@ def _build_formatted_session_action_marker_parts( ).strip() }) skill_marker_action = action_name in { - "APPEND_SKILL", - "APPEND_SKILLS", - "REMOVE_SKILL", - "REMOVE_SKILLS", + "LOAD_SKILL", + "LOAD_SKILLS", + "UNLOAD_SKILL", + "UNLOAD_SKILLS", } payload_distinct_parts = ( _build_payload_distinct_session_action_parts( @@ -1551,8 +1548,8 @@ def _build_formatted_session_action_marker_parts( ) elif ( action_name in { - "APPEND_SKILL", - "REMOVE_SKILL", + "LOAD_SKILL", + "UNLOAD_SKILL", } and payloads ): diff --git a/utils/skills_asset_utils.py b/utils/skills_asset_utils.py index 5bf9ba81..fc2e6977 100644 --- a/utils/skills_asset_utils.py +++ b/utils/skills_asset_utils.py @@ -234,7 +234,6 @@ def list_skills(skill: str = "") -> dict: return { "ok": True, - "action": "list_skills", "requested": requested, "skills": items, } @@ -256,7 +255,7 @@ def load_skill( if entry is None: return { "ok": False, - "action": "append_skill", + "action": "load_skill", "requested": requested, "error": "skill_not_found", } @@ -271,7 +270,7 @@ def load_skill( return { "ok": True, - "action": "append_skill", + "action": "load_skill", "requested": requested, "skill": item, } diff --git a/utils/stream_handler.py b/utils/stream_handler.py index ca2bc9d1..64f23bac 100644 --- a/utils/stream_handler.py +++ b/utils/stream_handler.py @@ -55,14 +55,61 @@ def build_validator_error_text( or "Generation stopped." ) - if validator.last_failure_preview: + loop_quote = ( + validator.last_failure_loop_preview + or validator.last_failure_preview + ) + + if loop_quote: reason = ( f'{reason} Looped text: ' - f'"{validator.last_failure_preview}"' + f'"{loop_quote}"' ) return reason + def build_validator_loop_log_text( + self, + validator, + ) -> str: + + reason = ( + validator.last_failure_reason + or "Generation stopped." + ) + loop_quote = ( + validator.last_failure_loop_preview + or validator.last_failure_preview + ) + + if not loop_quote: + return reason + + return ( + f'{reason}\n' + f'"{loop_quote}"' + ) + + async def log_validator_loop( + self, + validator, + ): + + log_method = getattr( + self.logger, + "log_validator_loop", + None, + ) + + if log_method is None: + log_method = self.logger.log_validator + + await log_method( + self.build_validator_loop_log_text( + validator + ) + ) + # --------------------------------------------------------- # START STREAM # --------------------------------------------------------- @@ -130,15 +177,8 @@ async def send_thinking( failure_reason ) - raw_chunk_preview = ( - chunk - .replace("\n", "\\n") - )[:160] - - await self.logger.log_validator( - f"{self.thinking_validator.last_failure_reason}\n" - f'Preview: "{self.thinking_validator.last_failure_preview}"\n' - f'Raw thinking chunk: "{raw_chunk_preview}"' + await self.log_validator_loop( + self.thinking_validator ) if emit: @@ -151,6 +191,7 @@ async def send_thinking( "text": self.build_validator_error_text( self.thinking_validator ), + "suppress_log": True, }) return False @@ -247,21 +288,8 @@ async def send_content( if not is_valid: - raw_chunk_preview = ( - chunk - .replace("\n", "\\n") - )[:160] - - safe_chunk_preview = ( - safe_chunk - .replace("\n", "\\n") - )[:160] - - await self.logger.log_validator( - f"{self.validator.last_failure_reason}\n" - f'Preview: "{self.validator.last_failure_preview}"\n' - f'Raw chunk: "{raw_chunk_preview}"\n' - f'Safe chunk: "{safe_chunk_preview}"' + await self.log_validator_loop( + self.validator ) reason = ( @@ -278,6 +306,7 @@ async def send_content( self.message_id ), "text": reason, + "suppress_log": True, }) return False diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 80b1b9cf..1026f4c6 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -175,6 +175,16 @@ def build_preview( .strip() )[:TRUNCATE] +def build_loop_preview( + text: str, +) -> str: + + return ( + str(text or "") + .replace("\n", "\\n") + .strip() + ) + class StreamValidator: def __init__(self): @@ -672,7 +682,7 @@ def validate_word_loops( ) self.last_failure_preview = build_preview(preview) - self.last_failure_loop_preview = build_preview( + self.last_failure_loop_preview = build_loop_preview( last_word ) @@ -730,7 +740,7 @@ def validate_word_loops( self.last_failure_preview = build_preview( preview ) - self.last_failure_loop_preview = build_preview( + self.last_failure_loop_preview = build_loop_preview( loop_preview ) @@ -987,7 +997,7 @@ def validate_recurrent_sentence_loop( self.last_failure_preview = build_preview( preview ) - self.last_failure_loop_preview = build_preview( + self.last_failure_loop_preview = build_loop_preview( sentence.strip() ) @@ -1101,8 +1111,8 @@ def validate_sentence_sequence_loop( self.last_failure_preview = build_preview( "".join(sequence) ) - self.last_failure_loop_preview = ( - self.last_failure_preview + self.last_failure_loop_preview = build_loop_preview( + current_sentence.strip() ) return False @@ -1190,8 +1200,8 @@ def validate_paragraphs( ) self.last_failure_preview = build_preview(paragraph) - self.last_failure_loop_preview = ( - self.last_failure_preview + self.last_failure_loop_preview = build_loop_preview( + paragraph ) return False diff --git a/websocket/__init__.py b/websocket/__init__.py index 2b1803a2..5811206b 100644 --- a/websocket/__init__.py +++ b/websocket/__init__.py @@ -36,6 +36,7 @@ ) from .bootstrap import ( apply_delayed_memory_reports, + apply_loaded_delayed_memory_ids, apply_runtime_memory_slot_delete, apply_runtime_resume, apply_session_bootstrap, @@ -272,6 +273,10 @@ async def process_pending_requests(): context, message_data, ) + apply_loaded_delayed_memory_ids( + context, + message_data, + ) for report_id in deleted_report_ids: delete_errors = delete_delayed_memory_report_files( report_id @@ -434,6 +439,10 @@ async def process_pending_requests(): context ) + await emit_delayed_memory_store_snapshot( + context + ) + await emit_runtime_session_memory_update( context ) diff --git a/websocket/bootstrap.py b/websocket/bootstrap.py index 074fd035..8fe465bc 100644 --- a/websocket/bootstrap.py +++ b/websocket/bootstrap.py @@ -141,6 +141,114 @@ def clean_deleted_delayed_memory_report_ids(value) -> list[str]: return report_ids +def clean_loaded_delayed_memory_report_ids(value) -> list[str]: + + source = value if isinstance(value, list) else [value] + report_ids = [] + seen = set() + + for item in source: + report_id = str( + item + or "" + ).strip().casefold() + + if ( + not report_id + or report_id in seen + or not is_delayed_memory_report_id( + report_id + ) + ): + continue + + seen.add(report_id) + report_ids.append(report_id) + + return report_ids + + +def apply_loaded_delayed_memory_ids( + context, + message_data: dict, +) -> list[str]: + + if "loaded_delayed_memory_ids" in message_data: + raw_ids = message_data.get( + "loaded_delayed_memory_ids", + [], + ) + elif "loaded_memory_ids" in message_data: + raw_ids = message_data.get( + "loaded_memory_ids", + [], + ) + else: + return list( + getattr( + context, + "runtime_loaded_delayed_memory_ids", + [], + ) + or [] + ) + + requested_ids = clean_loaded_delayed_memory_report_ids( + raw_ids + ) + reports = getattr( + context, + "delayed_memory_reports", + {}, + ) + reports = reports if isinstance(reports, dict) else {} + loaded_reports = {} + loaded_ids = [] + + for report_id in requested_ids: + report = reports.get(report_id) + + if not isinstance(report, dict): + continue + + loaded_ids.append(report_id) + loaded_reports[report_id] = { + **report, + "id": report_id, + } + + context.runtime_loaded_delayed_memory = loaded_reports + context.runtime_loaded_delayed_memory_ids = loaded_ids + + from runtime.L4_memory import ( + refresh_runtime_l4_archived_fact_ids, + ) + + refresh_runtime_l4_archived_fact_ids( + context + ) + + return loaded_ids + + +def get_context_loaded_delayed_memory_ids( + context, +) -> list[str]: + + loaded_reports = getattr( + context, + "runtime_loaded_delayed_memory", + {}, + ) + + if not isinstance(loaded_reports, dict): + return [] + + return clean_loaded_delayed_memory_report_ids( + list(loaded_reports.keys()) + ) + + def apply_delayed_memory_reports( context, message_data: dict, @@ -184,38 +292,38 @@ def apply_delayed_memory_reports( **incoming_reports, } - appended_reports = getattr( + loaded_reports = getattr( context, - "runtime_appended_delayed_memory", + "runtime_loaded_delayed_memory", None, ) if isinstance( - appended_reports, + loaded_reports, dict, ): for report_id in deleted_report_ids: - appended_reports.pop( + loaded_reports.pop( report_id, None, ) - appended_ids = getattr( + loaded_ids = getattr( context, - "runtime_appended_delayed_memory_ids", + "runtime_loaded_delayed_memory_ids", None, ) if isinstance( - appended_ids, + loaded_ids, list, ): deleted_report_id_set = set( deleted_report_ids ) - appended_ids[:] = [ + loaded_ids[:] = [ report_id - for report_id in appended_ids + for report_id in loaded_ids if str(report_id or "").strip().casefold() not in deleted_report_id_set ] @@ -1302,6 +1410,10 @@ def apply_session_bootstrap( context, message_data, ) + apply_loaded_delayed_memory_ids( + context, + message_data, + ) session_memory = clean_bootstrap_memory( message_data.get( @@ -1524,6 +1636,11 @@ async def emit_delayed_memory_store_snapshot( await context.emitter.emit({ "type": "delayed_memory_store_snapshot", "delayed_memory_reports": reports, + "loaded_delayed_memory_ids": ( + get_context_loaded_delayed_memory_ids( + context + ) + ), }) diff --git a/websocket/logger.py b/websocket/logger.py index 61ef2015..5d8cc9af 100644 --- a/websocket/logger.py +++ b/websocket/logger.py @@ -206,3 +206,12 @@ async def log_validator( message, details=details, ) + + async def log_validator_loop( + self, + message: str, + ): + await self.log( + "[VALIDATOR:LOOP]", + message, + ) From f668ba11e80396f0f80c9743164f06eda50dbd70 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:38:58 +0300 Subject: [PATCH 030/168] chore: txt attachments - append attached file content to the context max to 32k tokens --- tests/test_attachments.py | 81 +++++++++++--- ..._memory_citation_layers_client_contract.py | 2 +- ..._runtime_avatar_l4_ring_client_contract.py | 18 +++- ui/static/css/runtime-avatar.css | 8 +- ui/static/images/live-avatar.jpg | Bin 66021 -> 63939 bytes ui/static/js/runtime/runtime-avatar.js | 2 + ui/templates/index.html | 4 +- websocket/attachments.py | 101 ++++++++++++++++++ 8 files changed, 195 insertions(+), 21 deletions(-) diff --git a/tests/test_attachments.py b/tests/test_attachments.py index efc4ead8..8ed38f06 100644 --- a/tests/test_attachments.py +++ b/tests/test_attachments.py @@ -29,7 +29,7 @@ def test_redacted_attachment_for_log_redacts_full_text_content(self): redacted["text_content"], ) - def test_attachment_context_omits_text_preview_for_text_files(self): + def test_attachment_context_includes_full_text_for_text_files(self): context = format_attachment_context({ "attachments": [ { @@ -37,18 +37,16 @@ def test_attachment_context_omits_text_preview_for_text_files(self): "kind": "text", "type": "text/plain", "size_label": "12 KB", - "text_preview": "do not send this preview", - "text_content": "full content stays available to skills", + "text_preview": "preview must not replace full text", + "text_content": "full text body", }, { "name": "README.md", "kind": "text", "type": "text/markdown", - "size_label": "304.8 KB", - "text_preview": "# Dictionary body", - "preview_limit": 2000, - "truncated": True, - "text_content": "full markdown stays available to skills", + "size_label": "3 KB", + "text_preview": "# Preview heading", + "text_content": "# Real heading\n\nMarkdown body.", }, ], }) @@ -58,23 +56,78 @@ def test_attachment_context_omits_text_preview_for_text_files(self): context, ) self.assertIn( - "- README.md: text, text/markdown, 304.8 KB", + "full text body", context, ) - self.assertNotIn( - "runtime_attachment", + self.assertIn( + "--- BEGIN ATTACHMENT TEXT: README.md ---", + context, + ) + self.assertIn( + "# Real heading\n\nMarkdown body.", context, ) self.assertNotIn( - "text_preview", + "preview must not replace full text", context, ) self.assertNotIn( - "do not send this preview", + "# Preview heading", + context, + ) + + def test_attachment_context_falls_back_to_preview_for_older_text_payloads(self): + context = format_attachment_context({ + "attachments": [ + { + "name": "legacy.md", + "kind": "text", + "type": "text/markdown", + "text_preview": "legacy markdown body", + }, + ], + }) + + self.assertIn( + "legacy markdown body", + context, + ) + + def test_attachment_text_context_has_shared_message_budget(self): + context = format_attachment_context( + { + "attachments": [ + { + "name": "one.md", + "kind": "text", + "type": "text/markdown", + "text_content": "abcdefgh", + }, + { + "name": "two.md", + "kind": "text", + "type": "text/markdown", + "text_content": "ijklmnop", + }, + ], + }, + max_text_chars=10, + ) + + self.assertIn( + "abcdefgh", + context, + ) + self.assertIn( + "ij", context, ) self.assertNotIn( - "# Dictionary body", + "klmnop", + context, + ) + self.assertIn( + "[attachment text truncated: 6 chars omitted]", context, ) diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index 741657fa..66c0de97 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -115,7 +115,7 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/runtime-memory.css?v=delayed-context-loaded-1", source) self.assertIn("/static/css/theme-win95.css?v=delayed-context-loaded-1", source) self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1", source) - self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-19", source) + self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-20", source) self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) diff --git a/tests/test_runtime_avatar_l4_ring_client_contract.py b/tests/test_runtime_avatar_l4_ring_client_contract.py index 20e4f796..2ddc5e04 100644 --- a/tests/test_runtime_avatar_l4_ring_client_contract.py +++ b/tests/test_runtime_avatar_l4_ring_client_contract.py @@ -167,6 +167,20 @@ def test_avatar_core_button_toggles_memory_layers_without_refresh(self): ".jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-ring", css_source, ) + self.assertIn( + ".jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-scaffold", + css_source, + ) + self.assertIn( + ".jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-orbit-entry", + css_source, + ) + self.assertIn( + ".jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-center-ring", + css_source, + ) + self.assertIn('class: "jin-avatar-scaffold"', avatar_source) + self.assertIn('class: "jin-avatar-center-ring"', avatar_source) click_start = input_source.index("if (factCheckTrigger) {") click_end = input_source.index("chatForm.addEventListener", click_start) @@ -178,11 +192,11 @@ def test_avatar_cache_versions_are_bumped(self): source = INDEX_HTML.read_text(encoding="utf-8") self.assertIn( - "/static/css/runtime-avatar.css?v=memory-rings-8", + "/static/css/runtime-avatar.css?v=memory-rings-9", source, ) self.assertIn( - "/static/js/runtime/runtime-avatar.js?v=memory-rings-19", + "/static/js/runtime/runtime-avatar.js?v=memory-rings-20", source, ) self.assertIn( diff --git a/ui/static/css/runtime-avatar.css b/ui/static/css/runtime-avatar.css index 91383bf1..5e6f1392 100644 --- a/ui/static/css/runtime-avatar.css +++ b/ui/static/css/runtime-avatar.css @@ -210,9 +210,13 @@ opacity 0.28s cubic-bezier(0.22, 0.61, 0.36, 1); } +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-scaffold, +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-orbit-entry, .jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-ring, -.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-dash { - opacity: 0; +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-memory-dash, +.jin-runtime-avatar.is-memory-layers-hidden .jin-avatar-center-ring { + opacity: 0 !important; + visibility: hidden; pointer-events: none; } diff --git a/ui/static/images/live-avatar.jpg b/ui/static/images/live-avatar.jpg index e05727337327a08ce2ce7d47b7e7b2cacd17811c..44b01d2ba51a56b5b3a63ea27c3dcf9fc6b1f7d4 100644 GIT binary patch literal 63939 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 z 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 z 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 { center.appendChild(createSvgElement("circle", { + class: "jin-avatar-center-ring", cx: CENTER, cy: CENTER, r: radius, diff --git a/ui/templates/index.html b/ui/templates/index.html index 160ff00a..52f6280e 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -17,7 +17,7 @@ - + @@ -509,7 +509,7 @@ - + diff --git a/websocket/attachments.py b/websocket/attachments.py index 7875c144..88506a39 100644 --- a/websocket/attachments.py +++ b/websocket/attachments.py @@ -1,3 +1,6 @@ +TEXT_ATTACHMENT_CONTEXT_MAX_CHARS = 32000 + + def has_message_attachments( message_data: dict, ) -> bool: @@ -14,8 +17,48 @@ def has_message_attachments( ) +def _normalize_attachment_text( + value, +) -> str: + + return str( + value + if value is not None + else "" + ).replace( + "\r\n", + "\n", + ).replace( + "\r", + "\n", + ) + + +def _get_attachment_text_content( + attachment: dict, +) -> str: + + if "text_content" in attachment: + return _normalize_attachment_text( + attachment.get( + "text_content", + ) + ) + + # Backward compatibility for turns created by older clients that only + # supplied the preview field. + return _normalize_attachment_text( + attachment.get( + "text_preview", + "", + ) + ) + + def format_attachment_context( message_data: dict, + *, + max_text_chars: int = TEXT_ATTACHMENT_CONTEXT_MAX_CHARS, ) -> str: attachments = message_data.get( @@ -28,6 +71,21 @@ def format_attachment_context( ): return "" + try: + remaining_text_chars = max( + 0, + int( + max_text_chars + ), + ) + except ( + TypeError, + ValueError, + ): + remaining_text_chars = ( + TEXT_ATTACHMENT_CONTEXT_MAX_CHARS + ) + lines = [ "Attached context:", ] @@ -98,6 +156,49 @@ def format_attachment_context( f"- {name}: {', '.join(detail_parts)}" ) + if kind.strip().lower() != "text": + continue + + text_content = _get_attachment_text_content( + attachment + ) + + if not text_content.strip(): + continue + + if remaining_text_chars <= 0: + lines.append( + "[attachment text omitted: text context budget exhausted]" + ) + continue + + visible_text = text_content[ + :remaining_text_chars + ] + omitted_chars = ( + len(text_content) + - len(visible_text) + ) + remaining_text_chars -= len( + visible_text + ) + + lines.append( + f"--- BEGIN ATTACHMENT TEXT: {name} ---" + ) + lines.append( + visible_text + ) + + if omitted_chars > 0: + lines.append( + f"[attachment text truncated: {omitted_chars} chars omitted]" + ) + + lines.append( + f"--- END ATTACHMENT TEXT: {name} ---" + ) + if not included: return "" From a60268bf0c42bfade8ef26b0a830294b3bdc239d Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:28:04 +0300 Subject: [PATCH 031/168] chore: runtime context visibility - add real runtime change markers to avatar - add structured collapsible context viewer and numbered newest-first L4 facts - keep latest reasoning citations active with hover preview for previous turns - remove memory text glow for smoother scrolling - preserve full reasoning-loop previews and group same-turn runtime actions --- LIVE_AVATAR.md | 28 +- ..._memory_citation_layers_client_contract.py | 18 +- ui/static/css/runtime-memory.css | 307 +++++++++- ui/static/js/logger/trace-modal.js | 547 ++++++++++++++++++ ui/static/js/runtime/runtime-avatar.js | 341 ++++++++--- ui/static/js/runtime/runtime-memory-view.js | 80 ++- ui/static/js/think-citations.js | 236 +++++++- ui/templates/index.html | 4 +- utils/session_actions_history.py | 49 +- utils/stream_validator.py | 14 +- 10 files changed, 1507 insertions(+), 117 deletions(-) diff --git a/LIVE_AVATAR.md b/LIVE_AVATAR.md index 9bceb20a..c20e2db1 100644 --- a/LIVE_AVATAR.md +++ b/LIVE_AVATAR.md @@ -17,7 +17,7 @@ Read it like this: | Inner moving rings | Current runtime memory lines | | Faster inner motion | Larger runtime memory diff | | Color shifts in inner rings | Keyword/emotional content in runtime memory | -| Small nodes and stripes | Pending or long/notable runtime memory details | +| Runtime change markers | Last real runtime transition: filled = new line, hollow = changed line | | L4 blue dashes | Long-term facts | | Dim L4 dots | L4 facts already represented by delayed reports | | Delayed report dashes | Stored delayed memory reports | @@ -129,7 +129,9 @@ These rings are the most "alive" part of the avatar: they change radius, speed, | Ring direction | Seeded random direction | | Ring color | Keyword palette plus emotional/alert influence | | Extra stripes | The line is long compared to the others | -| Small colored nodes | The line contains pending-related language | +| Filled change marker | The line was added in the last real runtime transition | +| Hollow change marker | The line changed in the last real runtime transition | +| Change marker size | Magnitude of the line change | ### Runtime Ring Controls @@ -141,7 +143,7 @@ These rings are the most "alive" part of the avatar: they change radius, speed, | Dash texture | `runtime-avatar.js` | `appendOrbit()` -> `dashLength`, `gapLength`, `strokeWidth` | | Arc fragments | `runtime-avatar.js` | `appendOrbit()` -> `arcCount`, `appendArcCircle()` | | Long-line stripes | `runtime-avatar.js` | `appendLongFieldStripes()` | -| Pending nodes | `runtime-avatar.js` | `appendPendingNodes()` | +| Runtime change markers | `runtime-avatar.js` | `appendRuntimeChangeMarker()`, `resolveRuntimeChangeMarkers()` | ### Runtime Ring Speed @@ -167,6 +169,25 @@ To make them more energetic: const baseSpeed = 18 + random() * 52; ``` +### Runtime Change Markers + +Runtime circles are no longer decorative and are no longer triggered by words such as `pending`. + +They show the last real runtime memory transition: + +| Marker | Meaning | +|---|---| +| Filled circle | A runtime line was newly added | +| Hollow circle | An existing runtime line changed | +| Marker size | Larger `key_change_ratio` / `value_change_ratio` | +| Marker angle | Stable hash of the runtime line identity | + +If a later snapshot has no real line changes, the previous change markers remain visible. The avatar searches backward through runtime snapshot history until it finds the latest real transition. This keeps the last meaningful transition visible instead of making the markers disappear on a no-op redraw. + +A removal-only transition clears the old markers: the removed line is represented by its orbit disappearing, so no stale circle is carried forward. + +Random orbit circles and pending-keyword circles are intentionally not rendered. + ## Runtime Colors Runtime colors come from a weighted palette. The avatar looks at text in the current runtime snapshot and blends colors if specific words appear. @@ -179,7 +200,6 @@ The default palettes live near the top of: |---|---| | `KEYWORD_PALETTE` | Softly pushes rings toward thematic colors | | `AGGRESSIVE_PALETTE` | Strong alert-like coloring for high-priority words | -| `PENDING_NODE_PALETTE` | Colors small pending nodes | Current keyword palette: diff --git a/tests/test_memory_citation_layers_client_contract.py b/tests/test_memory_citation_layers_client_contract.py index 66c0de97..c22fd200 100644 --- a/tests/test_memory_citation_layers_client_contract.py +++ b/tests/test_memory_citation_layers_client_contract.py @@ -54,6 +54,22 @@ def test_reasoning_citation_reveal_is_not_hover_gated(self): self.assertIn("function resetThinkCitationHighlightTurn()", source) self.assertIn("resetThinkCitationHighlightTurn,", source) self.assertIn("window.JinThinkCitations.resetThinkCitationHighlightTurn();", chat_source) + + def test_only_latest_reasoning_is_persistent_and_old_reasoning_is_hover_preview(self): + source = THINK_CITATIONS_JS.read_text(encoding="utf-8") + + self.assertIn("let latestThinkCitationTarget = null;", source) + self.assertIn("let hoveredThinkCitationTarget = null;", source) + self.assertIn("function getActiveThinkCitationTarget()", source) + self.assertIn("thinkContent === getActiveThinkCitationTarget()", source) + self.assertIn("latestThinkCitationTarget =\n thinkContent;", source) + self.assertIn('"mouseenter",', source) + self.assertIn('"mouseleave",', source) + self.assertIn("activateHoveredThinkCitation(", source) + self.assertIn("deactivateHoveredThinkCitation(", source) + self.assertIn('origin: "think-citation-hover"', source) + self.assertNotIn('element.classList.add(\n "has-rule-highlights"', source) + def test_latest_persistent_reference_text_combines_reasoning_and_answer(self): source = CHAT_JS.read_text(encoding="utf-8") start = source.index("const memoryReferenceText =") @@ -116,7 +132,7 @@ def test_cache_versions_are_bumped_for_citation_sync_assets(self): self.assertIn("/static/css/theme-win95.css?v=delayed-context-loaded-1", source) self.assertIn("/static/js/runtime/runtime-memory-view.js?v=delayed-context-loaded-1", source) self.assertIn("/static/js/runtime/runtime-avatar.js?v=memory-rings-20", source) - self.assertIn("/static/js/think-citations.js?v=think-citations-5", source) + self.assertIn("/static/js/think-citations.js?v=think-citations-6", source) self.assertIn("/static/js/chat.js?v=reasoning-gap-1", source) diff --git a/ui/static/css/runtime-memory.css b/ui/static/css/runtime-memory.css index de47404e..04b41f18 100644 --- a/ui/static/css/runtime-memory.css +++ b/ui/static/css/runtime-memory.css @@ -71,13 +71,34 @@ opacity: 0.5; } +/* Keep highlight colors, but remove expensive text glow from every memory record. */ +#runtime-memory-text .runtime-memory-line .runtime-memory-key, +#runtime-memory-text .runtime-memory-line .runtime-memory-value { + text-shadow: none !important; +} + +.runtime-memory-l4-row .runtime-memory-fact-number { + display: inline-block; + min-width: 3ch; + margin-right: 0.25ch; + color: rgba(161, 161, 170, 0.68); + font-variant-numeric: tabular-nums; + text-align: right; +} + +.runtime-memory-l4-row .runtime-memory-fact-separator { + display: inline-block; + width: 1.4em; + color: rgba(161, 161, 170, 0.48); + text-align: center; +} + .runtime-memory-key, .runtime-memory-value { color: rgba(244,244,245,0.62); transition: color 0.62s cubic-bezier(0.22, 0.61, 0.36, 1), - font-weight 0.62s cubic-bezier(0.22, 0.61, 0.36, 1), - text-shadow 0.62s cubic-bezier(0.22, 0.61, 0.36, 1); + font-weight 0.62s cubic-bezier(0.22, 0.61, 0.36, 1); } .runtime-memory-value { @@ -486,3 +507,285 @@ letter-spacing: 0.08em; text-transform: uppercase; } + +/* Structured context-window viewer inside the shared trace modal. */ +.jin-context-trace-modal .delayed-memory-modal-panel { + width: min(1320px, calc(100vw - 32px)); + max-width: 1320px; +} + +.jin-context-trace-modal .delayed-memory-modal-content { + padding: 14px; + background: + linear-gradient(180deg, rgba(24, 24, 27, 0.30), rgba(9, 9, 11, 0.04)); +} + +.jin-context-overview { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; + padding: 2px 2px 8px; +} + +.jin-context-overview-title { + color: rgba(228, 228, 231, 0.92); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; +} + +.jin-context-overview-badges, +.jin-context-card-meta { + display: flex; + min-width: 0; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 6px; +} + +.jin-context-badge { + display: inline-flex; + min-width: 0; + align-items: center; + border: 1px solid rgba(63, 63, 70, 0.82); + border-radius: 999px; + background: rgba(24, 24, 27, 0.62); + padding: 2px 7px; + color: rgba(212, 212, 216, 0.74); + font-size: 9px; + line-height: 1.45; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.jin-context-badge-attribute { + border-color: rgba(56, 189, 248, 0.20); + background: rgba(8, 47, 73, 0.18); + color: rgba(186, 230, 253, 0.72); +} + +.jin-context-badge-muted { + opacity: 0.68; +} + +.jin-context-stack { + display: grid; + gap: 10px; +} + +.jin-context-card { + overflow: hidden; + border: 1px solid rgba(63, 63, 70, 0.78); + border-radius: 7px; + background: rgba(9, 9, 11, 0.48); + box-shadow: inset 0 1px rgba(255, 255, 255, 0.018); +} + +.jin-context-card-xml { + border-color: rgba(82, 82, 91, 0.92); +} + +.jin-context-card-user { + border-color: rgba(56, 189, 248, 0.34); + background: rgba(8, 47, 73, 0.12); +} + +.jin-context-card-header { + display: flex; + min-height: 38px; + align-items: center; + justify-content: space-between; + gap: 14px; + border-bottom: 1px solid rgba(63, 63, 70, 0.70); + background: rgba(24, 24, 27, 0.62); + padding: 7px 10px; + cursor: row-resize; + user-select: none; + transition: + background-color 0.16s ease, + border-color 0.16s ease; +} + +.jin-context-card-header:hover, +.jin-context-card-header:focus-visible { + outline: none; + background: rgba(39, 39, 42, 0.70); + border-color: rgba(82, 82, 91, 0.90); +} + +.jin-context-card-heading { + display: flex; + min-width: 0; + align-items: center; + gap: 8px; +} + +.jin-context-card-chevron { + width: 12px; + flex: 0 0 12px; + color: rgba(161, 161, 170, 0.62); + font-size: 10px; + line-height: 1; + transition: transform 0.16s ease; +} + +.jin-context-card-title { + overflow: hidden; + color: rgba(244, 244, 245, 0.90); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.09em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.jin-context-card-xml .jin-context-card-title { + color: rgba(186, 230, 253, 0.90); +} + +.jin-context-card-user .jin-context-card-title { + color: rgba(224, 242, 254, 0.96); +} + +.jin-context-card-body { + min-width: 0; + padding: 10px; +} + +.jin-context-card.is-collapsed .jin-context-card-header { + border-bottom-color: transparent; +} + +.jin-context-card.is-collapsed .jin-context-card-body { + display: none; +} + +.jin-context-card.is-collapsed .jin-context-card-chevron { + transform: rotate(-90deg); +} + +.jin-context-kv-list { + display: grid; + gap: 0; +} + +.jin-context-kv-row { + display: grid; + grid-template-columns: minmax(150px, 0.23fr) minmax(0, 1fr); + gap: 14px; + padding: 8px 3px; + border-bottom: 1px solid rgba(39, 39, 42, 0.74); +} + +.jin-context-kv-row:last-child { + border-bottom: 0; +} + +.jin-context-kv-key { + min-width: 0; + overflow-wrap: anywhere; + color: rgba(125, 211, 252, 0.72); + font-size: 10px; + line-height: 1.55; +} + +.jin-context-kv-value { + min-width: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: rgba(228, 228, 231, 0.84); + font-size: 11px; + line-height: 1.62; +} + +.jin-context-chat-list { + display: grid; + gap: 7px; +} + +.jin-context-chat-row { + display: grid; + grid-template-columns: 54px minmax(0, 1fr); + gap: 10px; + border-left: 2px solid rgba(113, 113, 122, 0.46); + background: rgba(24, 24, 27, 0.32); + padding: 8px 10px; +} + +.jin-context-chat-user { + border-left-color: rgba(56, 189, 248, 0.58); +} + +.jin-context-chat-jin, +.jin-context-chat-brain { + border-left-color: rgba(45, 212, 191, 0.52); +} + +.jin-context-chat-role { + color: rgba(161, 161, 170, 0.76); + font-size: 9px; + letter-spacing: 0.08em; +} + +.jin-context-chat-content { + min-width: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: rgba(228, 228, 231, 0.84); + font-size: 11px; + line-height: 1.58; +} + +.jin-context-line-list { + display: grid; + gap: 5px; +} + +.jin-context-line-item { + min-width: 0; + border-left: 1px solid rgba(82, 82, 91, 0.72); + padding: 3px 9px; + overflow-wrap: anywhere; + color: rgba(212, 212, 216, 0.82); + font-size: 11px; + line-height: 1.52; +} + +.jin-context-raw { + margin: 0; + max-width: none; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: rgba(212, 212, 216, 0.82); + font-size: 11px; + line-height: 1.62; + tab-size: 4; +} + +.jin-context-empty { + padding: 8px 2px; + color: rgba(113, 113, 122, 0.70); + font-size: 10px; + letter-spacing: 0.12em; +} + +@media (max-width: 760px) { + .jin-context-overview, + .jin-context-card-header { + align-items: flex-start; + flex-direction: column; + } + + .jin-context-overview-badges, + .jin-context-card-meta { + justify-content: flex-start; + } + + .jin-context-kv-row { + grid-template-columns: 1fr; + gap: 4px; + } +} diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index 5f22e1dc..37a9aebe 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -891,12 +891,559 @@ function formatEmbeddedSummarizerReasoning(details) { ); } + +function contextElement( + tag, + className, + text = null, +) { + const element = + document.createElement(tag); + + if (className) { + element.className = className; + } + + if (text !== null) { + element.textContent = text; + } + + return element; +} + +function parseContextTraceSnapshot(details) { + const text = + String(details || "").replace( + /\r\n?/g, + "\n" + ); + + const header = + text.match( + /^SYSTEM PROMPT(?: \(INTERNAL ACTION RULES HIDDEN\))?\n-+\n/ + ); + const userMarker = + /\nUSER PROMPT \/ CONTEXT PAYLOAD\n-+\n/; + const user = + userMarker.exec(text); + + if (!header || !user) { + return null; + } + + return { + hiddenInternalActionRules: + header[0].includes( + "INTERNAL ACTION RULES HIDDEN" + ), + systemPrompt: + text.slice( + header[0].length, + user.index + ).trim(), + userPrompt: + text.slice( + user.index + user[0].length + ).trim(), + }; +} + +function parseContextAttributes(raw) { + const attributes = []; + const pattern = + /([A-Za-z_][\w.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s]+))/g; + let match; + + while ((match = pattern.exec(String(raw || "")))) { + attributes.push( + `${match[1]}=${match[2] ?? match[3] ?? match[4] ?? ""}` + ); + } + + return attributes; +} + +function splitContextPlainText(text) { + const blocks = []; + let title = "SYSTEM RULES"; + let lines = []; + + const flush = () => { + const content = + lines.join("\n").trim(); + + if (content) { + blocks.push({ + title, + content, + attributes: [], + xml: false, + }); + } + + lines = []; + }; + + String(text || "") + .split("\n") + .forEach((line) => { + const heading = + line.trim(); + + if ( + /^[A-Z][A-Z0-9 _/&()\-]{3,}:$/.test(heading) + ) { + flush(); + title = heading.slice(0, -1); + return; + } + + lines.push(line); + }); + + flush(); + return blocks; +} + +function parseContextBlocks(text) { + const source = + String(text || "").replace( + /\r\n?/g, + "\n" + ); + const lines = source.split("\n"); + const blocks = []; + let plain = []; + + const flushPlain = () => { + const content = + plain.join("\n").trim(); + + if (content) { + blocks.push( + ...splitContextPlainText(content) + ); + } + + plain = []; + }; + + for (let i = 0; i < lines.length; i += 1) { + const open = + lines[i].match( + /^\s*<([A-Za-z][\w.-]*)(\s+[^>]*)?>\s*$/ + ); + + if (!open) { + plain.push(lines[i]); + continue; + } + + const closeText = + ``; + let close = -1; + + for (let j = i + 1; j < lines.length; j += 1) { + if (lines[j].trim() === closeText) { + close = j; + break; + } + } + + if (close < 0) { + plain.push(lines[i]); + continue; + } + + flushPlain(); + blocks.push({ + title: open[1], + attributes: + parseContextAttributes(open[2]), + content: + lines.slice(i + 1, close) + .join("\n") + .trim(), + xml: true, + }); + i = close; + } + + flushPlain(); + return blocks; +} + +function contextBadge(text, attribute = false) { + return contextElement( + "span", + attribute + ? "jin-context-badge jin-context-badge-attribute" + : "jin-context-badge", + text + ); +} + +function parseContextRows(content) { + const lines = + String(content || "") + .split("\n") + .filter((line) => line.trim()); + + const chat = lines.map((line) => { + const match = + line.match( + /^\s*<(USER|JIN|SERVICE|BRAIN)>([\s\S]*?)(?:<\/\1>)?\s*$/ + ); + + return match + ? { + kind: "chat", + key: match[1], + value: match[2].trim(), + } + : null; + }); + + if (chat.length && chat.every(Boolean)) { + return chat; + } + + const tags = lines.map((line) => { + const match = + line.match( + /^\s*<([A-Za-z][\w.-]*)>([\s\S]*?)<\/\1>\s*$/ + ); + + return match + ? { + kind: "kv", + key: match[1], + value: match[2].trim(), + } + : null; + }); + + if (tags.length && tags.every(Boolean)) { + return tags; + } + + const fields = lines + .map((line) => { + const match = + line.match( + /^\s*([^:]{1,90}):\s*(.*)$/ + ); + + return match + ? { + kind: "kv", + key: match[1].trim(), + value: match[2].trim(), + } + : null; + }) + .filter(Boolean); + + if ( + fields.length >= 2 + && fields.length / Math.max(lines.length, 1) >= 0.7 + ) { + return fields; + } + + return []; +} + +function renderContextBody(parent, content) { + const text = + String(content || "").trim(); + + if (!text) { + parent.appendChild( + contextElement( + "div", + "jin-context-empty", + "EMPTY" + ) + ); + return; + } + + const rows = + parseContextRows(text); + + if (rows.length) { + const list = + contextElement( + "div", + "jin-context-kv-list" + ); + + rows.forEach((row) => { + const item = + contextElement( + "div", + row.kind === "chat" + ? `jin-context-chat-row jin-context-chat-${row.key.toLowerCase()}` + : "jin-context-kv-row" + ); + + item.appendChild( + contextElement( + "div", + row.kind === "chat" + ? "jin-context-chat-role" + : "jin-context-kv-key", + row.key + ) + ); + item.appendChild( + contextElement( + "div", + row.kind === "chat" + ? "jin-context-chat-content" + : "jin-context-kv-value", + row.value || "" + ) + ); + list.appendChild(item); + }); + + parent.appendChild(list); + return; + } + + const lines = + text.split("\n") + .map((line) => line.trim()) + .filter(Boolean); + const simpleList = + lines.length > 1 + && lines.length <= 40 + && lines.every((line) => ( + /^\d+[.)]\s+/.test(line) + || /^[A-Za-z0-9_#.-]+(?:\s+.*)?$/.test(line) + )); + + if (simpleList) { + const list = + contextElement( + "div", + "jin-context-line-list" + ); + + lines.forEach((line) => { + list.appendChild( + contextElement( + "div", + "jin-context-line-item", + line + ) + ); + }); + + parent.appendChild(list); + return; + } + + parent.appendChild( + contextElement( + "pre", + "jin-context-raw", + text + ) + ); +} + +function appendContextCard( + parent, + block, +) { + const card = + contextElement( + "section", + `jin-context-card ${block.xml ? "jin-context-card-xml" : "jin-context-card-plain"}` + ); + const header = + contextElement( + "div", + "jin-context-card-header" + ); + const heading = + contextElement( + "div", + "jin-context-card-heading" + ); + const meta = + contextElement( + "div", + "jin-context-card-meta" + ); + const body = + contextElement( + "div", + "jin-context-card-body" + ); + + header.title = + "Double-click to collapse / expand"; + header.tabIndex = 0; + header.setAttribute("role", "button"); + header.setAttribute("aria-expanded", "true"); + + heading.appendChild( + contextElement( + "span", + "jin-context-card-chevron", + "▾" + ) + ); + heading.appendChild( + contextElement( + "div", + "jin-context-card-title", + block.title + ) + ); + + block.attributes.forEach((attribute) => { + meta.appendChild( + contextBadge(attribute, true) + ); + }); + meta.appendChild( + contextBadge( + `${String(block.content || "").split("\n").filter((line) => line.trim()).length} lines` + ) + ); + + const toggle = () => { + const collapsed = + card.classList.toggle( + "is-collapsed" + ); + + header.setAttribute( + "aria-expanded", + collapsed ? "false" : "true" + ); + }; + + header.addEventListener( + "dblclick", + toggle + ); + header.addEventListener( + "keydown", + function (event) { + if ( + event.key !== "Enter" + && event.key !== " " + ) { + return; + } + + event.preventDefault(); + toggle(); + } + ); + + header.appendChild(heading); + header.appendChild(meta); + card.appendChild(header); + card.appendChild(body); + renderContextBody(body, block.content); + parent.appendChild(card); +} + +function renderContextSnapshotTrace(snapshot) { + const blocks = + parseContextBlocks( + snapshot.systemPrompt + ); + const overview = + contextElement( + "div", + "jin-context-overview" + ); + const badges = + contextElement( + "div", + "jin-context-overview-badges" + ); + + overview.appendChild( + contextElement( + "div", + "jin-context-overview-title", + "CONTEXT WINDOW" + ) + ); + badges.appendChild( + contextBadge(`${blocks.length} blocks`) + ); + badges.appendChild( + contextBadge( + `${snapshot.systemPrompt.length.toLocaleString()} system chars` + ) + ); + if (snapshot.hiddenInternalActionRules) { + badges.appendChild( + contextBadge("internal rules hidden") + ); + } + overview.appendChild(badges); + traceModalContent.appendChild(overview); + + const stack = + contextElement( + "div", + "jin-context-stack" + ); + + blocks.forEach((block) => { + appendContextCard(stack, block); + }); + + appendContextCard( + stack, + { + title: "USER PROMPT / CONTEXT PAYLOAD", + content: + snapshot.userPrompt || "", + attributes: [], + xml: false, + } + ); + stack.lastElementChild.classList.add( + "jin-context-card-user" + ); + + traceModalContent.appendChild(stack); +} + function renderTraceDetails( details, title = "Trace", ) { traceModalContent.replaceChildren(); + const contextSnapshot = + parseContextTraceSnapshot(details); + + traceModal.classList.toggle( + "jin-context-trace-modal", + Boolean(contextSnapshot) + ); + + if (contextSnapshot) { + renderContextSnapshotTrace( + contextSnapshot + ); + + return; + } + const parsed = parseTraceJson(details); diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index 2ba8e2cf..a9051764 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -71,13 +71,6 @@ [["memory"], "#e1a449"], ]; - const PENDING_NODE_PALETTE = [ - "#e3b95b", - "#a58ae8", - "#70a9dc", - "#65c99a", - ]; - const DEFAULT_RING_COLOR = "#28cfc7"; const DEFAULT_CENTER_COLOR = "#1f4f8f"; const ACCENT_RING_COLOR = "#5be8df"; @@ -481,6 +474,56 @@ : ""; } + function getRuntimeChangeMarkerStatus(line) { + const statuses = [ + line && line.status, + line && line.key_status, + line && line.value_status, + ] + .map(value => String(value || "").trim().toLowerCase()) + .filter(Boolean); + + if (statuses.includes("new")) { + return "new"; + } + + if ( + statuses.includes("changed") + || Math.max( + Number(line && line.key_change_ratio || 0), + Number(line && line.value_change_ratio || 0) + ) > 0 + ) { + return "changed"; + } + + return ""; + } + + function getRuntimeChangeMarkerIdentity(line, index) { + const lineId = String(line && line.id || "").trim(); + + if (lineId) { + return `id:${lineId}`; + } + + const activeMemoryId = + String(line && line.active_memory_id || "").trim(); + + if (activeMemoryId) { + return `active:${activeMemoryId}`; + } + + const key = + normalizeRuntimeCitationIdentity( + line && line.key || "" + ); + + return key + ? `key:${key}` + : `line:${index}`; + } + function getSnapshotLines(snapshot) { const sourceLines = snapshot && Array.isArray(snapshot.lines) ? snapshot.lines @@ -491,22 +534,41 @@ const key = String(line && line.key || `memory_${index + 1}`).trim(); const value = String(line && line.value || "").trim(); const text = `${key}: ${value}`.trim(); + const lineId = + String(line && line.id || "").trim(); + const activeMemoryId = + String(line && line.active_memory_id || "").trim(); + const status = + String(line && line.status || "").trim().toLowerCase(); + const keyStatus = + String(line && line.key_status || "").trim().toLowerCase(); + const valueStatus = + String(line && line.value_status || "").trim().toLowerCase(); return { + id: lineId, + activeMemoryId, key, value, text, + status, + keyStatus, + valueStatus, + changeMarkerIdentity: + getRuntimeChangeMarkerIdentity(line, index), + changeMarkerStatus: + getRuntimeChangeMarkerStatus(line), avatarMemoryHoverId: buildAvatarMemoryHoverId( "runtime", - line && line.id || `line-${index}` + lineId || `line-${index}` ), referenceAliases: normalizeMemoryReferenceAliases([ key, getAvatarMemoryReferenceDisplayKey(key), - line && line.id, - line && line.active_memory_id, + lineId, + activeMemoryId, ...collectMemoryMetadataReferenceAliases(value), ]), length: Array.from(text).length, @@ -519,6 +581,127 @@ .filter(line => line.text); } + function collectSnapshotChangeMarkers(snapshot) { + const markers = new Map(); + + getSnapshotLines(snapshot).forEach((line) => { + if (!line.changeMarkerStatus) { + return; + } + + markers.set( + line.changeMarkerIdentity, + { + status: line.changeMarkerStatus, + ratio: + line.changeMarkerStatus === "new" + ? 1 + : clamp(line.changeRatio, 0, 1), + } + ); + }); + + return markers; + } + + function snapshotHasRuntimeChange(snapshot) { + const totalDiff = + Number(snapshot && snapshot.total_diff); + + if (Number.isFinite(totalDiff) && totalDiff > 0) { + return true; + } + + const patch = + snapshot && snapshot.patch; + + if (!patch || typeof patch !== "object") { + return false; + } + + return ["added", "changed", "removed"] + .some(key => ( + Array.isArray(patch[key]) + && patch[key].length > 0 + )); + } + + function getSnapshotHistory() { + const runtime = getRuntimeApi(); + + if ( + !runtime + || typeof runtime.getRuntimeMemorySnapshots !== "function" + ) { + return []; + } + + const snapshots = runtime.getRuntimeMemorySnapshots(); + + return Array.isArray(snapshots) + ? snapshots + : []; + } + + function resolveRuntimeChangeMarkers(snapshot, snapshotIndex = null) { + const directMarkers = + collectSnapshotChangeMarkers(snapshot); + + if ( + directMarkers.size + || snapshotHasRuntimeChange(snapshot) + ) { + return directMarkers; + } + + const snapshots = getSnapshotHistory(); + + if (!snapshots.length) { + return directMarkers; + } + + const requestedIndex = + snapshotIndex !== null && snapshotIndex !== undefined + ? Number(snapshotIndex) + : Number(snapshot && snapshot.index); + let resolvedIndex = + Number.isInteger(requestedIndex) + ? requestedIndex + : snapshots.findIndex(candidate => candidate === snapshot); + + if (resolvedIndex < 0) { + const runtimeMemoryId = + String(snapshot && snapshot.runtime_memory_id || "").trim(); + + if (runtimeMemoryId) { + resolvedIndex = + snapshots.findIndex(candidate => ( + String( + candidate && candidate.runtime_memory_id || "" + ).trim() === runtimeMemoryId + )); + } + } + + if (resolvedIndex < 0) { + resolvedIndex = snapshots.length - 1; + } + + for (let index = resolvedIndex - 1; index >= 0; index -= 1) { + const markers = + collectSnapshotChangeMarkers(snapshots[index]); + + if ( + markers.size + || snapshotHasRuntimeChange(snapshots[index]) + ) { + return markers; + } + } + + return directMarkers; + } + function getSnapshotDiff(snapshot, lines) { const directDiff = Number(snapshot && snapshot.total_diff); @@ -576,7 +759,7 @@ return bestMatch; } - function computeRingRecords(lines, snapshotSeed) { + function computeRingRecords(lines, snapshotSeed, changeMarkers = new Map()) { const lengths = lines.map(line => line.length); const minLength = Math.min(...lengths); const maxLength = Math.max(...lengths); @@ -603,6 +786,8 @@ isLong: line.length >= averageLength + Math.max(7, deviation * 0.62) || (line.length === maxLength && lines.length > 1), aggressive: getAggressiveMatch(line.text), + changeMarker: + changeMarkers.get(line.changeMarkerIdentity) || null, }; }); @@ -1847,53 +2032,53 @@ } } - function appendPendingNodes(group, record) { - const pendingCount = countOccurrences(record.text, [ - "pending", - "pending choice", - "pending_choices", - "pending value", - "pending_value", - ]); + function appendRuntimeChangeMarker(group, record, color) { + const marker = record && record.changeMarker; - if (!pendingCount) { + if (!marker) { return; } - const random = record.random; - const nodeCount = clamp(2 + pendingCount, 2, 6); - const startAngle = random() * 360; + const ratio = + marker.status === "new" + ? 1 + : clamp(marker.ratio, 0, 1); + const markerRadius = + 3.4 + Math.sqrt(ratio) * 2.8; + const angle = + (hashString(record.changeMarkerIdentity) % 36000) / 100; + const point = + polarPoint(record.radius, angle); + const isNew = + marker.status === "new"; + const markerGroup = createSvgElement("g", { + class: + `jin-avatar-runtime-change-marker is-${isNew ? "new" : "changed"}`, + "pointer-events": "none", + }); - for (let index = 0; index < nodeCount; index += 1) { - const angle = startAngle + index * (15 + random() * 24); - const nodeRadius = - clamp( - record.radius + (random() - 0.5) * 8, - MIN_RING_RADIUS, - INNER_DECORATION_MAX_RADIUS - 6 - ); - const point = polarPoint(nodeRadius, angle); - const color = PENDING_NODE_PALETTE[index % PENDING_NODE_PALETTE.length]; + appendTitle( + markerGroup, + `${record.key} · ${isNew ? "new runtime line" : "runtime line changed"}` + + ( + isNew + ? "" + : ` · ${Math.round(ratio * 100)}%` + ) + ); - group.appendChild(createSvgElement("circle", { - cx: point.x, - cy: point.y, - r: 4.2 + random() * 1.6, - fill: "none", - stroke: color, - "stroke-width": 0.55, - "stroke-opacity": 0.34, - })); + markerGroup.appendChild(createSvgElement("circle", { + cx: point.x, + cy: point.y, + r: markerRadius, + fill: isNew ? color : "none", + "fill-opacity": isNew ? 0.94 : null, + stroke: color, + "stroke-width": isNew ? 0.85 : 1.05, + "stroke-opacity": 0.92, + })); - group.appendChild(createSvgElement("circle", { - cx: point.x, - cy: point.y, - r: 1.15 + random() * 0.85, - fill: color, - "fill-opacity": 0.92, - filter: "url(#jin-avatar-soft-glow)", - })); - } + group.appendChild(markerGroup); } function appendOrbit(svg, record, records, overallColor, diffPercent, options = {}) { @@ -1992,32 +2177,11 @@ appendLongFieldStripes(orbitGroup, record, ringColor); } - appendPendingNodes(orbitGroup, record); - - if (random() > 0.28) { - const nodeAngle = random() * 360; - const nodePoint = polarPoint(record.radius, nodeAngle); - const nodeColor = random() > 0.76 ? AMBER_ACCENT : ringColor; - - orbitGroup.appendChild(createSvgElement("circle", { - cx: nodePoint.x, - cy: nodePoint.y, - r: 4.1 + random() * 2.3, - fill: "#071014", - stroke: nodeColor, - "stroke-width": 0.65 + random() * 0.8, - "stroke-opacity": 0.7, - })); - - orbitGroup.appendChild(createSvgElement("circle", { - cx: nodePoint.x, - cy: nodePoint.y, - r: 1.1 + random() * 1.15, - fill: nodeColor, - "fill-opacity": 0.95, - filter: "url(#jin-avatar-soft-glow)", - })); - } + appendRuntimeChangeMarker( + orbitGroup, + record, + ringColor + ); entryGroup.appendChild(orbitGroup); svg.appendChild(entryGroup); @@ -2450,7 +2614,14 @@ snapshot && snapshot.runtime_memory_id, snapshot && snapshot.index, snapshot && snapshot.total_diff, - lines.map(line => [line.key, line.value, line.status, line.key_status, line.value_status].join("␟")).join("␞"), + lines.map(line => [ + line.key, + line.value, + line.status, + line.keyStatus, + line.valueStatus, + line.changeRatio, + ].join("␟")).join("␞"), activeMemoryRecords.map(record => record.text).join("␞"), delayedMemoryRecords.map(record => [ record.id, @@ -2485,7 +2656,19 @@ options.seedNonce || avatarRefreshNonce, ].join(":"); const random = createRandom(seed || "jin-avatar"); - const records = lines.length ? computeRingRecords(lines, seed || "jin-avatar") : []; + const changeMarkers = + resolveRuntimeChangeMarkers( + snapshot, + options.snapshotIndex + ); + const records = + lines.length + ? computeRingRecords( + lines, + seed || "jin-avatar", + changeMarkers + ) + : []; const activeMemoryRecords = getActiveMemoryAvatarRecords(); const delayedMemoryRecords = getDelayedMemoryAvatarRecords(); const l4MemoryRecords = getL4MemoryAvatarRecords(); diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index 9f9f3576..eda4a7db 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -786,6 +786,23 @@ }); } + function getLongTermFactNumber(fact) { + const match = + String(fact && fact.id || "") + .trim() + .match(/^F(\d+)$/i); + + if (!match) { + return null; + } + + const number = Number(match[1]); + + return Number.isSafeInteger(number) + ? number + : null; + } + function getLongTermMemoryFactRecords() { const facts = typeof getLongTermMemoryFacts === "function" @@ -805,13 +822,29 @@ && String(fact.value || "").trim() )) .sort((left, right) => { - const updatedDifference = - String(right.updated_at || "").localeCompare( - String(left.updated_at || "") - ); + const leftNumber = + getLongTermFactNumber(left); + const rightNumber = + getLongTermFactNumber(right); + + if ( + leftNumber !== null + || rightNumber !== null + ) { + if (leftNumber === null) { + return 1; + } + + if (rightNumber === null) { + return -1; + } - if (updatedDifference) { - return updatedDifference; + const idDifference = + rightNumber - leftNumber; + + if (idDifference) { + return idDifference; + } } return String(left.key || "").localeCompare( @@ -1896,6 +1929,39 @@ keySpan.textContent = `${memoryModel.runtimeMemoryDisplay.convertKeyToName(key) || key}:`; + if (options.interactiveLongTermMemory) { + row.classList.add( + "runtime-memory-l4-row" + ); + + const factNumber = + line && Number.isSafeInteger( + line.fact_number + ) + ? line.fact_number + : null; + + if (factNumber !== null) { + const numberSpan = + document.createElement("span"); + const separatorSpan = + document.createElement("span"); + + numberSpan.className = + "runtime-memory-fact-number"; + numberSpan.textContent = + String(factNumber); + + separatorSpan.className = + "runtime-memory-fact-separator"; + separatorSpan.textContent = + "·"; + + row.appendChild(numberSpan); + row.appendChild(separatorSpan); + } + } + const valueSpan = document.createElement("span"); @@ -3842,6 +3908,8 @@ return { id, key, + fact_number: + getLongTermFactNumber(fact), value: memoryModel.appendProperties( value, formatLongTermFactMetadata(fact) diff --git a/ui/static/js/think-citations.js b/ui/static/js/think-citations.js index 5f100484..c2d2fcbc 100644 --- a/ui/static/js/think-citations.js +++ b/ui/static/js/think-citations.js @@ -7,6 +7,8 @@ "/static/js/think-rule-worker.js?v=rule-citations-6"; const THINK_RUNTIME_CITATION_HIGHLIGHT_EVENT = "jin:think-runtime-citation-highlight"; + const MEMORY_REFERENCE_HIGHLIGHT_EVENT = + "jin:memory-reference-highlight"; const buildCitationRecordIdentity = window.JinRuntime && typeof window.JinRuntime.buildCitationRecordIdentity === "function" @@ -16,6 +18,9 @@ let thinkRuleCitationWorker = null; let thinkRuleCitationRegistryPromise = null; let nextThinkRuntimeCitationIndex = 0; + let latestThinkCitationTarget = null; + let hoveredThinkCitationTarget = null; + let latestPersistentMemoryReferenceText = ""; const activeThinkRuleCitationJobs = new Map(); function isThinkCitationDebugEnabled() { @@ -802,31 +807,227 @@ ); } - function resetThinkCitationHighlightTurn() { + + function hasThinkRuleHighlights(thinkContent) { + + return Boolean( + thinkContent + && thinkContent.__jinHasRuleHighlights + ); + + } + + function getActiveThinkCitationTarget() { + + return ( + hoveredThinkCitationTarget + || latestThinkCitationTarget + ); + + } + + function setThinkCitationElementActive( + thinkContent, + active + ) { + + if (!thinkContent) { + return; + } + + const nextActive = Boolean( + active + && hasThinkRuleHighlights( + thinkContent + ) + ); + + thinkContent.classList.toggle( + "has-rule-highlights", + nextActive + ); + + const runtimeActive = Boolean( + nextActive + && shouldRevealThinkRuntimeCitations( + thinkContent + ) + ); + + if ( + thinkContent.__jinRuntimeCitationHighlightActive + === runtimeActive + ) { + return; + } + + thinkContent.__jinRuntimeCitationHighlightActive = + runtimeActive; + + dispatchThinkRuntimeCitationHighlight( + thinkContent, + runtimeActive + ); + + } + + function syncAllThinkCitationHighlights() { + + const activeTarget = + getActiveThinkCitationTarget(); document .querySelectorAll( - ".jin-think-content.has-rule-highlights" + ".jin-think-content" ) .forEach((thinkContent) => { - thinkContent.classList.remove( - "has-rule-highlights" + setThinkCitationElementActive( + thinkContent, + thinkContent === activeTarget + ); + }); + + } + + function dispatchPersistentMemoryReferenceOverride(text) { + + window.dispatchEvent( + new CustomEvent( + MEMORY_REFERENCE_HIGHLIGHT_EVENT, + { + detail: { + source: "persistent", + text: String(text || ""), + active: Boolean( + String(text || "") + ), + origin: "think-citation-hover", + }, + } + ) + ); + + } + + function restoreLatestPersistentMemoryReference() { + + dispatchPersistentMemoryReferenceOverride( + latestPersistentMemoryReferenceText + ); + + } + + function activateHoveredThinkCitation(thinkContent) { + + if ( + !thinkContent + || thinkContent === latestThinkCitationTarget + ) { + return; + } + + hoveredThinkCitationTarget = + thinkContent; + + dispatchPersistentMemoryReferenceOverride( + thinkContent.__jinThinkRawText + || thinkContent.textContent + || "" + ); + + syncAllThinkCitationHighlights(); + + } + + function deactivateHoveredThinkCitation(thinkContent) { + + if ( + !thinkContent + || hoveredThinkCitationTarget !== thinkContent + ) { + return; + } + + hoveredThinkCitationTarget = null; + + restoreLatestPersistentMemoryReference(); + syncAllThinkCitationHighlights(); + + } + + function bindThinkCitationHover(thinkContent) { + + if ( + !thinkContent + || thinkContent.__jinCitationHoverBound + ) { + return; + } + + thinkContent.__jinCitationHoverBound = true; + + thinkContent.addEventListener( + "mouseenter", + () => { + activateHoveredThinkCitation( + thinkContent + ); + } + ); + + thinkContent.addEventListener( + "mouseleave", + () => { + deactivateHoveredThinkCitation( + thinkContent ); + } + ); - dispatchThinkRuntimeCitationHighlight( + } + + function handlePersistentMemoryReferenceHighlight(event) { + + const detail = + event && event.detail || {}; + + if ( + detail.source !== "persistent" + || detail.origin === "think-citation-hover" + ) { + return; + } + + latestPersistentMemoryReferenceText = + detail.active === false + ? "" + : String(detail.text || ""); + + } + + function resetThinkCitationHighlightTurn() { + + latestThinkCitationTarget = null; + hoveredThinkCitationTarget = null; + + document + .querySelectorAll( + ".jin-think-content" + ) + .forEach((thinkContent) => { + setThinkCitationElementActive( thinkContent, false ); }); } + function syncThinkRuntimeCitationHighlight(thinkContent) { - dispatchThinkRuntimeCitationHighlight( + setThinkCitationElementActive( thinkContent, - shouldRevealThinkRuntimeCitations( - thinkContent - ) + thinkContent === getActiveThinkCitationTarget() ); } @@ -943,9 +1144,7 @@ element.replaceChildren( fragment ); - element.classList.add( - "has-rule-highlights" - ); + element.__jinHasRuleHighlights = true; element.__jinThinkTextNode = null; updateThinkContentExpandedHeight( @@ -1056,6 +1255,14 @@ thinkContent.__jinThinkRawText = text; + bindThinkCitationHover( + thinkContent + ); + + latestThinkCitationTarget = + thinkContent; + syncAllThinkCitationHighlights(); + activeThinkRuleCitationJobs.set( thinkId, { @@ -1128,6 +1335,11 @@ } + window.addEventListener( + MEMORY_REFERENCE_HIGHLIGHT_EVENT, + handlePersistentMemoryReferenceHighlight + ); + window.JinThinkCitations = { resetThinkCitationHighlightTurn, startThinkRuleCitationAnalysis, diff --git a/ui/templates/index.html b/ui/templates/index.html index 52f6280e..4a8fd567 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -509,7 +509,7 @@ - + @@ -518,7 +518,7 @@ - + diff --git a/utils/session_actions_history.py b/utils/session_actions_history.py index dfbbce94..2fae6bbb 100644 --- a/utils/session_actions_history.py +++ b/utils/session_actions_history.py @@ -1083,7 +1083,7 @@ def _build_session_action_marker_detail( "UPDATE_DELAYED_MEMORY", } -SEPARATE_SESSION_ACTION_MARKER_ITEMS = { +SEPARATE_REPEATED_SESSION_ACTION_MARKER_ITEMS = { "SAVE_ACTIVE_MEMORY", } @@ -1656,8 +1656,21 @@ def flush_grouped_parts() -> None: if not grouped_parts: return + preserve_separate = ( + len(grouped_parts) == 1 + and str( + grouped_parts[0].get( + "text", + "", + ) + or "" + ).strip().upper() + in SEPARATE_REPEATED_SESSION_ACTION_MARKER_ITEMS + ) + append_item( - grouped_parts + grouped_parts, + preserve_separate=preserve_separate, ) grouped_parts.clear() @@ -1670,13 +1683,31 @@ def flush_grouped_parts() -> None: or "" ).strip().upper() - if action_name in SEPARATE_SESSION_ACTION_MARKER_ITEMS: - flush_grouped_parts() - append_item( - [ - part, - ], - preserve_separate=True, + if ( + action_name + in SEPARATE_REPEATED_SESSION_ACTION_MARKER_ITEMS + ): + # Multiple SAVE_ACTIVE_MEMORY payloads in one model message + # must stay individually addressable, but a heterogeneous + # action set from the same message is one sequence step. + # Example: SAVE_ACTIVE_MEMORY + SAVE_SESSION should render as + # one "JIN message N executed: ..." entry, not two turns. + has_same_action = any( + str( + grouped_part.get( + "text", + "", + ) + or "" + ).strip().upper() == action_name + for grouped_part in grouped_parts + ) + + if has_same_action: + flush_grouped_parts() + + grouped_parts.append( + part ) continue diff --git a/utils/stream_validator.py b/utils/stream_validator.py index 1026f4c6..89648f43 100644 --- a/utils/stream_validator.py +++ b/utils/stream_validator.py @@ -1104,15 +1104,25 @@ def validate_sentence_sequence_loop( sequence = self.sentence_history[ -sequence_size: ] + loop_text = "\n".join( + sentence.strip() + for sentence in sequence + if sentence.strip() + ) self.last_failure_reason = ( "Repeated sentence loop detected." ) self.last_failure_preview = build_preview( - "".join(sequence) + loop_text ) + # Keep the whole detected sentence period, not only the + # final sentence that happened to trip the threshold. + # The loop preview is used both by the validator console + # and CURRENT_SEQUENCE recovery context, so reducing a + # two-sentence loop to e.g. only "No." loses the cause. self.last_failure_loop_preview = build_loop_preview( - current_sentence.strip() + loop_text ) return False From 38b1fc1ff78a73a125daa7d109f1af0bfcd8803e Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:41:58 +0300 Subject: [PATCH 032/168] chore: refine tracing and markers - show resolved memory content and numbered deleted L4 facts - disambiguate duplicate citations and scale avatar marker fill by diff - fix runtime action marker layout in context viewer --- clients/brain_client.py | 74 +++++++++++++ ...ayed_memory_pinned_glow_client_contract.py | 2 +- tests/test_l4_logger_cards_client_contract.py | 6 +- ui/static/js/logger/log-entries.js | 26 ++++- ui/static/js/logger/trace-modal.js | 98 ++++++++++++++--- ui/static/js/runtime/runtime-avatar.js | 102 ++++++++++++++---- ui/static/js/runtime/runtime-memory-view.js | 77 ++++++++----- ui/templates/index.html | 8 +- utils/actions/action_counter_utils.py | 20 +++- 9 files changed, 340 insertions(+), 73 deletions(-) diff --git a/clients/brain_client.py b/clients/brain_client.py index 57a53827..a1d45728 100644 --- a/clients/brain_client.py +++ b/clients/brain_client.py @@ -16,6 +16,7 @@ RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, RUNTIME_ACTION_SAVE_SESSION, + RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY, RUNTIME_ACTION_WEB_SEARCH, build_runtime_action_display_text, get_runtime_action_display_name, @@ -35,6 +36,7 @@ from utils.brain_client_utils import ( apply_runtime_action_calls, build_pending_asset_action_preview, + find_active_memory_slot_record, flush_pending_active_memory_resolve_failure_history, log_runtime_action_marker_removals, should_execute_save_delayed_memory, @@ -72,6 +74,7 @@ RuntimeActionRepetitionGuard, RuntimeActionResult, RuntimeActionStreamFilter, + extract_active_memory_resolve_slot_id, extract_runtime_actions, normalize_jin_color_payload, ) @@ -695,6 +698,7 @@ def preserve_duplicate_load_skill_marker( or [] ) action_counter = RuntimeActionCounter() + resolved_active_memory_display_payloads = {} raw_content_parts = [] raw_model_output_parts = [] pending_idle_action_calls = [] @@ -849,6 +853,58 @@ def get_applied_jin_colors() -> list[str]: return colors + def get_resolve_active_memory_display_payload( + payload, + ) -> str: + + normalized_payload = str( + payload + or "" + ).strip() + active_memory_id = extract_active_memory_resolve_slot_id( + normalized_payload + ) + + if not active_memory_id: + return normalized_payload + + cached_content = resolved_active_memory_display_payloads.get( + active_memory_id, + "", + ) + + if cached_content: + return cached_content + + record = find_active_memory_slot_record( + context, + active_memory_id, + ) + content = re.sub( + r"^\s*active_memory(?:_\d+)?\s*:\s*", + "", + str(record or ""), + flags=re.IGNORECASE, + ) + content = re.sub( + r"\s*\[[^\]]+\]\s*", + " ", + content, + ) + content = re.sub( + r"\s+", + " ", + content, + ).strip() + + if content: + resolved_active_memory_display_payloads[ + active_memory_id + ] = content + return content + + return normalized_payload + def get_action_counter_display_payloads() -> dict: display_payloads = {} @@ -859,6 +915,24 @@ def get_action_counter_display_payloads() -> dict: RUNTIME_ACTION_JIN_COLOR ] = applied_colors + for resolve_entry in action_counter.entries(): + if ( + resolve_entry is None + or resolve_entry.name != RUNTIME_ACTION_RESOLVE_ACTIVE_MEMORY + or not resolve_entry.payloads + ): + continue + + display_payloads[( + resolve_entry.name, + resolve_entry.identity, + )] = [ + get_resolve_active_memory_display_payload( + payload + ) + for payload in resolve_entry.payloads + ] + delayed_memory_display_actions = ( RUNTIME_ACTION_LOAD_DELAYED_MEMORY, RUNTIME_ACTION_UNLOAD_DELAYED_MEMORY, diff --git a/tests/test_delayed_memory_pinned_glow_client_contract.py b/tests/test_delayed_memory_pinned_glow_client_contract.py index d2262fd5..ca8f6902 100644 --- a/tests/test_delayed_memory_pinned_glow_client_contract.py +++ b/tests/test_delayed_memory_pinned_glow_client_contract.py @@ -50,7 +50,7 @@ def test_cache_versions_are_bumped(self): self.assertIn("/static/css/runtime-memory.css?v=delayed-context-loaded-1", source) self.assertIn("/static/js/runtime/runtime.js?v=delayed-context-loaded-1", source) self.assertIn("/static/js/chat-attachments.js?v=chat-attachments-4", source) - self.assertIn("/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1", source) + self.assertIn("/static/js/logger/trace-modal.js?v=context-marker-layout-1", source) self.assertIn("/static/js/logger/session-actions.js?v=logger-session-actions-6", source) diff --git a/tests/test_l4_logger_cards_client_contract.py b/tests/test_l4_logger_cards_client_contract.py index 9698f7c0..abe8d2df 100644 --- a/tests/test_l4_logger_cards_client_contract.py +++ b/tests/test_l4_logger_cards_client_contract.py @@ -22,6 +22,8 @@ def test_deleted_fact_card_exposes_full_payload_and_restore(self): self.assertIn('api.requestFactRestore(', source) self.assertIn('window.handleL4LoggerMemoryRestoreResult', source) self.assertIn('window.handleL4MemoryRestoreResult', source) + self.assertIn('resolveDeletedL4FactNumber(fact)', source) + self.assertIn('`${factNumber} · ${factTitle}`', source) def test_deleted_delayed_memory_card_exposes_payload_and_restore(self): source = LOG_ENTRIES_JS.read_text(encoding="utf-8") @@ -89,8 +91,8 @@ def test_cache_versions_are_bumped(self): self.assertIn('/static/css/runtime-memory.css?v=delayed-context-loaded-1', source) self.assertIn('/static/js/runtime/runtime-l4-memory.js?v=delayed-loaded-facts-1', source) self.assertIn('/static/js/logger/logger.js?v=l4-trace-reason-2', source) - self.assertIn('/static/js/logger/trace-modal.js?v=l4-truncate-diagnostics-1', source) - self.assertIn('/static/js/logger/log-entries.js?v=delayed-delete-restore-1', source) + self.assertIn('/static/js/logger/trace-modal.js?v=context-marker-layout-1', source) + self.assertIn('/static/js/logger/log-entries.js?v=l4-deleted-fact-number-1', source) self.assertIn('/static/js/socket/event-handlers.js?v=l4-restore-2', source) diff --git a/ui/static/js/logger/log-entries.js b/ui/static/js/logger/log-entries.js index 6b5e14fb..2dad0602 100644 --- a/ui/static/js/logger/log-entries.js +++ b/ui/static/js/logger/log-entries.js @@ -1497,6 +1497,23 @@ function resolveDeletedL4Fact( return null; } +function resolveDeletedL4FactNumber(fact) { + const match = + String(fact && fact.id || "") + .trim() + .match(/^F(\d+)$/i); + + if (!match) { + return null; + } + + const number = Number(match[1]); + + return Number.isSafeInteger(number) + ? number + : null; +} + function handleL4DeletedFactLog( tag, details, @@ -1531,9 +1548,16 @@ function handleL4DeletedFactLog( key.className = "block mt-2 text-zinc-200 font-semibold"; - key.textContent = + const factNumber = + resolveDeletedL4FactNumber(fact); + const factTitle = String(fact.key || fact.id || "L4 fact"); + key.textContent = + factNumber !== null + ? `${factNumber} · ${factTitle}` + : factTitle; + const value = document.createElement("span"); diff --git a/ui/static/js/logger/trace-modal.js b/ui/static/js/logger/trace-modal.js index 37a9aebe..63ef1e30 100644 --- a/ui/static/js/logger/trace-modal.js +++ b/ui/static/js/logger/trace-modal.js @@ -963,6 +963,28 @@ function parseContextAttributes(raw) { return attributes; } +function parseContextRuntimeActionMarkerTitle( + line, + nextLine, +) { + if ( + !/^Follow-up:\s*(?:true|false)\s*$/i.test( + String(nextLine || "").trim() + ) + ) { + return ""; + } + + const marker = + String(line || "").trim(); + const match = + marker.match( + /^<([A-Z][A-Z0-9_]*)(?::[^>\n]*)?>(?:<\/\1>)?$/ + ); + + return match ? match[1] : ""; +} + function splitContextPlainText(text) { const blocks = []; let title = "SYSTEM RULES"; @@ -984,22 +1006,52 @@ function splitContextPlainText(text) { lines = []; }; - String(text || "") - .split("\n") - .forEach((line) => { - const heading = - line.trim(); + const sourceLines = + String(text || "").split("\n"); - if ( - /^[A-Z][A-Z0-9 _/&()\-]{3,}:$/.test(heading) - ) { - flush(); - title = heading.slice(0, -1); - return; - } + for (let i = 0; i < sourceLines.length; i += 1) { + const line = sourceLines[i]; + const heading = line.trim(); + const markerTitle = + parseContextRuntimeActionMarkerTitle( + line, + sourceLines[i + 1] + ); + if (markerTitle) { + flush(); + title = markerTitle; lines.push(line); - }); + + for (i += 1; i < sourceLines.length; i += 1) { + const actionLine = sourceLines[i]; + + if (!actionLine.trim()) { + break; + } + + if (actionLine.trim() === `${markerTitle}:`) { + continue; + } + + lines.push(actionLine); + } + + flush(); + title = "SYSTEM RULES"; + continue; + } + + if ( + /^[A-Z][A-Z0-9 _/&()\-]{3,}:$/.test(heading) + ) { + flush(); + title = heading.slice(0, -1); + continue; + } + + lines.push(line); + } flush(); return blocks; @@ -1029,6 +1081,26 @@ function parseContextBlocks(text) { }; for (let i = 0; i < lines.length; i += 1) { + const markerTitle = + parseContextRuntimeActionMarkerTitle( + lines[i], + lines[i + 1] + ); + + if (markerTitle) { + plain.push(lines[i]); + + for (i += 1; i < lines.length; i += 1) { + plain.push(lines[i]); + + if (!lines[i].trim()) { + break; + } + } + + continue; + } + const open = lines[i].match( /^\s*<([A-Za-z][\w.-]*)(\s+[^>]*)?>\s*$/ diff --git a/ui/static/js/runtime/runtime-avatar.js b/ui/static/js/runtime/runtime-avatar.js index a9051764..8a7e8d6b 100644 --- a/ui/static/js/runtime/runtime-avatar.js +++ b/ui/static/js/runtime/runtime-avatar.js @@ -518,9 +518,17 @@ normalizeRuntimeCitationIdentity( line && line.key || "" ); + const value = + normalizeRuntimeCitationIdentity( + line && line.value || "" + ); - return key - ? `key:${key}` + // Runtime memory may legitimately contain repeated keys (for example, + // several `jin_fact` lines). A key-only identity collapses those lines + // into one marker, so every sibling inherits the same change ratio. + // Keep the concrete line text and source position in the fallback. + return (key || value) + ? `line:${index}:${key}␟${value}` : `line:${index}`; } @@ -2043,14 +2051,25 @@ marker.status === "new" ? 1 : clamp(marker.ratio, 0, 1); - const markerRadius = - 3.4 + Math.sqrt(ratio) * 2.8; + const isNew = + marker.status === "new"; + const markerRadius = 6.2; + const markerStrokeWidth = isNew ? 0 : 1.05; + const markerFillGap = 0.7; + const markerMaxInnerRadius = Math.max( + 0, + markerRadius - markerStrokeWidth * 0.5 - markerFillGap + ); + const markerInnerRadius = isNew + ? markerRadius + : Math.max( + 0, + markerMaxInnerRadius * Math.sqrt(ratio) + ); const angle = (hashString(record.changeMarkerIdentity) % 36000) / 100; const point = polarPoint(record.radius, angle); - const isNew = - marker.status === "new"; const markerGroup = createSvgElement("g", { class: `jin-avatar-runtime-change-marker is-${isNew ? "new" : "changed"}`, @@ -2067,16 +2086,30 @@ ) ); - markerGroup.appendChild(createSvgElement("circle", { - cx: point.x, - cy: point.y, - r: markerRadius, - fill: isNew ? color : "none", - "fill-opacity": isNew ? 0.94 : null, - stroke: color, - "stroke-width": isNew ? 0.85 : 1.05, - "stroke-opacity": 0.92, - })); + if (!isNew) { + markerGroup.appendChild(createSvgElement("circle", { + class: "jin-avatar-runtime-change-marker-ring", + cx: point.x, + cy: point.y, + r: markerRadius, + fill: "none", + stroke: color, + "stroke-width": markerStrokeWidth, + "stroke-opacity": 0.92, + })); + } + + if (markerInnerRadius > 0.01) { + markerGroup.appendChild(createSvgElement("circle", { + class: "jin-avatar-runtime-change-marker-fill", + cx: point.x, + cy: point.y, + r: markerInnerRadius.toFixed(2), + fill: color, + "fill-opacity": 0.94, + stroke: "none", + })); + } group.appendChild(markerGroup); } @@ -2492,11 +2525,30 @@ const activeIdentities = getActiveThinkRuntimeCitationIdentitySets(); - Array.from( + const citationNodes = Array.from( svg.querySelectorAll( ".jin-avatar-orbit[data-runtime-line-key], .jin-avatar-counter-orbit[data-runtime-line-key], .jin-avatar-memory-dash" ) - ).forEach((orbitGroup) => { + ); + const lineKeyUsage = new Map(); + + citationNodes.forEach((node) => { + const lineKey = + normalizeRuntimeCitationIdentity( + node.dataset.runtimeLineKey + ); + + if (!lineKey) { + return; + } + + lineKeyUsage.set( + lineKey, + Number(lineKeyUsage.get(lineKey) || 0) + 1 + ); + }); + + citationNodes.forEach((orbitGroup) => { const lineIdentity = normalizeRuntimeCitationIdentity( orbitGroup.dataset.runtimeLineIdentity @@ -2509,13 +2561,19 @@ normalizeRuntimeCitationIdentity( orbitGroup.dataset.runtimeLineText ); + const exactTextMatch = Boolean( + lineText + && activeIdentities.lineTexts.has(lineText) + ); + const uniqueKeyMatch = Boolean( + lineKey + && Number(lineKeyUsage.get(lineKey) || 0) === 1 + && activeIdentities.lineKeys.has(lineKey) + ); const cited = lineIdentity ? activeIdentities.lineIdentities.has(lineIdentity) - : ( - (lineKey && activeIdentities.lineKeys.has(lineKey)) - || (lineText && activeIdentities.lineTexts.has(lineText)) - ); + : (exactTextMatch || uniqueKeyMatch); orbitGroup.classList.toggle( "is-runtime-cited", diff --git a/ui/static/js/runtime/runtime-memory-view.js b/ui/static/js/runtime/runtime-memory-view.js index eda4a7db..b87c5375 100644 --- a/ui/static/js/runtime/runtime-memory-view.js +++ b/ui/static/js/runtime/runtime-memory-view.js @@ -490,36 +490,61 @@ const activeIdentities = getActiveThinkMemoryCitationIdentitySets(); - runtimeMemoryText - .querySelectorAll( + const citationRows = Array.from( + runtimeMemoryText.querySelectorAll( "[data-runtime-memory-line-key]" ) - .forEach((row) => { - const lineIdentity = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineIdentity - ); - const lineKey = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineKey - ); - const lineText = - normalizeRuntimeCitationIdentity( - row.dataset.runtimeMemoryLineText - ); - const matched = - lineIdentity - ? activeIdentities.lineIdentities.has(lineIdentity) - : ( - (lineKey && activeIdentities.lineKeys.has(lineKey)) - || (lineText && activeIdentities.lineTexts.has(lineText)) - ); + ); + const lineKeyUsage = new Map(); - row.classList.toggle( - "runtime-memory-citation-hit", - Boolean(matched) + citationRows.forEach((row) => { + const lineKey = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineKey ); - }); + + if (!lineKey) { + return; + } + + lineKeyUsage.set( + lineKey, + Number(lineKeyUsage.get(lineKey) || 0) + 1 + ); + }); + + citationRows.forEach((row) => { + const lineIdentity = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineIdentity + ); + const lineKey = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineKey + ); + const lineText = + normalizeRuntimeCitationIdentity( + row.dataset.runtimeMemoryLineText + ); + const exactTextMatch = Boolean( + lineText + && activeIdentities.lineTexts.has(lineText) + ); + const uniqueKeyMatch = Boolean( + lineKey + && Number(lineKeyUsage.get(lineKey) || 0) === 1 + && activeIdentities.lineKeys.has(lineKey) + ); + const matched = + lineIdentity + ? activeIdentities.lineIdentities.has(lineIdentity) + : (exactTextMatch || uniqueKeyMatch); + + row.classList.toggle( + "runtime-memory-citation-hit", + Boolean(matched) + ); + }); sortHighlightedMemoryRows(); } diff --git a/ui/templates/index.html b/ui/templates/index.html index 4a8fd567..45cf176f 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -507,17 +507,17 @@ - + - + - + - + diff --git a/utils/actions/action_counter_utils.py b/utils/actions/action_counter_utils.py index 46c1af77..dcc3d744 100644 --- a/utils/actions/action_counter_utils.py +++ b/utils/actions/action_counter_utils.py @@ -197,8 +197,14 @@ def marker_actions( continue payloads = resolved_display_payloads.get( - entry.name, - entry.payloads, + ( + entry.name, + entry.identity, + ), + resolved_display_payloads.get( + entry.name, + entry.payloads, + ), ) normalized_payloads = normalize_runtime_action_counter_payloads( @@ -363,8 +369,14 @@ async def emit_runtime_action_counter_updates( continue payloads = resolved_display_payloads.get( - entry.name, - entry.payloads, + ( + entry.name, + entry.identity, + ), + resolved_display_payloads.get( + entry.name, + entry.payloads, + ), ) normalized_payloads = normalize_runtime_action_counter_payloads( From 02f82a49134a4a865a72c6e3ab4efa27d4a5c4a1 Mon Sep 17 00:00:00 2001 From: makeitdouble <22776772+makeitdouble@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:28:55 +0300 Subject: [PATCH 033/168] chore: pending confirmations - retry confirmed runtime actions once after lost connection - preserve original context for reconnect replay - fail stale confirmation bubbles when retry cannot complete - apply recovery generically across action guards --- agent/nodes/brain.py | 54 +++- runtime/action_guard.py | 91 +++++++ runtime/runtime_context.py | 8 + runtime/stream.py | 335 +++++++++++++++++++------ tests/test_runtime_stream_tokens.py | 92 +++++++ tests/test_websocket_pending_usage.py | 191 ++++++++++++++ ui/static/js/chat-runtime-actions.js | 16 ++ ui/static/js/socket/runtime-actions.js | 8 + ui/templates/index.html | 10 +- websocket/__init__.py | 24 ++ websocket/messages.py | 291 ++++++++++++++++++++- 11 files changed, 1026 insertions(+), 94 deletions(-) diff --git a/agent/nodes/brain.py b/agent/nodes/brain.py index 51d1908f..81a7d3b6 100644 --- a/agent/nodes/brain.py +++ b/agent/nodes/brain.py @@ -1435,6 +1435,22 @@ async def run( if not isinstance(idle_followup, dict): idle_followup = {} + action_guard_retry = getattr( + context, + "runtime_action_guard_retry", + {}, + ) + retry_context_snapshot = ( + action_guard_retry.get( + "context_snapshot", + {}, + ) + if isinstance(action_guard_retry, dict) + else {} + ) + if not isinstance(retry_context_snapshot, dict): + retry_context_snapshot = {} + if idle_followup: idle_system_prompt = build_idle_followup_system_prompt( idle_followup @@ -1462,19 +1478,39 @@ async def run( brain_payload = "" sequence_user_request = sequence_origin_request else: - system_prompt = ( - build_brain_context( - context, - runtime_actions=runtime_actions, - commit_active_memory_refresh=True, + retry_system_prompt = str( + retry_context_snapshot.get( + "system_prompt", + "", ) + or "" ) - brain_payload = ( - build_brain_payload( - state.translated_input, - context=context, + retry_user_prompt = str( + retry_context_snapshot.get( + "user_prompt", + "", ) + or "" ) + + if retry_system_prompt: + system_prompt = retry_system_prompt + brain_payload = retry_user_prompt + else: + system_prompt = ( + build_brain_context( + context, + runtime_actions=runtime_actions, + commit_active_memory_refresh=True, + ) + ) + brain_payload = ( + build_brain_payload( + state.translated_input, + context=context, + ) + ) + sequence_user_request = str( getattr( context, diff --git a/runtime/action_guard.py b/runtime/action_guard.py index 5379d48a..c04f9042 100644 --- a/runtime/action_guard.py +++ b/runtime/action_guard.py @@ -65,6 +65,58 @@ def build_action_guard_confirmation_text( ) +def get_matching_action_guard_retry( + context, + action, + guard_name: str, +) -> dict[str, Any]: + + if bool( + getattr( + context, + "runtime_action_guard_retry_consumed", + False, + ) + ): + return {} + + retry = getattr( + context, + "runtime_action_guard_retry", + None, + ) + + if not isinstance(retry, dict) or not retry: + return {} + + action_name = str( + getattr(action, "name", "") + or "" + ).strip().lower() + retry_action = str( + retry.get("action", "") + or "" + ).strip().lower() + retry_guard = str( + retry.get("guard", "") + or "" + ).strip() + expected_guard = get_action_guard_name_for_runtime_action( + getattr(action, "name", "") + ) + + if ( + not action_name + or action_name != retry_action + or not expected_guard + or guard_name != expected_guard + or retry_guard != expected_guard + ): + return {} + + return retry + + def get_action_guard_display_id( context, action, @@ -161,6 +213,15 @@ async def wait_for_action_guard_confirmation( get_action_guard_triggers(guard_name) ), "timeout_ms": 0, + "retry_user_message": str( + getattr( + context, + "runtime_turn_user_message", + "", + ) + or "" + ), + "retry_attempt": 1, } if action.name == RUNTIME_ACTION_JIN_COLOR: @@ -227,6 +288,36 @@ async def confirm_runtime_action_guards( if not guard_name: continue + retry = get_matching_action_guard_retry( + context, + action, + guard_name, + ) + if retry: + retry_action_id = str( + retry.get("id", "") + or "" + ).strip() + retry_confirmation_id = str( + retry.get("confirmation_id", "") + or "" + ).strip() + + if retry_action_id: + action_display_ids[id(action)] = retry_action_id + if retry_confirmation_id: + confirmation_ids[id(action)] = retry_confirmation_id + + confirmed_guard_names.add(guard_name) + confirmed_action_ids.add(id(action)) + context.runtime_action_guard_retry_consumed = True + append_action_guard_decision_message( + context, + guard_name, + ACTION_ACCEPTED_MISSING_TRIGGER_WORDS_MESSAGE, + ) + continue + if get_action_guard_blocker_match( guard_name, user_message, diff --git a/runtime/runtime_context.py b/runtime/runtime_context.py index 5f0b93ad..d6b2f174 100644 --- a/runtime/runtime_context.py +++ b/runtime/runtime_context.py @@ -147,6 +147,14 @@ class RuntimeContext: default_factory=dict ) + runtime_action_guard_retry: dict[str, object] = field( + default_factory=dict + ) + + runtime_action_guard_retry_consumed: bool = False + + runtime_suppress_chat_content: bool = False + runtime_pending_requests_queue: object | None = None runtime_session_action_history: list[dict] = field( diff --git a/runtime/stream.py b/runtime/stream.py index cb40c12e..d623267f 100644 --- a/runtime/stream.py +++ b/runtime/stream.py @@ -138,11 +138,24 @@ def __init__( self.model_output_log_method = ( model_output_log_method ) - self.emit_to_chat = emit_to_chat - self.emit_content_to_chat = ( + suppress_chat_content = bool( + getattr( + context, + "runtime_suppress_chat_content", + False, + ) + ) + self.emit_to_chat = ( emit_to_chat + and not suppress_chat_content + ) + self.emit_content_to_chat = ( + self.emit_to_chat if emit_content_to_chat is None - else emit_content_to_chat + else ( + emit_content_to_chat + and not suppress_chat_content + ) ) self.context_snapshot = context_snapshot or {} self.runtime_actions = runtime_actions or {} @@ -222,6 +235,112 @@ def build_loaded_skill_name_set(self) -> set[str]: return names + def get_action_guard_retry( + self, + action, + guard_name: str = "", + ) -> dict: + + retry = getattr( + self.context, + "runtime_action_guard_retry", + None, + ) + + if not isinstance(retry, dict) or not retry: + return {} + + action_name = str( + getattr(action, "name", "") + or "" + ).strip().lower() + retry_action = str( + retry.get("action", "") + or "" + ).strip().lower() + + if not action_name or action_name != retry_action: + return {} + + expected_guard = get_action_guard_name_for_runtime_action( + getattr(action, "name", "") + ) + retry_guard = str( + retry.get("guard", "") + or "" + ).strip() + + if ( + not expected_guard + or retry_guard != expected_guard + or (guard_name and guard_name != expected_guard) + ): + return {} + + return retry + + def get_action_guard_retry_confirmation_id( + self, + action, + guard_name: str = "", + ) -> str: + + retry = self.get_action_guard_retry( + action, + guard_name, + ) + + return str( + retry.get("confirmation_id", "") + if retry + else "" + ).strip() + + def get_action_guard_retry_display_id( + self, + action, + ) -> str: + + retry = self.get_action_guard_retry( + action + ) + + return str( + retry.get("id", "") + if retry + else "" + ).strip() + + def accept_action_guard_retry( + self, + action, + guard_name: str, + *, + completed: bool = False, + ) -> bool: + + confirmation_id = ( + self.get_action_guard_retry_confirmation_id( + action, + guard_name, + ) + ) + + if not confirmation_id: + return False + + self.confirmed_action_guard_names.add( + guard_name + ) + self.action_guard_confirmation_ids[ + id(action) + ] = confirmation_id + + if completed: + self.context.runtime_action_guard_retry_consumed = True + + return True + def should_preserve_action_marker( self, raw_marker: str, @@ -1205,6 +1324,12 @@ def get_runtime_action_display_id( action, ) -> str: + retry_display_id = self.get_action_guard_retry_display_id( + action + ) + if retry_display_id: + return retry_display_id + if action.name == RUNTIME_ACTION_JIN_COLOR: if not self.jin_color_action_id: sequence = int( @@ -1386,6 +1511,24 @@ async def confirm_unmatched_action_guards( if not guard_name: continue + retry_was_confirmed = ( + guard_name in self.confirmed_action_guard_names + ) + if self.accept_action_guard_retry( + action, + guard_name, + completed=True, + ): + if not retry_was_confirmed: + self.append_action_guard_missing_trigger_message( + guard_name, + ACTION_ACCEPTED_MISSING_TRIGGER_WORDS_MESSAGE, + ) + confirmed_action_ids.add( + id(action) + ) + continue + if guard_name in self.rejected_action_guard_names: rejected_action_ids.add( id(action) @@ -1460,6 +1603,16 @@ async def confirm_started_runtime_action_guards( if not guard_name: continue + if self.accept_action_guard_retry( + action, + guard_name, + ): + self.append_action_guard_missing_trigger_message( + guard_name, + ACTION_ACCEPTED_MISSING_TRIGGER_WORDS_MESSAGE, + ) + continue + if ( guard_name in self.confirmed_action_guard_names or guard_name in self.rejected_action_guard_names @@ -1707,6 +1860,15 @@ async def wait_for_action_guard_confirmation( ), "missing_triggers": triggers, "timeout_ms": 0, + "retry_user_message": str( + getattr( + self.context, + "runtime_turn_user_message", + "", + ) + or "" + ), + "retry_attempt": 1, } if action.name == RUNTIME_ACTION_JIN_COLOR: @@ -1823,22 +1985,28 @@ async def emit_started_runtime_actions( pending_ids ) - action_id = build_runtime_action_id( - RUNTIME_ACTION_ASSET_ACTION, - len( - getattr( - self.context, - "runtime_asset_results", - [], + action_id = ( + self.get_action_guard_retry_display_id( + action + ) + or build_runtime_action_id( + RUNTIME_ACTION_ASSET_ACTION, + len( + getattr( + self.context, + "runtime_asset_results", + [], + ) + or [] ) - or [] + + len(pending_ids) + + 1, ) - + len(pending_ids) - + 1, - ) - pending_ids.append( - action_id ) + if action_id not in pending_ids: + pending_ids.append( + action_id + ) payload = { "type": "runtime_action", @@ -1850,22 +2018,26 @@ async def emit_started_runtime_actions( "close_tag": has_close_tag, } elif action.name == RUNTIME_ACTION_SAVE_ACTIVE_MEMORY: - current_sequence = int( - getattr( - self.context, - "runtime_active_memory_action_sequence", - 0, - ) - or 0 - ) - next_sequence = current_sequence + 1 - self.context.runtime_active_memory_action_sequence = ( - next_sequence - ) - action_id = build_runtime_action_id( - RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, - next_sequence, + action_id = self.get_action_guard_retry_display_id( + action ) + if not action_id: + current_sequence = int( + getattr( + self.context, + "runtime_active_memory_action_sequence", + 0, + ) + or 0 + ) + next_sequence = current_sequence + 1 + self.context.runtime_active_memory_action_sequence = ( + next_sequence + ) + action_id = build_runtime_action_id( + RUNTIME_ACTION_SAVE_ACTIVE_MEMORY, + next_sequence, + ) self.started_active_memory_action_ids.append( action_id ) @@ -1899,50 +2071,55 @@ async def emit_started_runtime_actions( pending_ids ) - current_sequence = max( - int( - getattr( - self.context, - "runtime_delayed_memory_action_sequence", - 0, - ) - or 0 - ), - len( - getattr( - self.context, - "delayed_memory_reports", - {}, - ) - or {} - ), - len([ - event - for event in getattr( - self.context, - "runtime_action_events", - [], - ) - if isinstance( - event, - dict, - ) - and event.get( - "name" - ) == "save_delayed_memory_content" - ]), - ) - next_sequence = current_sequence + 1 - self.context.runtime_delayed_memory_action_sequence = ( - next_sequence - ) - action_id = build_runtime_action_id( - RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, - next_sequence, - ) - pending_ids.append( - action_id + action_id = self.get_action_guard_retry_display_id( + action ) + if not action_id: + current_sequence = max( + int( + getattr( + self.context, + "runtime_delayed_memory_action_sequence", + 0, + ) + or 0 + ), + len( + getattr( + self.context, + "delayed_memory_reports", + {}, + ) + or {} + ), + len([ + event + for event in getattr( + self.context, + "runtime_action_events", + [], + ) + if isinstance( + event, + dict, + ) + and event.get( + "name" + ) == "save_delayed_memory_content" + ]), + ) + next_sequence = current_sequence + 1 + self.context.runtime_delayed_memory_action_sequence = ( + next_sequence + ) + action_id = build_runtime_action_id( + RUNTIME_ACTION_SAVE_DELAYED_MEMORY_CONTENT, + next_sequence, + ) + if action_id not in pending_ids: + pending_ids.append( + action_id + ) self.started_delayed_memory_action_ids.append( action_id ) @@ -1995,6 +2172,16 @@ async def emit_started_runtime_actions( if action.payload and not has_close_tag: payload["payload"] = action.payload + retry_confirmation_id = ( + self.get_action_guard_retry_confirmation_id( + action + ) + ) + if retry_confirmation_id: + payload["confirmation_id"] = ( + retry_confirmation_id + ) + if delayed_memory_report_id: payload["delayed_memory_report_id"] = ( delayed_memory_report_id diff --git a/tests/test_runtime_stream_tokens.py b/tests/test_runtime_stream_tokens.py index b38bdd47..f4d5a287 100644 --- a/tests/test_runtime_stream_tokens.py +++ b/tests/test_runtime_stream_tokens.py @@ -1965,6 +1965,98 @@ async def emit( ) ) + async def test_reconnected_delayed_memory_confirmation_replays_once_without_new_guard(self): + + async def delayed_memory_generator(): + + yield { + "type": "content", + "content": ( + "\n" + "title: Reconnected report\n" + "summary: Runtime summary.\n" + "tags: runtime, summary\n" + "body: Full report.\n" + "\n" + ), + } + + context = SimpleNamespace( + websocket=FakeWebSocket(), + logger=FakeLogger(), + emitter=FakeEmitter(), + runtime_action_events=[], + runtime_usage_events=[], + runtime_asset_results=[], + runtime_delayed_memory_results=[], + runtime_session_action_history=[], + runtime_action_guard_confirmations={}, + runtime_action_guard_retry={ + "action": "save_delayed_memory_content", + "guard": "save_delayed_memory", + "confirmation_id": "stale-confirmation", + "id": "save_delayed_memory_content_9", + "attempt": 1, + }, + runtime_action_guard_retry_consumed=False, + runtime_suppress_chat_content=True, + delayed_memory_reports={}, + active_memory_records=[], + runtime_turn_user_message="создай отчот", + runtime_current_turn_id="retry_000001", + runtime_turn_started_at=0, + session_id="session-1", + timestamp="2026-07-13T17:00:00", + ) + + stream = RuntimeStream( + context=context, + runtime_id=settings.SERVICE_MODEL_UID, + role="service", + context_window=settings.SERVICE_CONTEXT_WINDOW, + log_method=context.logger.log_service, + runtime_actions={ + "CAN_SAVE_DELAYED_MEMORY": True, + }, + ) + + await stream.run( + delayed_memory_generator() + ) + + confirmation_events = [ + event + for event in context.emitter.events + if event.get("type") == "runtime_action_guard_confirmation" + ] + runtime_events = [ + event + for event in context.emitter.events + if event.get("type") == "runtime_action" + ] + lifecycle_events = [ + event + for event in runtime_events + if not event.get("counter_only") + ] + + self.assertEqual(confirmation_events, []) + self.assertEqual(len(context.delayed_memory_reports), 1) + self.assertTrue(context.runtime_action_guard_retry_consumed) + self.assertEqual( + {event.get("id") for event in lifecycle_events}, + {"save_delayed_memory_content_9"}, + ) + self.assertEqual( + { + event.get("confirmation_id") + for event in lifecycle_events + if event.get("confirmation_id") + }, + {"stale-confirmation"}, + ) + self.assertEqual(context.websocket.messages, []) + async def test_jin_color_applies_without_trigger_confirmation(self): state = { diff --git a/tests/test_websocket_pending_usage.py b/tests/test_websocket_pending_usage.py index ca74f15d..6fe28d71 100644 --- a/tests/test_websocket_pending_usage.py +++ b/tests/test_websocket_pending_usage.py @@ -3,6 +3,7 @@ from types import SimpleNamespace from runtime.L1_memory_utils import emit_runtime_session_memory_update +from runtime.action_guard import confirm_runtime_action_guards from runtime.registry import runtime_state from config_loader import ( config, @@ -17,9 +18,11 @@ from websocket import ( PendingRequestQueue, apply_runtime_resume, + build_runtime_action_guard_retry_request, apply_session_bootstrap, arm_save_session_from_user_text, cancel_current_task, + emit_runtime_action_guard_confirmation_failure, reject_when_all_models_offline, refresh_pending_brain_usage, wait_for_runtime_memory_update, @@ -28,6 +31,7 @@ from utils.runtime_action_abort import ( mark_runtime_action_started, ) +from utils.actions.common_action_utils import RuntimeActionCall class FakeEmitter: @@ -150,6 +154,193 @@ async def send_json( class WebSocketPendingUsageTests(unittest.IsolatedAsyncioTestCase): + def test_stale_guard_confirmation_builds_single_retry_with_original_context(self): + + message = { + "decision": "continue", + "action": "save_delayed_memory_content", + "guard": "save_delayed_memory", + "confirmation_id": "turn_7:save_delayed_memory_content:abc", + "id": "save_delayed_memory_content_7", + "retry_attempt": 1, + "retry_user_message": "создай отчот", + "retry_context_snapshot": { + "system_prompt": "original system", + "user_prompt": "original model payload", + }, + } + + retry_request = build_runtime_action_guard_retry_request( + message + ) + + self.assertEqual( + retry_request["type"], + "runtime_action_guard_retry", + ) + self.assertEqual( + retry_request["text"], + "создай отчот", + ) + self.assertEqual( + retry_request["runtime_action_guard_retry"], + { + "action": "save_delayed_memory_content", + "guard": "save_delayed_memory", + "confirmation_id": "turn_7:save_delayed_memory_content:abc", + "id": "save_delayed_memory_content_7", + "attempt": 1, + "context_snapshot": { + "system_prompt": "original system", + "user_prompt": "original model payload", + }, + }, + ) + + second_attempt = dict( + message, + retry_attempt=2, + ) + self.assertIsNone( + build_runtime_action_guard_retry_request( + second_attempt + ) + ) + self.assertIsNone( + build_runtime_action_guard_retry_request({ + **message, + "decision": "reject", + }) + ) + self.assertIsNone( + build_runtime_action_guard_retry_request({ + **message, + "guard": "save_session", + }) + ) + + async def test_stale_guard_confirmation_failure_is_terminal_for_same_bubble(self): + + context = SimpleNamespace( + emitter=FakeEmitter(), + ) + + await emit_runtime_action_guard_confirmation_failure( + context, + { + "decision": "continue", + "action": "save_delayed_memory_content", + "confirmation_id": "stale-confirmation", + "id": "save_delayed_memory_content_3", + }, + ) + + self.assertEqual( + context.emitter.events, + [{ + "type": "runtime_action", + "action": "save_delayed_memory_content", + "status": "failed", + "display_name": "SAVE_DELAYED_MEMORY_CONTENT", + "close_tag": True, + "confirmation_id": "stale-confirmation", + "error": "runtime_action_confirmation_expired", + "text": "SAVE_DELAYED_MEMORY_CONTENT: FAILED", + "detail": "The original confirmation no longer exists after reconnect.", + "id": "save_delayed_memory_content_3", + }], + ) + + async def test_action_guard_retry_bypasses_only_matching_guard_once(self): + + context = SimpleNamespace( + emitter=FakeEmitter(), + runtime_action_guard_confirmations={}, + runtime_action_guard_retry={ + "action": "save_delayed_memory_content", + "guard": "save_delayed_memory", + "confirmation_id": "stale-confirmation", + "id": "save_delayed_memory_content_4", + "attempt": 1, + }, + runtime_action_guard_retry_consumed=False, + runtime_action_failure_followup_messages=[], + ) + action = RuntimeActionCall( + name="SAVE_DELAYED_MEMORY_CONTENT", + payload="title: Replay report", + ) + + ( + confirmed_action_ids, + rejected_action_ids, + confirmation_ids, + action_display_ids, + ) = await confirm_runtime_action_guards( + context, + (action,), + user_message="создай отчот", + ) + + self.assertEqual( + confirmed_action_ids, + {id(action)}, + ) + self.assertEqual( + rejected_action_ids, + set(), + ) + self.assertEqual( + confirmation_ids[id(action)], + "stale-confirmation", + ) + self.assertEqual( + action_display_ids[id(action)], + "save_delayed_memory_content_4", + ) + self.assertTrue( + context.runtime_action_guard_retry_consumed + ) + self.assertFalse( + any( + event.get("type") == "runtime_action_guard_confirmation" + for event in context.emitter.events + ) + ) + + wrong_action = RuntimeActionCall( + name="SAVE_SESSION", + ) + context.runtime_action_guard_retry_consumed = False + context.runtime_action_guard_retry = { + **context.runtime_action_guard_retry, + "action": "save_delayed_memory_content", + "guard": "save_delayed_memory", + } + context.runtime_action_guard_confirmations = {} + + async def reject_new_confirmation(event): + await FakeEmitter.emit( + context.emitter, + event, + ) + if event.get("type") == "runtime_action_guard_confirmation": + future = context.runtime_action_guard_confirmations[ + event["confirmation_id"] + ] + future.set_result("reject") + + context.emitter.emit = reject_new_confirmation + + confirmed, rejected, _, _ = await confirm_runtime_action_guards( + context, + (wrong_action,), + user_message="сохрани сесию", + ) + + self.assertEqual(confirmed, set()) + self.assertEqual(rejected, {id(wrong_action)}) + async def test_cancel_current_task_aborts_active_action_when_task_already_done(self): context = SimpleNamespace( diff --git a/ui/static/js/chat-runtime-actions.js b/ui/static/js/chat-runtime-actions.js index 26f4a9d6..7d377dbb 100644 --- a/ui/static/js/chat-runtime-actions.js +++ b/ui/static/js/chat-runtime-actions.js @@ -1566,6 +1566,22 @@ function bindRuntimeActionGuardConfirmation( id: options.id || "", guard: confirmation.guard || "", decision, + retry_user_message: + String( + confirmation.retryUserMessage + || confirmation.retry_user_message + || "" + ), + retry_attempt: + Number( + confirmation.retryAttempt + || confirmation.retry_attempt + || 1 + ), + retry_context_snapshot: + confirmation.retryContextSnapshot + || confirmation.retry_context_snapshot + || null, }) : false; diff --git a/ui/static/js/socket/runtime-actions.js b/ui/static/js/socket/runtime-actions.js index 6a5b1695..305cebcf 100644 --- a/ui/static/js/socket/runtime-actions.js +++ b/ui/static/js/socket/runtime-actions.js @@ -77,6 +77,14 @@ function handleRuntimeActionGuardConfirmation( : [], timeoutMs: Number(data.timeout_ms || 0), + retryUserMessage: + String( + data.retry_user_message || "" + ), + retryAttempt: + Number(data.retry_attempt || 1), + retryContextSnapshot: + data.context || null, }, } ); diff --git a/ui/templates/index.html b/ui/templates/index.html index 45cf176f..32d08e69 100644 --- a/ui/templates/index.html +++ b/ui/templates/index.html @@ -78,15 +78,15 @@ class="absolute inset-0 bg-cover bg-center opacity-80 pointer-events-none">

      c8qe<+_A2+}mA+MMM5k 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

      >sy5x69dab>2S<8+oy7< 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

      3dvd9xm7NOlJ`7jP=b0&8zACtZ@ly+63VNrJ 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