@@ -249,6 +249,47 @@ def save_familiar_doc_state(fam_id: str, mtimes: dict) -> None:
249249 write_json_atomic (FAMILIAR_DOCS_CACHE_PATH , cache )
250250
251251
252+ def _resolve_merge_file_ref (fam_id : str , ref : str ) -> Path | None :
253+ raw = str (ref or "" ).strip ().replace ("\\ " , "/" )
254+ if not raw :
255+ return None
256+ candidates = [
257+ FAMILIARS_DIR / fam_id / raw ,
258+ fam_docs_dir (fam_id ) / raw ,
259+ BASE_DIR .parent / raw ,
260+ BASE_DIR / raw ,
261+ ]
262+ seen = set ()
263+ for cand in candidates :
264+ norm = str (cand .resolve ()) if cand .exists () else str (cand )
265+ if norm in seen :
266+ continue
267+ seen .add (norm )
268+ if cand .exists () and cand .is_file ():
269+ return cand
270+ return None
271+
272+
273+ def _load_merge_map (path : Path ) -> dict :
274+ try :
275+ data = json .loads (read_text (path ) or "{}" )
276+ return data if isinstance (data , dict ) else {}
277+ except Exception :
278+ return {}
279+
280+
281+ def _env_signal_enabled (signals ) -> bool :
282+ if not isinstance (signals , list ):
283+ return False
284+ for sig in signals :
285+ token = str (sig or "" ).strip ()
286+ if token .lower ().startswith ("env:" ):
287+ name = token .split (":" , 1 )[1 ].strip ()
288+ if name and os .environ .get (name ):
289+ return True
290+ return False
291+
292+
252293def load_outfits_with_avatars (fam_id : str ) -> str :
253294 """Load outfits.md and resolve all referenced avatars.md files.
254295
@@ -438,9 +479,6 @@ def build_prompt(
438479 settings = read_json (SETTINGS_PATH , {})
439480 if bool (settings .get ("disable_familiar_cache" , False )):
440481 should_inject_fam = True
441- agent = read_text (fdocs / "agent.md" ).strip () if should_inject_fam else ""
442- personality = read_text (fdocs / "personality.md" ).strip () if should_inject_fam else ""
443- coding = read_text (fdocs / "coding.md" ).strip () if should_inject_fam else ""
444482 greet = read_text (fdocs / "greet.md" ).strip () if should_inject_fam else ""
445483 profile = read_text (fdir / "profile.json" ).strip () if should_inject_fam else ""
446484 affection_global = read_text (BASE_DIR / "docs" / "agents" / "affection_system.md" ).strip () if should_inject_fam else ""
@@ -465,27 +503,51 @@ def build_prompt(
465503 current_datetime = datetime .now ().strftime ("%A, %B %d, %Y at %H:%M" )
466504
467505 parts = []
468- # Only inject heavy familiar docs when new/changed; otherwise keep a tiny stub.
506+ merge_map = _load_merge_map (fdocs / "merge.json" )
507+ merge_phase = "first_prompt" if should_inject_fam else "per_message"
508+ merge_phase_cfg = merge_map .get (merge_phase , {}) if isinstance (merge_map , dict ) else {}
469509 watched_labels = ", " .join (sorted (familiar_docs_targets (fam_id ).keys ()))
510+ injected_static = False
511+
512+ def _format_static_ref (ref : str , content : str ) -> str :
513+ low = str (ref or "" ).strip ().replace ("\\ " , "/" ).lower ()
514+ if low .endswith ("docs/coding.md" ):
515+ return "[Coding Support]\n " + content
516+ if low .endswith ("meta.json" ):
517+ return "[Familiar Identity]\n " + content
518+ if low .endswith ("docs/locations.md" ):
519+ return "[Available Locations]\n " + content
520+ if low .endswith ("profile.json" ):
521+ return "[User Profile]\n " + content
522+ if low .endswith ("docs/preferences.md" ):
523+ return "[User Preferences (Strongly Preferred)]\n " + content
524+ if low .endswith ("docs/memories.md" ):
525+ return "[Permanent Memories]\n " + content
526+ if low .endswith ("docs/agents/chronos.md" ):
527+ return "[Chronos Protocols]\n " + content
528+ if low .endswith ("docs/agents/agents.md" ):
529+ return "[Chronos Agent Guide]\n " + content
530+ return content
531+
470532 if should_inject_fam :
471- if agent :
472- parts .append (agent )
473- if personality :
474- parts .append (personality )
475- if coding :
476- parts .append ("[Coding Support]\n " + coding )
477- if meta_json :
478- parts .append ("[Familiar Identity]\n " + meta_json )
479- if locations_md :
533+ for ref in (merge_phase_cfg .get ("static_files" ) or []):
534+ path = _resolve_merge_file_ref (fam_id , str (ref ))
535+ if not path :
536+ continue
537+ content = read_text (path ).strip ()
538+ if not content :
539+ continue
540+ parts .append (_format_static_ref (str (ref ), content ))
541+ injected_static = True
542+ if locations_md and "docs/locations.md" not in [str (x ) for x in (merge_phase_cfg .get ("static_files" ) or [])]:
480543 parts .append ("[Available Locations]\n " + locations_md )
481544 if fam_note :
482545 parts .append ("[Familiar Docs Update]\n " + fam_note )
483- # Persist mtimes after a successful inject
484546 try :
485547 save_familiar_doc_state (fam_id , fam_mtimes )
486548 except Exception :
487549 pass
488- else :
550+ if not should_inject_fam :
489551 parts .append ("[Familiar Context Cached]\n No changes since last inject. Watched: " + watched_labels )
490552
491553 # Inject Current Date/Time
@@ -514,47 +576,59 @@ def build_prompt(
514576 if immersive_on and lore :
515577 parts .append ("[Immersive Lore Enabled]\n " + lore )
516578
517- # --- Chronos / External Context Injection ---
518- # Only if ADUC_EXTERNAL_CONTEXT_FILE is set (Chronos Mode)
519- ext_context_path = os .environ .get ("ADUC_EXTERNAL_CONTEXT_FILE" )
520- if ext_context_path and os .path .isfile (ext_context_path ):
521- # 1. ALWAYS inject Chronos Protocol when in Chronos Mode (not cached!)
522- chronos_proto = read_text (BASE_DIR / "docs" / "agents" / "chronos.md" ).strip ()
523- if chronos_proto :
524- parts .append ("[Chronos Protocols]\n " + chronos_proto )
525- chronos_index = read_text (BASE_DIR .parent / "docs" / "INDEX.md" ).strip ()
526- if chronos_index :
527- parts .append ("[Chronos Docs Index]\n " + chronos_index )
528- trick_protocol = read_text (BASE_DIR .parent / "docs" / "agents" / "trick.md" ).strip ()
529- if trick_protocol :
530- parts .append ("[TRICK Protocol]\n " + trick_protocol )
531-
532- # 2. Inject External System Context (The Manual) only when first/changed
533- try :
534- should_ext , mtime_val = external_context_should_inject (ext_context_path )
535- except Exception :
536- should_ext , mtime_val = True , None
537- if should_ext :
538- try :
539- sys_ctx = read_text (Path (ext_context_path )).strip ()
540- if sys_ctx :
541- parts .append ("[System Context]\n " + sys_ctx )
542- except Exception :
543- pass
544- if mtime_val is not None :
579+ # --- Chronos / External Context Injection (declarative) ---
580+ optional_cfg = (merge_map .get ("optional" ) or {}) if isinstance (merge_map , dict ) else {}
581+ chronos_mode_cfg = optional_cfg .get ("chronos_mode" ) if isinstance (optional_cfg , dict ) else {}
582+ if isinstance (chronos_mode_cfg , dict ) and _env_signal_enabled (chronos_mode_cfg .get ("enabled_by" )):
583+ cfg_ref = chronos_mode_cfg .get ("config" )
584+ cfg_path = _resolve_merge_file_ref (fam_id , str (cfg_ref )) if cfg_ref else None
585+ chronos_merge = _load_merge_map (cfg_path ) if cfg_path else {}
586+ for ref in (chronos_merge .get ("static_files" ) or []):
587+ path = _resolve_merge_file_ref (fam_id , str (ref ))
588+ if not path :
589+ continue
590+ content = read_text (path ).strip ()
591+ if not content :
592+ continue
593+ parts .append (_format_static_ref (str (ref ), content ))
594+ for block in (chronos_merge .get ("dynamic_blocks" ) or []):
595+ token = str (block or "" ).strip ()
596+ if token == "trick_runtime_query_guidance" :
597+ parts .append (
598+ "[TRICK Runtime Guidance]\n "
599+ "TRICK is a runtime UI-control protocol. Do not assume dashboard elements from memory.\n "
600+ "When you need UI capabilities, query them at runtime via the `trick` command or the TRICK registry.\n "
601+ "Use the protocol docs for command syntax, not as a full element inventory."
602+ )
603+ elif token .startswith ("system_context_from:env:" ):
604+ env_name = token .split (":" , 2 )[2 ].strip ()
605+ ext_context_path = os .environ .get (env_name )
606+ if ext_context_path and os .path .isfile (ext_context_path ):
607+ try :
608+ should_ext , mtime_val = external_context_should_inject (ext_context_path )
609+ except Exception :
610+ should_ext , mtime_val = True , None
611+ if should_ext :
612+ try :
613+ sys_ctx = read_text (Path (ext_context_path )).strip ()
614+ if sys_ctx :
615+ parts .append ("[System Context]\n " + sys_ctx )
616+ except Exception :
617+ pass
618+ if mtime_val is not None :
619+ try :
620+ save_external_context_mtime (mtime_val )
621+ except Exception :
622+ pass
623+ else :
624+ parts .append ("[System Context Cached]\n Unchanged external context at: " + str (ext_context_path ))
625+ elif token == "chronos_docs_update_note" :
545626 try :
546- save_external_context_mtime (mtime_val )
627+ doc_note = chronos_doc_change_note ()
628+ if doc_note :
629+ parts .append ("[Chronos Docs Update]\n " + doc_note )
547630 except Exception :
548631 pass
549- else :
550- parts .append ("[System Context Cached]\n Unchanged external context at: " + str (ext_context_path ))
551- # Detect Chronos doc changes (avoids re-merging full docs each turn)
552- try :
553- doc_note = chronos_doc_change_note ()
554- if doc_note :
555- parts .append ("[Chronos Docs Update]\n " + doc_note )
556- except Exception :
557- pass
558632 # --------------------------------------------
559633
560634 if affection_global :
@@ -663,8 +737,6 @@ def build_prompt(
663737 "dev_nsfw_override" : bool (settings .get ("dev_nsfw_override" , False )),
664738 }
665739 parts .append ("[NSFW Policy]\n " + json .dumps (policy , ensure_ascii = False ))
666- if profile :
667- parts .append ("[User Profile]\n " + profile )
668740
669741 # Affection Style Policy based on hearts + consent + boundaries
670742 try :
@@ -807,14 +879,6 @@ def _tier_from_hearts(h: float) -> str:
807879 if outfits_txt :
808880 parts .append ("[Available Outfits]\n " + outfits_txt )
809881
810- # Inject preferences if present
811- if preferences :
812- parts .append ("[User Preferences]\n " + preferences )
813-
814- # Inject permanent memories if present
815- if memories :
816- parts .append ("[Permanent Memories]\n " + memories )
817-
818882 merged = "\n \n " .join (parts )
819883 instruction = (
820884 "You are roleplaying this Familiar. Respond to the USER in character. "
0 commit comments