← Files BetterContextARCHIVED FILE
skills/bettercontext/scripts/runtime/memory.py
17 KB · Oct 2, 2026 · 00:36 UTC
import argparse
import sys
import json
import sqlite3
import chat_tracking
import time
from pathlib import Path
from database import (
init_db, add_fact, get_facts, delete_fact,
get_connection,
add_task, get_tasks, update_task_status,
add_log, get_logs, replace_logs, count_logs,
link_chat, resolve_chat, get_all_aliases,
send_relay_message, get_relay_inbox, get_relay_outbox,
acknowledge_relay_messages, get_relay_message
)
def format_context(chat=None, relay_limit=20, claim_relays=False):
facts = get_facts()
tasks = get_tasks(status='pending')
output = ["# BetterContext: Active Memory Bank\n"]
if chat:
messages = get_relay_inbox(chat, limit=relay_limit, claim=claim_relays)
output.append(f"## Unread Relay Messages for {chat}")
if not messages:
output.append("*No unread relay messages.*")
else:
for message in messages:
body = message['body'].replace('\n', '\n ')
output.append(
f"- [Relay #{message['id']}] From {message['sender_chat']} "
f"at {message['created_at']} ({message['state']}):\n {body}"
)
ids = ' '.join(str(message['id']) for message in messages)
output.append(
f"\nAfter processing these messages, acknowledge them with: "
f"`memory.py relay-ack {chat} {ids}`"
)
output.append("")
output.append("## Core Facts (Rules & Preferences)")
if not facts:
output.append("*No core facts stored yet.*")
else:
current_cat = None
for fact in facts:
if fact['category'] != current_cat:
current_cat = fact['category']
output.append(f"\n### {current_cat.upper()}")
output.append(f"- [{fact['id']}] {fact['fact']}")
output.append("\n## Pending Tasks")
if not tasks:
output.append("*No pending tasks.*")
else:
for task in tasks:
output.append(f"- [ ] (ID: {task['id']}) {task['description']}")
return "\n".join(output)
def _message_text(content):
chunks = []
for part in content or []:
if not isinstance(part, dict):
continue
text = part.get('text') or part.get('output_text') or part.get('input_text') or ''
if text:
chunks.append(text)
return ''.join(chunks)
def import_codex_thread(rollout_path, session_id, replace=False, include_environment_context=False):
path = Path(rollout_path)
if not path.exists():
raise FileNotFoundError(f"Codex rollout file not found: {path}")
thread_id = None
messages = []
with path.open('r', encoding='utf-8') as file:
for line_no, line in enumerate(file, 1):
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if obj.get('type') == 'session_meta':
payload = obj.get('payload') or {}
thread_id = payload.get('id') or thread_id
continue
if obj.get('type') != 'response_item':
continue
payload = obj.get('payload') or {}
if payload.get('type') != 'message':
continue
role = payload.get('role')
if role not in {'user', 'assistant'}:
continue
text = _message_text(payload.get('content'))
if not text.strip():
continue
if role == 'user' and not include_environment_context and text.lstrip().startswith('<environment_context>'):
continue
phase = payload.get('phase')
meta = f"[codex_thread={thread_id or 'unknown'} rollout_line={line_no}"
if phase:
meta += f" phase={phase}"
meta += "]"
messages.append((session_id, role, meta + "\n" + text, obj.get('timestamp')))
if replace:
replace_logs(session_id, messages)
else:
for session_id_value, role, content, timestamp in messages:
add_log(session_id_value, role, content, timestamp)
return {
'session_id': session_id,
'rollout_path': str(path),
'imported': len(messages),
'user_messages': sum(1 for item in messages if item[1] == 'user'),
'assistant_messages': sum(1 for item in messages if item[1] == 'assistant'),
'replace': replace,
}
def main():
parser = argparse.ArgumentParser(
description="BetterContext CLI - Persistent memory and cross-chat relay for local AI agents."
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
parser_register = subparsers.add_parser('register-chat', help='Register a prefix and recover the MEM message counter.')
parser_register.add_argument('alias')
parser_register.add_argument('identity')
parser_register.add_argument('--visible-history-count', type=int)
parser_counter = subparsers.add_parser('chat-counter', help='Read the registered MEM counter.')
parser_counter.add_argument('chat')
parser_next = subparsers.add_parser('next-message', help='Reserve the next visible-message prefix; retries reuse the event key.')
parser_next.add_argument('chat')
parser_next.add_argument('--event-key', required=True)
subparsers.add_parser("init", help="Initialize the database.")
parser_add_fact = subparsers.add_parser("add-fact", help="Add a core fact.")
parser_add_fact.add_argument("category", help="Category of the fact (e.g., 'preference', 'rule', 'context')")
parser_add_fact.add_argument("fact", help="The text of the fact itself.")
parser_get_facts = subparsers.add_parser("get-facts", help="List all core facts.")
parser_get_facts.add_argument("--category", "-c", help="Filter by category", default=None)
parser_del_fact = subparsers.add_parser("delete-fact", help="Delete a fact.")
parser_del_fact.add_argument("id", type=int, help="ID of the fact to delete")
parser_add_task = subparsers.add_parser("add-task", help="Add a new task.")
parser_add_task.add_argument("description", help="Description of the task.")
parser_comp_task = subparsers.add_parser("complete-task", help="Mark a task as completed.")
parser_comp_task.add_argument("id", type=int, help="ID of the task")
parser_add_log = subparsers.add_parser("add-log", help="Add one chat/session log entry.")
parser_add_log.add_argument("session_id", help="Session or resolved chat id, e.g. SESSION-EXAMPLE")
parser_add_log.add_argument("role", choices=["user", "assistant", "system", "developer", "tool"], help="Log role")
parser_add_log.add_argument("content", help="Log content")
parser_add_log.add_argument("--timestamp", help="Optional timestamp to preserve source chronology")
parser_get_logs = subparsers.add_parser("get-logs", help="Read recent logs for a session.")
parser_get_logs.add_argument("session_id", help="Session id to read")
parser_get_logs.add_argument("--limit", type=int, default=50, help="Maximum logs to return")
parser_count_logs = subparsers.add_parser("count-logs", help="Count logs globally or for a session.")
parser_count_logs.add_argument("session_id", nargs="?", help="Optional session id")
parser_import_codex = subparsers.add_parser("import-codex-thread", help="Import user/assistant messages from a Codex rollout JSONL file.")
parser_import_codex.add_argument("rollout_path", help="Path to rollout-*.jsonl")
parser_import_codex.add_argument("session_id", help="Session id to store logs under, e.g. SESSION-EXAMPLE")
parser_import_codex.add_argument("--replace", action="store_true", help="Replace existing logs for this session before import")
parser_import_codex.add_argument("--include-environment-context", action="store_true", help="Also import synthetic <environment_context> user messages")
parser_get_context = subparsers.add_parser("get-context", help="Print active memory and an optional chat relay inbox.")
parser_get_context.add_argument("--chat", help="Chat alias or UUID whose unread relay messages should be included")
parser_get_context.add_argument("--relay-limit", type=int, default=20, help="Maximum unread relay messages to include")
parser_get_context.add_argument("--claim-relays", action="store_true", help="Mark included relay messages as delivered")
parser_link_chat = subparsers.add_parser("link-chat", help="Link a short ID to a full UUID.")
parser_link_chat.add_argument("short_id", help="The short ID (e.g., BC1).")
parser_link_chat.add_argument("uuid", help="The full conversation UUID.")
parser_resolve_chat = subparsers.add_parser("resolve-chat", help="Resolve a short ID to a full UUID.")
parser_resolve_chat.add_argument("short_id", help="The short ID to resolve.")
subparsers.add_parser("list-aliases", help="List all chat aliases.")
parser_relay_send = subparsers.add_parser("relay-send", help="Send a durable message from one registered chat to another.")
parser_relay_send.add_argument("sender", help="Sender chat alias or UUID")
parser_relay_send.add_argument("recipient", help="Recipient chat alias or UUID")
parser_relay_send.add_argument("message", help="Message body")
parser_relay_send.add_argument("--reply-to", type=int, help="Relay message ID being answered")
parser_relay_send.add_argument("--dedupe-key", help="Sender-scoped retry key; repeated sends return the original message")
parser_relay_send.add_argument("--metadata-json", help="Optional JSON object with machine-readable metadata")
parser_relay_inbox = subparsers.add_parser("relay-inbox", help="Read a chat's relay inbox.")
parser_relay_inbox.add_argument("recipient", help="Recipient chat alias or UUID")
parser_relay_inbox.add_argument("--limit", type=int, default=50, help="Maximum messages to return")
parser_relay_inbox.add_argument("--include-read", action="store_true", help="Include acknowledged messages")
parser_relay_inbox.add_argument("--claim", action="store_true", help="Mark returned pending messages as delivered")
parser_relay_watch = subparsers.add_parser("relay-watch", help="Wait for an unread relay message.")
parser_relay_watch.add_argument("recipient", help="Recipient chat alias or UUID")
parser_relay_watch.add_argument("--timeout", type=float, default=60, help="Maximum seconds to wait")
parser_relay_watch.add_argument("--interval", type=float, default=1, help="Polling interval in seconds")
parser_relay_watch.add_argument("--limit", type=int, default=50, help="Maximum messages to return")
parser_relay_watch.add_argument("--claim", action="store_true", help="Mark returned pending messages as delivered")
parser_relay_ack = subparsers.add_parser("relay-ack", help="Acknowledge relay messages after processing them.")
parser_relay_ack.add_argument("recipient", help="Recipient chat alias or UUID")
parser_relay_ack.add_argument("message_ids", nargs='+', type=int, help="One or more relay message IDs")
parser_relay_outbox = subparsers.add_parser("relay-outbox", help="List messages sent by a chat.")
parser_relay_outbox.add_argument("sender", help="Sender chat alias or UUID")
parser_relay_outbox.add_argument("--limit", type=int, default=50, help="Maximum messages to return")
parser_relay_status = subparsers.add_parser("relay-status", help="Inspect one relay message and its delivery state.")
parser_relay_status.add_argument("message_id", type=int, help="Relay message ID")
args = parser.parse_args()
init_db()
if args.command == "init":
init_db()
print("Database initialized successfully at memory.db")
elif args.command == "add-fact":
success = add_fact(args.category, args.fact)
if success:
print(f"Fact added successfully to category '{args.category}'.")
else:
print("Fact already exists.", file=sys.stderr)
elif args.command == "get-facts":
facts = get_facts(args.category)
print(json.dumps(facts, indent=2, default=str))
elif args.command == "delete-fact":
delete_fact(args.id)
print(f"Fact {args.id} deleted.")
elif args.command == "add-task":
task_id = add_task(args.description)
print(f"Task added with ID: {task_id}")
elif args.command == "complete-task":
update_task_status(args.id, 'completed')
print(f"Task {args.id} marked as completed.")
elif args.command == "add-log":
add_log(args.session_id, args.role, args.content, args.timestamp)
print(f"Log added to session '{args.session_id}'.")
elif args.command == "get-logs":
logs = get_logs(args.session_id, args.limit)
print(json.dumps(logs, indent=2, default=str))
elif args.command == "count-logs":
print(count_logs(args.session_id))
elif args.command == "import-codex-thread":
result = import_codex_thread(
args.rollout_path,
args.session_id,
replace=args.replace,
include_environment_context=args.include_environment_context,
)
print(json.dumps(result, indent=2, default=str))
elif args.command == "get-context":
if args.claim_relays and not args.chat:
raise ValueError("--claim-relays requires --chat")
print(format_context(args.chat, args.relay_limit, args.claim_relays))
elif args.command in {'link-chat', 'register-chat'}:
alias = args.short_id if args.command == 'link-chat' else args.alias
identity = args.uuid if args.command == 'link-chat' else args.identity
result = chat_tracking.register(get_connection, alias, identity,
visible_history_count=getattr(args, 'visible_history_count', None))
print(json.dumps(result, indent=2))
elif args.command == 'chat-counter':
print(json.dumps(chat_tracking.status(get_connection, args.chat), indent=2))
elif args.command == 'next-message':
print(json.dumps(chat_tracking.next_message(get_connection, args.chat, args.event_key), indent=2))
elif args.command == "resolve-chat":
uuid = resolve_chat(args.short_id)
if uuid:
print(uuid)
else:
print(f"No UUID found for {args.short_id}", file=sys.stderr)
elif args.command == "list-aliases":
aliases = get_all_aliases()
print(json.dumps(aliases, indent=2, default=str))
elif args.command == "relay-send":
metadata = json.loads(args.metadata_json) if args.metadata_json else None
if metadata is not None and not isinstance(metadata, dict):
raise ValueError("--metadata-json must contain a JSON object")
message = send_relay_message(
args.sender,
args.recipient,
args.message,
reply_to_id=args.reply_to,
dedupe_key=args.dedupe_key,
metadata=metadata,
)
print(json.dumps(message, indent=2, default=str))
elif args.command == "relay-inbox":
messages = get_relay_inbox(
args.recipient,
limit=args.limit,
include_read=args.include_read,
claim=args.claim,
)
print(json.dumps(messages, indent=2, default=str))
elif args.command == "relay-watch":
if args.timeout < 0 or args.timeout > 3600:
raise ValueError("--timeout must be between 0 and 3600 seconds")
if args.interval < 0.1 or args.interval > 60:
raise ValueError("--interval must be between 0.1 and 60 seconds")
deadline = time.monotonic() + args.timeout
messages = []
while True:
messages = get_relay_inbox(
args.recipient,
limit=args.limit,
claim=args.claim,
)
if messages or time.monotonic() >= deadline:
break
time.sleep(min(args.interval, max(0, deadline - time.monotonic())))
print(json.dumps(messages, indent=2, default=str))
elif args.command == "relay-ack":
messages = acknowledge_relay_messages(args.recipient, args.message_ids)
print(json.dumps(messages, indent=2, default=str))
elif args.command == "relay-outbox":
messages = get_relay_outbox(args.sender, limit=args.limit)
print(json.dumps(messages, indent=2, default=str))
elif args.command == "relay-status":
message = get_relay_message(args.message_id)
if not message:
raise ValueError(f"relay message {args.message_id} does not exist")
print(json.dumps(message, indent=2, default=str))
else:
parser.print_help()
if __name__ == "__main__":
try:
main()
except (ValueError, json.JSONDecodeError, sqlite3.Error) as error:
print(f"Error: {error}", file=sys.stderr)
raise SystemExit(2)
SHA-256: 4201a3462a64f56f7a6f71df14b8d84bd258fd070efba0db6da15a8f789005f2