Skip to content

Commit 6a7f2cb

Browse files
committed
v43: isolate interactive inference budgets, expose active chat and stop timeout retry loops
1 parent 6ac52aa commit 6a7f2cb

11 files changed

Lines changed: 155 additions & 11 deletions

‎CHAT_GUI_PACING_FIX.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@
77
This fixes optional vision admission, not every possible source of latency. An admitted image request still uses the configured remote timeout. A chat request can still show `queued` while executing. It is not proof that the dashboard was edited or that the latest layout passed host validation.
88

99
Verification: 123 remote-helper/campaign tests passed, including deferred cooldown, thread contention, preserved screenshot and unchanged idle reservation. Ruff and installer shell syntax checks passed. V100/RTX deployment requires the operator upgrade; no real hardware result is claimed here.
10+
11+
## v43: interactive budgets and visible processing
12+
13+
Research policy no longer overrides chat thinking/output settings. Chat uses no thinking, at most 2048 output tokens and at most 60 seconds per HTTP operation. A shared monotonic 180-second deadline follows the client's copies across template/token counting, tool routing, final generation and helper pacing. This is an inference/pacing budget, not a hard wall-clock guarantee for CPU model startup or arbitrary guest tools. Timeout ends the request with a visible failure instead of endlessly retrying the same timeout.
14+
15+
`chat-status` includes a `processing` record for the request currently handled by the live mission PID, including start time and time budget. The persisted state remains `queued` for compatibility while execution is active. The record is cleared when handling finishes. Completion updates only still-queued requests, preserving operator cancellation.
16+
17+
The dashboard request still requires actual execution and host layout validation. These changes do not claim that an HTML edit or profitable training has occurred.

‎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.42"
6+
version = "0.1.3+v100.43"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/clients/llamacpp.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,25 @@ def request(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str
114114
)
115115
return result
116116

117+
def remaining_timeout(self) -> float:
118+
deadline = getattr(self, "request_deadline", None)
119+
if deadline is None:
120+
return self.timeout
121+
remaining = deadline - time.monotonic()
122+
if remaining <= 0:
123+
raise requests.Timeout("Chat request exceeded its total time budget")
124+
return min(self.timeout, remaining)
125+
117126
def http_request(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
118127
# No environment proxy routing for local model traffic.
119128
with requests.Session() as session:
120129
session.trust_env = False
121130
response = (
122-
session.get(self.base_url + endpoint, timeout=self.timeout)
131+
session.get(self.base_url + endpoint, timeout=self.remaining_timeout())
123132
if data is None
124-
else session.post(self.base_url + endpoint, json=data, timeout=self.timeout)
133+
else session.post(
134+
self.base_url + endpoint, json=data, timeout=self.remaining_timeout()
135+
)
125136
)
126137
response.raise_for_status()
127138
return response.json()

‎rlm/v100/mission_chat.py‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import copy
88
import json
9+
import os
910
import sqlite3
1011
import threading
1112
import time
@@ -60,6 +61,15 @@ def inspect(root: Path, identity: str) -> dict:
6061
result = dict(row)
6162
if result["response"]:
6263
result["response"] = json.loads(result["response"])
64+
active_path = root / "research/state/chat-active.json"
65+
if active_path.exists():
66+
active = json.loads(active_path.read_text())
67+
if active.get("id") == identity and result["state"] == "queued":
68+
from rlm.v100.mission import status
69+
70+
mission = status(root)
71+
if mission["running"] and mission.get("pid") == active.get("pid"):
72+
result["processing"] = active
6373
return result
6474

6575

@@ -316,6 +326,12 @@ def respond(root: Path, directory: Path, request: dict, accepted_cpu=None) -> di
316326
client.enable_thinking = False
317327
client.activity_actor = "chat-delegate"
318328
delegated = True
329+
client.request_deadline = request.get("deadline", time.monotonic() + 180)
330+
client.timeout = min(client.timeout, 60)
331+
client.enable_thinking = False
332+
client.sampling_args = dict(client.sampling_args)
333+
client.sampling_args["max_tokens"] = min(2048, client.sampling_args.get("max_tokens", 2048))
334+
client.activity_context = {**client.activity_context, "chat_request_id": request.get("id")}
319335
with connect(root) as db:
320336
history = db.execute(
321337
"SELECT message,response FROM requests WHERE state='completed' ORDER BY rowid DESC LIMIT 3"
@@ -440,6 +456,17 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
440456
if row is None:
441457
continue
442458
request = dict(row)
459+
request["deadline"] = time.monotonic() + 180
460+
atomic_json(
461+
root / "research/state/chat-active.json",
462+
{
463+
"id": request["id"],
464+
"pid": os.getpid(),
465+
"started": time.time(),
466+
"time_budget_seconds": 180,
467+
"phase": "processing",
468+
},
469+
)
443470
try:
444471
response = (
445472
respond(root, directory, request, accepted_cpu)
@@ -448,17 +475,23 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
448475
)
449476
with connect(root) as db:
450477
db.execute(
451-
"UPDATE requests SET state='completed',response=?,error=NULL,updated=? WHERE id=?",
478+
"UPDATE requests SET state='completed',response=?,error=NULL,updated=? WHERE id=? AND state='queued'",
452479
(json.dumps(response, ensure_ascii=False), time.time(), request["id"]),
453480
)
454481
ActivityLog(root, "controller", "chat").write(
455482
"decisions", "user-command-completed", {"id": request["id"], **response}
456483
)
457484
except (requests.ConnectionError, requests.Timeout) as error:
485+
expired = time.monotonic() >= request["deadline"] or isinstance(error, requests.Timeout)
458486
with connect(root) as db:
459487
db.execute(
460-
"UPDATE requests SET attempts=attempts+1,error=?,updated=? WHERE id=?",
461-
(str(error)[:300], time.time(), request["id"]),
488+
"UPDATE requests SET state=?,attempts=attempts+1,error=?,updated=? WHERE id=? AND state='queued'",
489+
(
490+
"failed" if expired else "queued",
491+
str(error)[:300],
492+
time.time(),
493+
request["id"],
494+
),
462495
)
463496
except Exception as error:
464497
with connect(root) as db:
@@ -469,6 +502,8 @@ def service_loop(root: Path, directory: Path, stop: threading.Event, accepted_cp
469502
ActivityLog(root, "controller", "chat").write(
470503
"errors", "user-command-failed", {"id": request["id"], "error": str(error)[:500]}
471504
)
505+
finally:
506+
atomic_json(root / "research/state/chat-active.json", {"phase": "idle"})
472507

473508

474509
@contextmanager

‎rlm/v100/remote_helper.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,13 @@ def workload_slot(self, wait: bool = True):
180180
if type(wait) is not bool:
181181
raise ValueError("Helper wait must be boolean")
182182
with ExitStack() as scope:
183-
if not HELPER_WORKLOAD_LOCK.acquire(blocking=wait):
183+
deadline_bound = getattr(self, "request_deadline", None)
184+
acquired = (
185+
HELPER_WORKLOAD_LOCK.acquire(timeout=self.remaining_timeout())
186+
if wait and deadline_bound is not None
187+
else HELPER_WORKLOAD_LOCK.acquire(blocking=wait)
188+
)
189+
if not acquired:
184190
raise TimeoutError("Helper busy; optional work deferred")
185191
scope.callback(HELPER_WORKLOAD_LOCK.release)
186192
path = None
@@ -194,7 +200,18 @@ def workload_slot(self, wait: bool = True):
194200
path.parent.mkdir(parents=True, exist_ok=True)
195201
lease = scope.enter_context(path.with_suffix(".lock").open("a"))
196202
try:
197-
fcntl.flock(lease, fcntl.LOCK_EX | (0 if wait else fcntl.LOCK_NB))
203+
while True:
204+
try:
205+
fcntl.flock(
206+
lease,
207+
fcntl.LOCK_EX
208+
| (fcntl.LOCK_NB if not wait or deadline_bound else 0),
209+
)
210+
break
211+
except BlockingIOError:
212+
if not wait:
213+
raise
214+
time.sleep(min(0.1, self.remaining_timeout()))
198215
except BlockingIOError as error:
199216
raise TimeoutError("Helper lease busy; optional work deferred") from error
200217
state = json.loads(path.read_text()) if path.exists() else {}
@@ -213,6 +230,7 @@ def workload_slot(self, wait: bool = True):
213230
if not wait and started < deadline:
214231
raise TimeoutError("Helper cooling down; optional work deferred")
215232
while time.monotonic() < deadline:
233+
self.remaining_timeout()
216234
time.sleep(max(0, min(1, deadline - time.monotonic())))
217235
quota = {
218236
"path": path,
@@ -255,7 +273,7 @@ def remote_request(self, endpoint: str, data: dict | None = None) -> dict:
255273
arguments = {} if data is None else {"json": data}
256274
with method(
257275
self.base_url + endpoint,
258-
timeout=(5, self.timeout),
276+
timeout=(min(5, self.remaining_timeout()), self.remaining_timeout()),
259277
allow_redirects=False,
260278
stream=True,
261279
**arguments,

‎rlm/v100/research_policy.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def choose(root: Path, target: str, thinking: bool, max_tokens: int, batch_token
5050

5151
def apply(client, root: Path):
5252
result = copy.copy(client)
53+
if getattr(client, "activity_actor", "").startswith("chat"):
54+
# Interactive latency limits must not inherit long research thinking/output budgets.
55+
return result
5356
if getattr(client, "research_device", None) == "cpu":
5457
return result
5558
target = "helper" if hasattr(result, "helper_batch_tokens") else "master"

‎tests/test_v100_chat_backend.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def test_wrong_cpu_endpoint_is_not_reused(tmp_path, monkeypatch):
113113

114114
def test_chat_selects_same_accepted_master_during_gpu_training(tmp_path, monkeypatch):
115115
accepted, cpu, _ = admitted(tmp_path, monkeypatch)
116+
cpu.timeout = 600
117+
cpu.sampling_args = {"max_tokens": 1024}
118+
cpu.activity_context = {}
116119
atomic_json(tmp_path / "serving-active.json", accepted)
117120
monkeypatch.setattr(
118121
mission,

‎tests/test_v100_mission_evidence.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,13 @@ 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-
client = SimpleNamespace(request=lambda *args: {"model_path": str(model)}, model_name="v100")
150+
client = SimpleNamespace(
151+
request=lambda *args: {"model_path": str(model)},
152+
model_name="v100",
153+
timeout=120,
154+
sampling_args={"max_tokens": 2048},
155+
activity_context={},
156+
)
151157
monkeypatch.setattr(competition, "helper_client", lambda *args: client)
152158
seen = []
153159

‎tests/test_v100_remote_helper.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,3 +834,40 @@ def get(self, url, **kwargs):
834834
client().remote_request("/api/tags")
835835
with pytest.raises(ValueError, match="management"):
836836
client().remote_request("/api/pull", {"model": remote_helper.MODEL})
837+
838+
839+
def test_chat_deadline_expires_before_any_http_request(monkeypatch):
840+
instance = LlamaCppClient(timeout=600)
841+
instance.request_deadline = 10
842+
monkeypatch.setattr(remote_helper.time, "monotonic", lambda: 11)
843+
with pytest.raises(requests.Timeout, match="total time budget"):
844+
instance.http_request("/props")
845+
846+
847+
def test_chat_deadline_bounds_timeout_and_helper_cooldown(transport, tmp_path, monkeypatch):
848+
instance = client(activity_root=str(tmp_path))
849+
clock = {"now": 0.0}
850+
monkeypatch.setattr(remote_helper.time, "monotonic", lambda: clock["now"])
851+
monkeypatch.setattr(
852+
remote_helper.time, "sleep", lambda seconds: clock.update(now=clock["now"] + seconds)
853+
)
854+
with instance.workload_slot() as quota:
855+
instance.reserve_helper_idle(quota, 600)
856+
instance.request_deadline = 3
857+
assert instance.remaining_timeout() == min(instance.timeout, 3)
858+
with pytest.raises(requests.Timeout, match="total time budget"):
859+
with instance.workload_slot():
860+
pytest.fail("Cooldown outlived the chat deadline")
861+
assert clock["now"] == 3
862+
assert not transport[0]
863+
864+
865+
def test_chat_does_not_inherit_research_thinking_budget(tmp_path):
866+
from rlm.v100.research_policy import apply, choose
867+
868+
choose(tmp_path, "master", True, 8192, 512)
869+
instance = LlamaCppClient(sampling_args={"max_tokens": 2048}, enable_thinking=False)
870+
instance.activity_actor = "chat"
871+
actual = apply(instance, tmp_path)
872+
assert actual.enable_thinking is False
873+
assert actual.sampling_args["max_tokens"] == 2048

‎tests/test_v100_upgrade_audit.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,26 @@ def server(*args):
565565
)
566566
assert verdict["passed"] is improves
567567
assert (selected is source) is not improves
568+
569+
570+
def test_chat_timeout_is_terminal_and_clears_processing_marker(tmp_path, monkeypatch):
571+
import requests
572+
573+
from rlm.v100 import mission_chat
574+
575+
identity = mission_chat.submit(tmp_path, "Edit the dashboard")
576+
stop = threading.Event()
577+
578+
def reply(root, directory, request):
579+
active = json.loads((root / "research/state/chat-active.json").read_text())
580+
assert active["id"] == identity and active["phase"] == "processing"
581+
stop.set()
582+
raise requests.Timeout("Read timed out")
583+
584+
monkeypatch.setattr(mission_chat, "respond", reply)
585+
mission_chat.service(tmp_path, tmp_path, stop)
586+
actual = mission_chat.inspect(tmp_path, identity)
587+
assert actual["state"] == "failed" and actual["attempts"] == 1
588+
assert json.loads((tmp_path / "research/state/chat-active.json").read_text()) == {
589+
"phase": "idle"
590+
}

0 commit comments

Comments
 (0)