Skip to content

Commit 30697bf

Browse files
committed
v56: use direct bounded conversational inference and finite queue retries
1 parent a09d101 commit 30697bf

6 files changed

Lines changed: 256 additions & 20 deletions

File tree

‎V100_AUDIT.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,26 @@ Validation: 1018 passed, 65 skipped, one resource-dependent worker integration
647647
deselected, one existing PEFT warning. Ruff and upgrade shell checks pass. The
648648
transaction test executes four refreshes and checks actual object identity for
649649
reader/text nodes, expanded state and page/card/text scroll offsets.
650+
651+
## v56: direct conversational transport and finite connection retries
652+
653+
Ordinary questions now make one chat-completions generation after a bounded
654+
accepted-model identity probe. They skip apply-template/tokenize requests, full
655+
research evidence/context assembly and cold-starting a large CPU chat model.
656+
The compact prompt contains the recorded goal and current mission state; absent
657+
profit/training counters remain unknown. Incomplete output is rejected, not
658+
reported as a model answer. Generation uses a 20-second socket timeout and a
659+
40-second total deadline. Explicit work keeps its executable tool path and
660+
separate 60-second transport budget.
661+
662+
Connection failures retry at most once before becoming terminal and releasing
663+
the next queued request. The console waits up to 200 seconds to cover the existing
664+
180-second work budget instead of leaving a still-processing request after 120.
665+
This does not increase the backend work budget. A busy/offline server can still
666+
fail explicitly; these tests do not certify operation on the user's hardware.
667+
668+
Validation: 1022 passed, 65 skipped, one resource-dependent worker integration
669+
deselected; one existing PEFT warning. Ruff, diff and upgrade shell checks pass.
670+
Tests cover the actual question route (one generation, bounded identity check,
671+
no CPU cold start/tool loop), incomplete output rejection and queue advancement
672+
after terminal connection failures.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "rlms"
33
authors = [
44
{name = "Alex Zhang", email = "altzhang@mit.edu"},
55
]
6-
version = "0.1.3+v100.55"
6+
version = "0.1.3+v100.56"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/v100/mission_chat.py‎

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,56 @@ def conversation_question(message: str) -> bool:
243243
)
244244

245245

246+
def conversational_response(
247+
root: Path, client, message: str, mission: dict, plans: dict, responder: dict
248+
) -> dict:
249+
"""One actual model request with bounded factual context and no tool routing."""
250+
goal = (plans.get("long") or {}).get("text", "No recorded goal")
251+
facts = {
252+
"mission_running": mission.get("running"),
253+
"phase": mission.get("state", {}).get("phase"),
254+
"learning": mission.get("learning"),
255+
"goal": goal[:3000],
256+
}
257+
client.timeout = min(client.timeout, 20)
258+
client.request_deadline = min(
259+
getattr(client, "request_deadline", time.monotonic() + 40), time.monotonic() + 40
260+
)
261+
reply = client.request(
262+
"/v1/chat/completions",
263+
{
264+
"model": client.model_name,
265+
"messages": [
266+
{
267+
"role": "system",
268+
"content": "You are Synta. Answer the user's question directly in Polish, briefly. This is read-only chat: do not claim to execute work or update plans. Only the supplied status is verified. Missing optimizer/profit counters are unknown. Explain how the recorded goal relates to the question, without changing it or promising profit. State missing evidence honestly. Verified current facts: "
269+
+ json.dumps(facts, ensure_ascii=False),
270+
},
271+
{"role": "user", "content": message},
272+
],
273+
"max_tokens": 512,
274+
"temperature": 0.0,
275+
"stream": False,
276+
**client.template_args(),
277+
},
278+
)
279+
choice = reply["choices"][0]
280+
answer = choice["message"].get("content")
281+
if (
282+
choice.get("finish_reason") not in ("stop", "eos")
283+
or not isinstance(answer, str)
284+
or not answer.strip()
285+
):
286+
raise ValueError("Conversational model response incomplete; no invented answer accepted")
287+
return {
288+
"answer": answer.strip(),
289+
"actions": [],
290+
"applied": [],
291+
"responder": responder,
292+
"scope": "Single model generation; read-only question, no tool execution or control changes",
293+
}
294+
295+
246296
def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> dict:
247297
from rlm.v100.agent import native_turn
248298
from rlm.v100.competition import helper_client
@@ -255,6 +305,14 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
255305
# Authorization comes only from the current authenticated local user message,
256306
# never from a model-generated action or text retrieved from the internet.
257307
message = request["message"].strip()
308+
quick_question = conversation_question(message) or message.casefold() in (
309+
"hej",
310+
"cześć",
311+
"czesc",
312+
"hello",
313+
"hi",
314+
"odpowiedz",
315+
)
258316
if re.search(
259317
r"\b(?:co potrafisz|co umiesz|twoje możliwości|twoje mozliwosci|what can you do)\b",
260318
message.casefold(),
@@ -388,6 +446,9 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
388446
client.activity_actor = "chat"
389447
# The endpoint is reused for candidate evaluation. Chat waits rather than
390448
# talking to an unaccepted candidate or opening another GPU server.
449+
client.request_deadline = request.get("deadline", time.monotonic() + 180)
450+
generation_timeout = min(getattr(client, "timeout", 120), 60)
451+
client.timeout = min(generation_timeout, 10)
391452
delegated = False
392453
cpu_master = False
393454
cpu_detail = None
@@ -398,9 +459,14 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
398459
except (requests.RequestException, OSError):
399460
if not profile.get("resources", {}).get("interactive_lab"):
400461
raise
401-
if accepted_cpu is not None and profile.get("resources", {}).get("accepted_cpu_chat"):
462+
if (
463+
accepted_cpu is not None
464+
and not quick_question
465+
and profile.get("resources", {}).get("accepted_cpu_chat")
466+
):
402467
try:
403468
client = accepted_cpu.get(profile)
469+
generation_timeout = min(client.timeout, 60)
404470
cpu_master = True
405471
except (ValueError, OSError, RuntimeError, requests.RequestException) as error:
406472
cpu_detail = str(error)[:300]
@@ -417,23 +483,39 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
417483
"Master busy; no independent remote chat helper"
418484
) from None
419485
client = helper_client(helper, root)
486+
client.request_deadline = request.get("deadline", time.monotonic() + 180)
487+
generation_timeout = min(client.timeout, 60)
488+
client.timeout = min(generation_timeout, 20)
420489
client.identity()
421490
client.sampling_args["max_tokens"] = 2048
422491
client.enable_thinking = False
423492
client.activity_actor = "chat-delegate"
424493
delegated = True
425494
client.request_deadline = request.get("deadline", time.monotonic() + 180)
426-
client.timeout = min(client.timeout, 60)
495+
client.timeout = generation_timeout
427496
client.enable_thinking = False
428497
client.sampling_args = dict(client.sampling_args)
429498
client.sampling_args["max_tokens"] = min(2048, client.sampling_args.get("max_tokens", 2048))
430499
client.activity_context = {**client.activity_context, "chat_request_id": request.get("id")}
431500
from rlm.v100 import chat_resources
432501

433502
resource_request = chat_resources.instruction(message)
434-
question_only = conversation_question(message) and not resource_request
503+
question_only = quick_question and not resource_request
435504
if question_only:
436-
client.sampling_args["max_tokens"] = min(768, client.sampling_args["max_tokens"])
505+
return conversational_response(
506+
root,
507+
client,
508+
message,
509+
mission,
510+
read_plans(root),
511+
{
512+
"model": client.model_name,
513+
"delegated_while_master_busy": delegated,
514+
"accepted_master_on_cpu": cpu_master,
515+
"accepted_model_sha256": accepted_cpu.sha256 if cpu_master else None,
516+
"cpu_admission_detail": cpu_detail,
517+
},
518+
)
437519
with connect(root) as db:
438520
history = db.execute(
439521
"SELECT message,response FROM requests WHERE state='completed' ORDER BY rowid DESC LIMIT 3"
@@ -667,7 +749,11 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
667749
"decisions", "user-command-completed", {"id": request["id"], **response}
668750
)
669751
except (requests.ConnectionError, requests.Timeout) as error:
670-
expired = time.monotonic() >= request["deadline"] or isinstance(error, requests.Timeout)
752+
expired = (
753+
time.monotonic() >= request["deadline"]
754+
or isinstance(error, requests.Timeout)
755+
or request["attempts"] >= 1
756+
)
671757
with connect(root) as db:
672758
db.execute(
673759
"UPDATE requests SET state=?,attempts=attempts+1,error=?,updated=? WHERE id=? AND state='queued'",
@@ -678,6 +764,16 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
678764
request["id"],
679765
),
680766
)
767+
ActivityLog(root, "controller", "chat").write(
768+
"errors",
769+
"user-command-failed" if expired else "user-command-retry",
770+
{
771+
"id": request["id"],
772+
"error": str(error)[:500],
773+
"attempts": request["attempts"] + 1,
774+
"terminal": expired,
775+
},
776+
)
681777
except Exception as error:
682778
with connect(root) as db:
683779
db.execute(
@@ -710,7 +806,7 @@ def alongside(root: Path, directory: Path):
710806
accepted_cpu.close()
711807

712808

713-
def wait_reply(root: Path, identity: str, timeout: float = 120, progress=None) -> dict:
809+
def wait_reply(root: Path, identity: str, timeout: float = 200, progress=None) -> dict:
714810
deadline = time.monotonic() + timeout
715811
next_update = time.monotonic()
716812
while time.monotonic() < deadline:

‎tests/test_v100_chat_backend.py‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,110 @@ def test_readonly_question_uses_conversation_route(message):
195195
)
196196
def test_explicit_work_keeps_executable_route(message):
197197
assert not mission_chat.conversation_question(message)
198+
199+
200+
def test_conversation_is_one_real_generation_without_template_or_tools(tmp_path):
201+
calls = []
202+
client = SimpleNamespace(
203+
model_name="v100",
204+
timeout=180,
205+
template_args=lambda: {"chat_template_kwargs": {"enable_thinking": False}},
206+
request=lambda endpoint, payload: calls.append((endpoint, payload))
207+
or {
208+
"choices": [
209+
{
210+
"finish_reason": "stop",
211+
"message": {"content": "Cel pozostaje ten sam; nie ma dowodu zysku."},
212+
}
213+
]
214+
},
215+
)
216+
result = mission_chat.conversational_response(
217+
tmp_path,
218+
client,
219+
"a co z zarabianiem",
220+
{"running": True, "state": {"phase": "income-research"}},
221+
{"long": {"text": "Operator goal"}},
222+
{"model": "v100"},
223+
)
224+
assert len(calls) == 1 and calls[0][0] == "/v1/chat/completions"
225+
assert calls[0][1]["max_tokens"] == 512
226+
assert "tools" not in calls[0][1]
227+
assert "Operator goal" in calls[0][1]["messages"][0]["content"]
228+
assert result["applied"] == [] and client.timeout == 20
229+
230+
231+
def test_conversation_rejects_truncated_generation(tmp_path):
232+
client = SimpleNamespace(
233+
model_name="v100",
234+
timeout=60,
235+
template_args=lambda: {},
236+
request=lambda *a: {
237+
"choices": [{"finish_reason": "length", "message": {"content": "partial"}}]
238+
},
239+
)
240+
with pytest.raises(ValueError, match="incomplete"):
241+
mission_chat.conversational_response(tmp_path, client, "hej", {}, {}, {"model": "v100"})
242+
243+
244+
def test_question_fast_path_skips_cpu_start_and_research_context(tmp_path, monkeypatch):
245+
accepted = profile(tmp_path)
246+
atomic_json(tmp_path / "serving-active.json", accepted)
247+
monkeypatch.setattr(mission_chat, "load_profile", lambda *a: accepted)
248+
monkeypatch.setattr(
249+
mission, "status", lambda *a: {"running": True, "state": {}, "learning": {}}
250+
)
251+
calls = []
252+
client = SimpleNamespace(
253+
model_name="v100",
254+
timeout=120,
255+
sampling_args={},
256+
activity_context={},
257+
template_args=lambda: {},
258+
)
259+
260+
def request(endpoint, payload=None):
261+
calls.append((endpoint, client.timeout))
262+
if endpoint == "/props":
263+
return {"model_path": accepted["server"]["model"]}
264+
return {"choices": [{"finish_reason": "stop", "message": {"content": "Odpowiedź modelu"}}]}
265+
266+
client.request = request
267+
monkeypatch.setattr(competition, "helper_client", lambda *a: client)
268+
269+
def forbidden(*a, **kw):
270+
pytest.fail("Question must not start a CPU model or research/tool loop")
271+
272+
monkeypatch.setattr(research_tools, "research_turn", forbidden)
273+
manager = SimpleNamespace(get=forbidden)
274+
result = mission_chat.respond(tmp_path, tmp_path, {"message": "a co z zarabianiem"}, manager)
275+
assert result["answer"] == "Odpowiedź modelu"
276+
assert calls == [("/props", 10), ("/v1/chat/completions", 20)]
277+
278+
279+
def test_connection_failures_stop_retrying_and_release_next_request(tmp_path, monkeypatch):
280+
import requests
281+
282+
first = mission_chat.submit(tmp_path, "first")
283+
second = mission_chat.submit(tmp_path, "second")
284+
with mission_chat.connect(tmp_path) as db:
285+
db.execute("UPDATE requests SET attempts=1 WHERE id=?", (first,))
286+
seen = []
287+
288+
def respond(*args):
289+
request = args[2]
290+
seen.append(request["id"])
291+
if request["id"] == first:
292+
raise requests.ConnectionError("backend unavailable")
293+
return {"answer": "next response", "actions": [], "applied": []}
294+
295+
monkeypatch.setattr(mission_chat, "respond", respond)
296+
297+
class Stop:
298+
def wait(self, seconds):
299+
return len(seen) >= 2
300+
301+
mission_chat.service_loop(tmp_path, tmp_path, Stop())
302+
assert mission_chat.inspect(tmp_path, first)["state"] == "failed"
303+
assert mission_chat.inspect(tmp_path, second)["state"] == "completed"
304+
assert seen == [first, second]

‎tests/test_v100_mission_evidence.py‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from rlm.v100 import agent, competition, mission, mission_chat, mission_evidence, research_tools
7+
from rlm.v100 import competition, mission, mission_chat, mission_evidence, research_tools
88
from rlm.v100.common import atomic_json
99

1010

@@ -147,23 +147,33 @@ def test_ordinary_chat_gets_host_evidence_before_model_can_guess(tmp_path, monke
147147
monkeypatch.setattr(
148148
mission_chat, "load_profile", lambda *args: {"runtime": {}, "server": {"model": str(model)}}
149149
)
150+
seen = []
151+
152+
def reply(endpoint, payload=None):
153+
if endpoint == "/props":
154+
return {"model_path": str(model)}
155+
seen.extend(payload["messages"])
156+
return {
157+
"choices": [
158+
{
159+
"finish_reason": "stop",
160+
"message": {"content": "Counters unavailable; no training claim."},
161+
}
162+
]
163+
}
164+
150165
client = SimpleNamespace(
151-
request=lambda *args: {"model_path": str(model)},
166+
request=reply,
167+
template_args=lambda: {},
152168
model_name="v100",
153169
timeout=120,
154170
sampling_args={"max_tokens": 2048},
155171
activity_context={},
156172
)
157173
monkeypatch.setattr(competition, "helper_client", lambda *args: client)
158-
seen = []
159-
160-
def reply(client, messages, **kwargs):
161-
seen.extend(messages)
162-
return {"content": '{"answer":"Counters unavailable; no training claim.","actions":[]}'}
163-
164-
monkeypatch.setattr(agent, "native_turn", reply)
165174
result = mission_chat.respond(tmp_path, run, {"message": "Czy trenujesz?"})
166-
evidence = json.loads(seen[-1]["content"])["mission_evidence"]
167-
assert evidence["optimizer_updates_observed"] is None
168-
assert "Never invent a grader error cause" in seen[0]["content"]
175+
evidence = json.loads(seen[0]["content"].split("Verified current facts: ", 1)[1])
176+
assert evidence["learning"]["completed_cycles"] == 0
177+
assert evidence["phase"] == "baseline-before-weight-updates"
178+
assert "Missing optimizer/profit counters are unknown" in seen[0]["content"]
169179
assert result["actions"] == [] and result["applied"] == []

‎tools/upgrade-v100-retention.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ PY
4747
uv --no-config pip install --python "$PY" --no-deps --reinstall-package rlms \
4848
"rlms @ git+https://github.com/Mantiee/rlm.git@$REV"
4949
uv --no-config pip check --python "$PY"
50-
uv --no-config pip freeze --python "$PY" > "$AI_V100_ROOT/research/requirements.continual.v10055.txt"
50+
uv --no-config pip freeze --python "$PY" > "$AI_V100_ROOT/research/requirements.continual.v10056.txt"
5151

5252
"$PY" <<'PY'
5353
import json, os

0 commit comments

Comments
 (0)