Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion osiris_cli/living.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@
# num_ctx tokens: a 100k-character paste timed out twice (601 s, 406 s) on 2026-10-01.
# Long messages go to the mentor as a bounded excerpt; the ledger keeps the full text.
LONG_MESSAGE = 3000
# A small voice can fall into a loop and repeat one block until the reply limit (qwen2.5:1.5b
# wrote one ~1,500-character block seven times on 2026-11-09). Code stops the stream once the
# last REPEAT_SPAN characters have appeared REPEAT_LIMIT times; the reply is marked incomplete.
REPEAT_SPAN = 300
REPEAT_LIMIT = 3


def repeating(text: str, span: int = REPEAT_SPAN, limit: int = REPEAT_LIMIT) -> bool:
"""True once the last ``span`` characters of ``text`` occur ``limit`` or more times in it."""
if len(text) < span * limit:
return False
return text.count(text[-span:]) >= limit

MESSAGE_BUDGET = 6000
# The voice: /mentor NAME, else OSIRIS_VOICE, else the first of these that is installed.
# OSIRIS_CHAT_MODEL is the console's Architect (intent -> spec), not the voice: it is
Expand Down Expand Up @@ -1066,6 +1079,7 @@ def converse(self, text: str, learnable: bool = True,

sent_user = None
incomplete = ""
looped = False
force_core = getattr(self, "force_core_voice", None)
use_core_voice = bool(force_core) if force_core is not None else (os.environ.get("OSIRIS_CORE_VOICE", "1") != "0")
if gate["open"] and self.core is not None and use_core_voice:
Expand Down Expand Up @@ -1113,6 +1127,9 @@ def converse(self, text: str, learnable: bool = True,
first_word = time.time() - waiting.started
reply += piece
self.out(piece)
if repeating(reply):
looped = True
break
except KeyboardInterrupt:
waiting.stop()
interrupted = True
Expand All @@ -1131,7 +1148,9 @@ def converse(self, text: str, learnable: bool = True,
if first_word is not None:
self._note_first_word(model, first_word)
done = getattr(self.mentor, "last_done", None) or {}
if not interrupted and done.get("done_reason") == "length":
if not interrupted and looped:
incomplete = "stopped: the reply began repeating itself"
elif not interrupted and done.get("done_reason") == "length":
incomplete = f"cut off at the {MENTOR_MAX_TOKENS}-token reply limit"
elif not interrupted and reply.count("```") % 2:
incomplete = "ends inside an unclosed code block"
Expand Down
55 changes: 55 additions & 0 deletions osiris_cli/osiris_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1902,6 +1902,53 @@ def execute_trio(rest: str) -> None:
print(f"\n[!] {e}\n")


# Commands this REPL handles itself (the command branches of dispatch_command); a test
# keeps the set in step with the source.
REPL_COMMANDS = {
"/apply", "/architecture", "/benchmark", "/bridges", "/chat", "/check", "/clean", "/closure",
"/code", "/core", "/digest", "/discard", "/dna", "/experiment", "/fast", "/firewall", "/forge",
"/forget", "/gemini", "/help", "/ignite", "/ingest", "/intent", "/introspect", "/learn",
"/ledger", "/legit", "/mentor", "/mesh", "/mutate", "/nclm", "/ollama", "/osiris", "/physics",
"/power", "/pr", "/publish", "/q", "/remember", "/retrieval", "/run", "/self", "/sim",
"/status", "/swarm", "/sync", "/tetra1d", "/tournament", "/train", "/trio", "/unlearn",
"/update", "/validate", "/voice", "/welcome",
}
# Safe to offer as a one-key [1]: they only read and print.
READ_ONLY_COMMANDS = {
"/help", "/status", "/home", "/check", "/osiris", "/mentors", "/why", "/ledger",
"/architecture", "/lab", "/sources", "/hypotheses", "/gaps", "/runs", "/suggest", "/welcome",
"/power", "/firewall", "/ollama", "/facts", "/nclm", "/intent",
}
# Commands that need words after them to do anything.
NEEDS_ARGUMENT = {"/code": "/code explain osiris_cli/living.py", "/legit": "/legit list",
"/remember": "/remember <fact>", "/forget": "/forget <words>",
"/self": "/self <text>", "/chat": "/chat <message>", "/dna": '/dna "<intent>"',
"/gemini": "/gemini <question>", "/physics": "/physics chsh"}


def bare_command(word: str) -> Optional[str]:
"""``"/" + word`` when that is a command name, else None. Single letters are menu keys."""
if len(word) < 2 or not word.isalpha():
return None
name = "/" + word.lower()
known = REPL_COMMANDS | CONSOLE_COMMANDS | set(getattr(otc, "IMMEDIATE_COMMANDS", ()) or ())
return name if name in known else None


def offer_bare_command(word: str) -> None:
name = bare_command(word)
print(f"\n[!] '{word}' on its own looks like the command {name} -- not sent to the model.")
if name in NEEDS_ARGUMENT:
print(f" It needs more words, for example: {NEEDS_ARGUMENT[name]}")
elif name in READ_ONLY_COMMANDS and otc is not None:
otc._pending_suggestions.clear()
otc._pending_suggestions.update({"1": name})
print(f" [1] Run {name} (read-only)")
else:
print(f" Type {name} to run it.")
print(" To talk to OSIRIS instead, write a few more words.\n")


def dispatch_capturing(state: OsirisReplState, line: str) -> None:
"""safe_dispatch with keys typed during the command's output captured, not echoed into it
(2026-10-25: '/architecture' typed while /check printed was scattered into the bench table,
Expand Down Expand Up @@ -2028,6 +2075,14 @@ def dispatch_command(state: OsirisReplState, line: str) -> None:
except Exception as e:
print(f"\033[1;31m[!] Console command error ({cmd}): {e}\033[0m\n")
return
# 3a. A lone word that is exactly a command name ("code", "status") is that command typed
# without its "/". It is not sent to the model: one word gives a small voice nothing to
# answer, and on 2026-11-09 "code" alone got a looping, invented description of OSIRIS.
# Read-only commands are offered as [1]; the rest are named, never run for you.
if len(parts) == 1 and not cmd.startswith("/") and bare_command(cmd):
offer_bare_command(cmd)
return

# 3. Commands need a leading "/". Anything else is conversation with OSIRIS,
# so a sentence that starts with "learn", "status" or "dna" is never hijacked.
if not cmd.startswith("/") and cmd not in ("?",):
Expand Down
12 changes: 11 additions & 1 deletion osiris_termux_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -3161,7 +3161,7 @@ def _bench_preflight(command, argv, label, ui):
+ ("" if argv[3] in ("gemini", "gateway") else ", or use cloud quota"))]
inner = osiris_ui.Canvas(ui.mode, ui.cols - 4)
print("\n" + ui.panel("MENTOR ROUND · PREFLIGHT", inner.facts(rows).split("\n") + [
"", ui.dim("Plug in the charger: the phone works hard for the whole round.")]))
*[ui.dim(t) if t else t for t in _charger_note()]]))
_pending_suggestions.clear()
_pending_suggestions.update({"1": command.strip() + " now", "0": "/home"})
print(ui.actions("START?", [("1", "Start the round"), ("0", "Not now")]) + "\n")
Expand Down Expand Up @@ -5376,6 +5376,16 @@ def _is_unknown_command(text: str) -> bool:
return stripped.split()[0].lower() not in IMMEDIATE_COMMANDS


def _charger_note() -> list:
"""The charger reminder, only on the phone (Node Alpha); a desktop has no battery to drain."""
try:
from osiris.substrate import node_type
on_phone = node_type() == "NODE_ALPHA_MOBILE"
except Exception: # noqa: BLE001 - no substrate module: say nothing rather than guess
on_phone = False
return ["", "Plug in the charger: the phone works hard for the whole round."] if on_phone else []


def _unknown_command(text: str):
first = text.strip().split()[0].lower()
head = "/" + first.lstrip("/").split("/")[0]
Expand Down
30 changes: 30 additions & 0 deletions tests/test_living.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,33 @@ def test_the_voice_is_osiris_voice_not_the_architects_chat_model(monkeypatch):
assert m.model() == "qwen2.5:3b"
m.set_model("qwen2.5:7b") # /mentor wins
assert m.model() == "qwen2.5:7b"


class LoopingMentor(ScriptedMentor):
"""Repeats one block forever, like qwen2.5:1.5b on 2026-11-09."""

def stream(self, messages, model=None):
self.calls.append(messages)
self.sent = 0
block = ("### Key Components\n1. **Quantum Computing Engine**: a block the voice repeats, "
"word for word, until the reply limit stops it. ") * 3
while self.sent < 200:
self.sent += 1
yield block


def test_repeating_spots_a_loop_and_not_ordinary_text():
assert not living.repeating("a short reply")
prose = " ".join(f"sentence {i} says something new." for i in range(200))
assert not living.repeating(prose)
assert living.repeating(("the same paragraph again and again. " * 12) * 4)


def test_a_looping_voice_is_stopped_by_code_and_not_learned(tmp_path):
mentor = LoopingMentor()
o, out = make(tmp_path, mentor=mentor)
o.converse("tell me about the code")
said = "".join(out)
assert mentor.sent < 10 # stopped long before 200 blocks
assert "stopped: the reply began repeating itself" in said
assert "not learned: incomplete reply" in said
28 changes: 28 additions & 0 deletions tests/test_repl_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,34 @@ def test_keys_and_typos_never_reach_the_model(calls, capsys):
assert calls == [("_unknown_command", ("/stauts",))]
assert capsys.readouterr().out.count("No menu item") == 2

def test_a_bare_command_word_is_not_sent_to_the_model(calls, capsys):
run("code")
run("status")
run("publish")
out = capsys.readouterr().out
assert not [c for c in calls if c[0] == "model"]
assert "looks like the command /code" in out and "/code explain" in out
assert "[1] Run /status (read-only)" in out
assert "Type /publish to run it" in out and "[1] Run /publish" not in out
assert otc._pending_suggestions == {"1": "/status"} # publish is never offered

def test_sentences_and_unknown_words_still_go_to_osiris(calls):
run("code review my parser please")
run("hello")
assert calls == [("model", "code review my parser please"), ("model", "hello")]

def test_repl_commands_set_matches_the_dispatch_source():
found = set(re.findall(r'cmd (?:==|in) \(?"(/[a-z0-9_-]+)"', REPL_SRC))
assert found <= osiris_repl.REPL_COMMANDS | osiris_repl.CONSOLE_COMMANDS, \
found - osiris_repl.REPL_COMMANDS - osiris_repl.CONSOLE_COMMANDS

def test_charger_note_only_on_the_phone(monkeypatch):
import osiris.substrate as sub
monkeypatch.setattr(sub, "node_type", lambda: "NODE_BETA_COMPUTE")
assert otc._charger_note() == []
monkeypatch.setattr(sub, "node_type", lambda: "NODE_ALPHA_MOBILE")
assert "charger" in " ".join(otc._charger_note())

def test_plain_text_still_goes_to_osiris(calls):
run("hello osiris")
run("/home/enki/log.txt line 1\nline 2") # a pasted log is conversation
Expand Down
Loading