diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a5dc15758..69655b6ec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -117,7 +117,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de ├── vision_routing.py ← Send-time image routing SSOT: inline vision vs generic captions vs placeholders on a per-send message copy (`OUROBOROS_IMAGE_INPUT_MODE`, `OUROBOROS_MODEL_VISION`) ├── fallback_cooldown.py ← Per-process 429-aware cooldown for the `OUROBOROS_MODEL_FALLBACKS` chain: a transiently-failed model is parked for a short window so fallback walks and repeated rounds skip it; advisory, default-on, fail-soft, passive heal; per-process only — honestly not a swarm-wide governor ├── model_concurrency.py ← Per-(model,use_local) `BoundedSemaphore` capping concurrent provider calls (`OUROBOROS_MODEL_MAX_CONCURRENCY`, default 3) so a task's own loop + subagent threads + status pings cannot self-DoS one model's rate limit; excess threads WAIT deadline-bounded; wraps only the provider call in `loop_llm_call.call_llm_with_retry`; per-process only - ├── project_naming.py ← SSOT for LLM-first project naming: bounded light-model title with deterministic fallback, shared by the proactive card namer (supervisor/worker_chat_lane.py), turn-into-project conversion (gateway/projects.py), and `ensure_project_scope`; the provider call goes through the model_concurrency slot + ├── project_naming.py ← SSOT for LLM-first project naming: bounded light-model title with deterministic fallback, shared by admission naming (`admission_names`: headless runs and chat promotion, no model call), turn-into-project conversion (gateway/projects.py), and `ensure_project_scope` — a direct conversation turn is never named; the provider call goes through the model_concurrency slot ├── loop_tool_execution.py ← Tool dispatch and tool-result handling ├── deadline_utils.py ← Shared deadline parsing/remaining-time helpers + the transport-vs-logical wait seam for loop milestones and process-tool/review timeouts ├── observability.py ← Private forensic execution ledger: redaction, gzip CAS blobs, call manifests, trace refs diff --git a/ouroboros/post_task_checkpoint.py b/ouroboros/post_task_checkpoint.py index 470b06620..3f26f2400 100644 --- a/ouroboros/post_task_checkpoint.py +++ b/ouroboros/post_task_checkpoint.py @@ -232,8 +232,8 @@ def project_root_post_task_checkpoint_fields( The root writer owns only post-task synthesis and its accounting snapshot; acceptance remains whatever the current record says. Once post-task state is terminal, an open or different-terminal stale patch cannot replace that - state or its accounting. A same-terminal patch remains valid so the - proactive namer's explicit ``refresh`` can update the final cost snapshot. + state or its accounting. A same-terminal patch remains valid so an explicit + ``refresh`` can update the final cost snapshot. """ overlay = dict(patch_fields) if canonical_fields.get("status"): @@ -316,8 +316,8 @@ def set_root_post_task_checkpoint( return authority_root = roots[0] finalized_event: Dict[str, Any] | None = None - # The proactive namer can settle concurrently with post-task synthesis. A - # shared critical section makes its refresh and the final snapshot linear. + # A late cost refresh can settle concurrently with post-task synthesis. A + # shared critical section makes that refresh and the final snapshot linear. with POST_TASK_SYNTHESIS_LOCK: existing = load_task_result(authority_root, task_id) or {} checkpoint = existing.get("root_phase_checkpoint") diff --git a/ouroboros/project_naming.py b/ouroboros/project_naming.py index 11c49e673..d7aa0a4b8 100644 --- a/ouroboros/project_naming.py +++ b/ouroboros/project_naming.py @@ -3,10 +3,11 @@ An LLM-first short human title for a project card, with a deterministic heuristic fallback. Shared by every path that names a project so the UI conversion and the agent never drift: - - the proactive card namer (names ANY task card up front, supervisor side); - - ``gateway/projects.py`` turn-into-project conversion (reuses the up-front name, + - ``gateway/projects.py`` turn-into-project conversion (reuses an admission name, or names inline as a race fallback); - - ``ensure_project_scope`` (the agent self-creates + names a project). + - ``ensure_project_scope`` (the agent self-creates + names a project); + - ``admission_names`` (headless runs and chat promotion, no model call). +A direct conversation turn is never named: it renders as an activity block. Doctrine: - P5 LLM-first: the model COINS the name; post-processing is purely lexical @@ -19,11 +20,8 @@ Doctrine: from __future__ import annotations import logging -import pathlib -import threading from dataclasses import replace -from typing import Any, Callable, Dict, Optional, Sequence -import contextvars +from typing import Any, Dict, Optional, Sequence log = logging.getLogger("ouroboros.project_naming") @@ -271,134 +269,6 @@ async def llm_project_name_async( return fb -def _refresh_root_cost_after_naming(drive_root: Any, task_id: str) -> None: - """Refresh a terminal root projection after the naming attempt settles.""" - try: - from types import SimpleNamespace - - from ouroboros.agent_task_pipeline import _set_root_post_task_checkpoint - from ouroboros.task_results import load_task_result - - current = load_task_result(drive_root, task_id) or {} - refreshed = {**current, "id": task_id, "budget_drive_root": str(drive_root)} - _set_root_post_task_checkpoint( - SimpleNamespace(drive_root=pathlib.Path(drive_root)), refreshed, "refresh", - ) - except Exception: - log.debug("project naming cost refresh failed for %s", task_id, exc_info=True) - - -def spawn_proactive_namer( - drive_root: Any, task_id: str, text: str, *, broadcast: Optional[Callable[[dict], None]] = None, -) -> None: - """Proactively coin an LLM project name for a fresh card in a DAEMON thread (Cluster B). - - Writes the coined ``suggested_name`` onto the task result (turn-into-project then reuses - it with zero extra call) and, via ``broadcast``, emits a ``task_named`` event so the live - card shows a human title up front. NEVER blocks the task. ``drive_root`` is captured at - CALL time — NOT read from a mutable module global at thread-execution time — so a later - context switch (or a test that swaps the supervisor drive) can't redirect this thread's - write. Skips cleanly unless ``drive_root`` is a real directory (test safety: a stub / - MagicMock drive must never materialise a stray path — chat_observed persists BEFORE the - LLM call). Fail-soft.""" - from ouroboros.settings_integrity import copy_task_settings_context - - body = " ".join(str(text or "").split()) - if not body: - return - try: - if not pathlib.Path(str(drive_root)).is_dir(): - return - except (OSError, TypeError, ValueError): - return - - def _work() -> None: - try: - # v6.58.0 (§3.4b): HARD total wall-clock bound. The transport timeout bounds - # ONE attempt, but llm.chat's retry/fallback chain under a degraded provider - # could stretch the whole call to tens of minutes (the incident where the - # card was named 24 minutes late). A title is cosmetic: if it hasn't landed - # within the transport budget + slack, drop it — the id/title heuristics and - # the convert path's own bounded inline call (8s) already cover naming. - _result: list[str] = [] - _detached = threading.Event() - _finished = threading.Event() - _refresh_lock = threading.Lock() - _refreshed = False - - def _refresh_detached_once() -> None: - nonlocal _refreshed - with _refresh_lock: - if _refreshed: - return - _refreshed = True - _refresh_root_cost_after_naming(drive_root, task_id) - - def _call() -> None: - try: - _result.append(llm_project_name(body, drive_root=drive_root, task_id=task_id)) - except Exception: - log.debug("proactive namer inner call failed for %s", task_id, exc_info=True) - finally: - _finished.set() - if _detached.is_set(): - _refresh_detached_once() - - settings_context = contextvars.Context() - copy_task_settings_context(settings_context) - inner = threading.Thread(target=settings_context.run, args=(_call,), name=f"namer-call-{task_id}", daemon=True) - inner.start() - if not _finished.wait(timeout=max(0.0, _naming_timeout_sec() + 30.0)): - _detached.set() - # Close the race where settlement lands between wait() and - # the detached marker. The once-guard covers both interleavings. - if _finished.is_set(): - _refresh_detached_once() - log.debug("proactive namer exceeded its wall-clock bound for %s; skipped", task_id) - return - inner.join() - if not _result: - log.debug("proactive namer exceeded its wall-clock bound for %s; skipped", task_id) - return - name = _result[0] - if not name: - return - from ouroboros.task_results import ( - STATUS_RUNNING, - load_task_result, - write_task_result, - ) - - # Persist suggested_name as same-status ENRICHMENT, not a RUNNING transition: a - # fast task may already be terminal (completed/failed/cancelled) by the time this - # daemon finishes, and write_task_result's monotonic guard DROPS a regressing - # RUNNING write — which would silently lose the name the convert path reuses. - # Writing under the current on-disk status lets the monotonic guard's same-status - # enrichment carry the field through (and a benign drop only in the rare race where - # the status advanced past our read — acceptable for a best-effort title). - current = load_task_result(drive_root, task_id) or {} - status = str(current.get("status") or "") or STATUS_RUNNING - write_task_result(drive_root, task_id, status, suggested_name=name) - # A cosmetic namer may settle concurrently with or after the ordinary - # post-task worker. The shared refresh/checkpoint critical section - # linearizes both cases without marking an unfinished phase complete. - _refresh_root_cost_after_naming(drive_root, task_id) - if broadcast is not None: - try: - broadcast({"type": "task_named", "task_id": task_id, "suggested_name": name}) - except Exception: - log.debug("task_named broadcast failed for %s", task_id, exc_info=True) - except Exception: - log.debug("proactive namer failed for %s", task_id, exc_info=True) - - try: - settings_context = contextvars.Context() - copy_task_settings_context(settings_context) - threading.Thread(target=settings_context.run, args=(_work,), name=f"namer-{task_id}", daemon=True).start() - except Exception: - log.debug("proactive namer thread spawn failed for %s", task_id, exc_info=True) - - def admission_names(body: Dict[str, Any], description: str) -> tuple: """The run's owner-facing name at admission: ``(title, suggested_name)``. diff --git a/ouroboros/tools/control_delegation.py b/ouroboros/tools/control_delegation.py index 22a720564..5407ebc9d 100644 --- a/ouroboros/tools/control_delegation.py +++ b/ouroboros/tools/control_delegation.py @@ -868,7 +868,7 @@ def _ensure_project_scope(ctx: ToolContext, project_name: str = "", project_id: ) from ouroboros.project_naming import clean_model_title - # Run the agent-supplied name through the SAME lexical cleaner the proactive namer and + # Run the agent-supplied name through the SAME lexical cleaner admission naming and # turn-into-project conversion use (project_naming SSOT) so every project-naming path # produces consistent titles (quote/emoji strip, length cap); fall back to the raw value. display_name = clean_model_title(project_name) or str(project_name or "").strip() diff --git a/supervisor/worker_chat_lane.py b/supervisor/worker_chat_lane.py index a21d72546..b2aebc756 100644 --- a/supervisor/worker_chat_lane.py +++ b/supervisor/worker_chat_lane.py @@ -187,7 +187,7 @@ def _host_operation_failure(metadata: Optional[dict]) -> dict: def _broadcast_task_named(msg: dict) -> None: - """Bridge broadcast callback for the proactive namer (kept tiny + fail-soft).""" + """Bridge broadcast callback for admission naming (kept tiny + fail-soft).""" try: from supervisor.message_bus import get_bridge @@ -345,15 +345,10 @@ def _run_chat_task( _pool()._report_binding_failure(task["id"], pid, exc, path="direct_project_turn") if not task["text"]: task["text"] = "(image attached)" if image_data else "" - # Cluster B: proactively coin a project name for a fresh MAIN-CHAT direct card - # (not an already-bound project-thread task) so - # the card shows a human title up front and turn-into-project reuses it. - if not task.get("project_id"): - from ouroboros.project_naming import spawn_proactive_namer - - spawn_proactive_namer( - _pool().DRIVE_ROOT, str(task["id"]), task["text"], broadcast=_broadcast_task_named - ) + # A direct turn is not named: it renders as an activity block, never as + # a titled task card, and joins a Project only through the model's own + # scope tools (owner decision 14=A). Managed promotes keep their + # admission names (worker_promotion._admitted_suggested_name). attach_task_contract(task) # Announce the authoritative start immediately (owner decision 2A): diff --git a/tests/test_attachment_staging.py b/tests/test_attachment_staging.py index 19c7899f5..33e74e77a 100644 --- a/tests/test_attachment_staging.py +++ b/tests/test_attachment_staging.py @@ -529,10 +529,6 @@ class TestDesktopChatFullSetStaging: # isolation precedent as tests/test_inflight_indicator_seams.py:: # _patch_workers). monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue()) - # Avoid the proactive namer spinning a real thread/LLM in this unit test - # (it is a local `from ouroboros.project_naming import ...`, so patch source). - import ouroboros.project_naming as project_naming - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None) # Two uploads on disk: an image and a non-image PDF. img_src = tmp_path / "photo.png" @@ -586,8 +582,6 @@ class TestDesktopChatFullSetStaging: # lifespan in this xdist worker must not abort the turn before # handle_task. monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue()) - import ouroboros.project_naming as project_naming - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None) b64 = base64.b64encode(_PNG_BYTES).decode("ascii") agent = _FakeChatAgent() diff --git a/tests/test_cancel_protocol_inventory_s6.py b/tests/test_cancel_protocol_inventory_s6.py index ee1862151..5a13d2c0f 100644 --- a/tests/test_cancel_protocol_inventory_s6.py +++ b/tests/test_cancel_protocol_inventory_s6.py @@ -78,7 +78,6 @@ TERMINAL_WRITERS = { ('ouroboros/post_task_checkpoint.py::set_root_post_task_checkpoint', 'str(existing.get("status") or task.get("status") or STATUS_COMPLETED)'): 'terminal', ('ouroboros/project_dialogue.py::_append_terminal_task_projection', 'status'): 'dynamic', ('ouroboros/project_dialogue.py::persist_continuation_narrative', 'requested_status'): 'dynamic', - ('ouroboros/project_naming.py::spawn_proactive_namer._work', 'status'): 'dynamic', # The locked field projector preserves the existing status, including a # terminal one; publishing review evidence never completes the task itself. ('ouroboros/review_projection.py::publish_acceptance_checkpoint', '"running"'): 'dynamic', diff --git a/tests/test_headless_task_title.py b/tests/test_headless_task_title.py index 5db00c3dd..ed301d225 100644 --- a/tests/test_headless_task_title.py +++ b/tests/test_headless_task_title.py @@ -125,10 +125,10 @@ def test_a_title_hidden_in_metadata_is_refused_like_a_project_id(admission): def test_a_derived_name_is_not_reported_as_model_coined(): """Turn-into-project reuses the name slot; it must not claim authorship. - Two producers fill `suggested_name`: the proactive namer coins one with a - model, and headless admission derives one from the request's first line. - The conversion cannot tell them apart, so its naming reason names the SLOT - it read rather than a coiner that may not exist. + `suggested_name` is filled by admission naming (a caller title, or the + request's first line) and by the agent's own scope tools; the conversion + cannot tell them apart, so its naming reason names the SLOT it read rather + than a coiner that may not exist. """ import pathlib diff --git a/tests/test_host_service_operation_identity.py b/tests/test_host_service_operation_identity.py index a1948af58..aa898c50a 100644 --- a/tests/test_host_service_operation_identity.py +++ b/tests/test_host_service_operation_identity.py @@ -126,7 +126,6 @@ def test_ordinary_main_and_transport_refusals_keep_their_original_envelope(monke @pytest.mark.parametrize("host_operation", [False, True]) def test_chat_crash_preserves_only_the_host_accepted_operation(tmp_path, monkeypatch, host_operation): from queue import SimpleQueue - from ouroboros import project_naming from supervisor import worker_chat_lane, workers class CrashingAgent: @@ -140,7 +139,6 @@ def test_chat_crash_preserves_only_the_host_accepted_operation(tmp_path, monkeyp monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path) monkeypatch.setattr(workers, "get_event_q", SimpleQueue) monkeypatch.setattr(workers, "send_with_budget", message_bus.send_with_budget) - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None) client = _client(tmp_path, bridge) assert client.post("/chat/inject", headers=_headers(), json={ "chat_id": CHAT, "client_message_id": MSG, "text": "do work", diff --git a/tests/test_inflight_indicator_seams.py b/tests/test_inflight_indicator_seams.py index ae6d76250..ca93913f2 100644 --- a/tests/test_inflight_indicator_seams.py +++ b/tests/test_inflight_indicator_seams.py @@ -64,9 +64,6 @@ def _patch_workers(monkeypatch, tmp_path): # Windows CI shard; same isolation precedent as # tests/test_promote_event_transport.py::_isolate_event_bus_shutdown_latch). monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue()) - import ouroboros.project_naming as project_naming - - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None) # Capture the turn's start announce (bridge typing frame) instead of # touching the real singleton bridge. bridge_probe = _BridgeProbe() diff --git a/tests/test_native_conversation_activity.py b/tests/test_native_conversation_activity.py index 61c8ebf9d..86ea544d4 100644 --- a/tests/test_native_conversation_activity.py +++ b/tests/test_native_conversation_activity.py @@ -10,7 +10,6 @@ from supervisor.active_activity import get_direct_activity_registry def _lane(monkeypatch, tmp_path): - from ouroboros import project_naming from supervisor import message_bus, state monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path) @@ -19,7 +18,6 @@ def _lane(monkeypatch, tmp_path): monkeypatch.setattr(workers, "send_with_budget", lambda *a, **kw: None) monkeypatch.setattr(state, "load_state", lambda: {}) monkeypatch.setattr(state, "budget_remaining", lambda *a, **kw: 100) - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None) monkeypatch.setattr(message_bus, "get_bridge", lambda: SimpleNamespace(send_chat_action=lambda *a, **kw: None)) workers.open_repo_writer_admission() diff --git a/tests/test_native_model_wait_integration.py b/tests/test_native_model_wait_integration.py index 24817c881..7bb353a96 100644 --- a/tests/test_native_model_wait_integration.py +++ b/tests/test_native_model_wait_integration.py @@ -85,7 +85,7 @@ def test_native_wait_controls_and_manual_restart_address_all_registered_actors(t @pytest.mark.parametrize("project_id", ["", "room"]) @pytest.mark.parametrize("action", ["switch", "stop", "stop_settled"]) def test_native_post_task_wait_remains_addressable_after_dialogue_closes(phase, monkeypatch, project_id, action): - from ouroboros import agent_task_pipeline as pipeline, project_naming + from ouroboros import agent_task_pipeline as pipeline from ouroboros.gateway.state import _chat_activities_snapshot_safe from ouroboros.post_task_checkpoint import post_task_model_wait from ouroboros.task_results import load_task_result, write_task_result @@ -97,7 +97,6 @@ def test_native_post_task_wait_remains_addressable_after_dialogue_closes(phase, monkeypatch.setattr(workers, "DRIVE_ROOT", f.root) monkeypatch.setattr(workers, "WORKERS", {}) monkeypatch.setattr(workers, "get_event_q", lambda: f.events) - monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None) monkeypatch.setattr(message_bus, "get_bridge", lambda: SimpleNamespace(send_chat_action=lambda *a, **kw: None)) monkeypatch.setattr(task_queue, "DRIVE_ROOT", f.root) monkeypatch.setattr(task_queue, "RUNNING", {}) diff --git a/tests/test_task_result_monotonic.py b/tests/test_task_result_monotonic.py index 227a6ddc6..6ffc8e206 100644 --- a/tests/test_task_result_monotonic.py +++ b/tests/test_task_result_monotonic.py @@ -217,102 +217,6 @@ def test_read_paths_do_not_create_task_results_dir(tmp_path): assert (root / "task_results").is_dir() -def test_proactive_namer_persists_name_on_already_terminal_task(tmp_path, monkeypatch): - """v6.40.0 #1: the proactive namer must persist ``suggested_name`` even when the task - already raced to a terminal status — it enriches under the CURRENT status instead of a - regressing RUNNING write (which the monotonic guard would drop, losing the convert-reuse - name).""" - import threading - import time - - from ouroboros import project_naming - - tr.write_task_result(tmp_path, "t", tr.STATUS_COMPLETED, result="fast done") - monkeypatch.setattr(project_naming, "llm_project_name", lambda *a, **k: "Nice Title") - project_naming.spawn_proactive_namer(tmp_path, "t", "build me a thing") - for _ in range(100): # join the daemon namer thread (best-effort, bounded) - if not any(th.name == "namer-t" for th in threading.enumerate()): - break - time.sleep(0.02) - r = tr.load_task_result(tmp_path, "t") - assert r["status"] == tr.STATUS_COMPLETED, "namer must NOT regress a terminal task to running" - assert r.get("suggested_name") == "Nice Title", "suggested_name must survive on a terminal task" - - -def test_proactive_namer_late_settlement_refreshes_cost_without_late_name(tmp_path, monkeypatch): - """A provider thread outliving the cosmetic deadline still closes accounting only.""" - import threading - import time - - from ouroboros import agent_task_pipeline, project_naming, usage_accounting - - tr.write_task_result( - tmp_path, - "late-root", - tr.STATUS_COMPLETED, - root_task_id="late-root", - accounted_upper_bound_usd=0.0, - cost_final=True, - root_phase_checkpoint={"post_task_synthesis": "completed"}, - ) - entered = threading.Event() - release = threading.Event() - - def late_paid_name(*_args, **_kwargs): - entered.set() - assert release.wait(2) - attempt = usage_accounting.reserve_attempt(usage_accounting.AttemptRequest( - model="openai/gpt-5.2", - provider="openai", - reservation_usd=0.25, - drive_root=tmp_path, - task_id="late-root", - root_task_id="late-root", - global_limit_usd=5.0, - root_limit_usd=5.0, - )) - usage_accounting.mark_dispatched(attempt) - usage_accounting.settle_attempt(attempt, {}, cost_usd=0.25, cost_final=True) - return "Too Late" - - monkeypatch.setattr(project_naming, "llm_project_name", late_paid_name) - monkeypatch.setattr(project_naming, "_naming_timeout_sec", lambda: -29.98) - broadcasts = [] - project_naming.spawn_proactive_namer( - tmp_path, "late-root", "build a thing", broadcast=broadcasts.append, - ) - assert entered.wait(1) - for _ in range(100): - if not any(th.name == "namer-late-root" for th in threading.enumerate()): - break - time.sleep(0.01) - assert not any(th.name == "namer-late-root" for th in threading.enumerate()) - - release.set() - # The subject is that the late settlement's refresh LANDS, not how fast: the - # detached thread runs reserve → dispatch → settle → cost reconstruction → - # locked task-result write, each a real lock cycle with fsyncs, and the - # Windows full-test leg took longer than a 2 s poll twice out of five runs - # (d0bb839e, 5fbdabd3) while green on the others. A refresh that never lands - # still fails here — after a bound wide enough for a loaded runner. - started = time.monotonic() - for _ in range(2000): - stored = tr.load_task_result(tmp_path, "late-root") - if stored.get("accounted_upper_bound_usd") == 0.25: - break - time.sleep(0.01) - stored = tr.load_task_result(tmp_path, "late-root") - assert stored["accounted_upper_bound_usd"] == 0.25, f"no refresh after {time.monotonic() - started:.1f}s" - assert stored["accounted_upper_bound_usd_with_children"] == 0.25 - assert stored["cost_final"] is True - assert stored.get("suggested_name") is None - assert broadcasts == [] - assert agent_task_pipeline._root_post_task_already_completed( - type("Env", (), {"drive_root": tmp_path})(), - {"id": "late-root", "root_task_id": "late-root"}, - ) - - def test_read_with_stub_root_leaks_no_cwd_dir(tmp_path, monkeypatch): """The exact pollution repro: a MagicMock-derived root (``MagicMock/mock``) reaching a READ scan must not create a ``MagicMock`` tree in the cwd.""" diff --git a/web/modules/chat.js b/web/modules/chat.js index e7bfd89d5..f52141f94 100644 --- a/web/modules/chat.js +++ b/web/modules/chat.js @@ -3745,10 +3745,10 @@ export function createChatInstance({ withRemoteActivity(() => updateLiveCardFromLogEvent(msg.data)); }); - // the proactive namer coined a project name for a fresh card — show it - // as the card title up front (turn-into-project then reuses the same name). Not - // thread-gated on chat_id: the broadcast carries only task_id, and applySuggestedName - // no-ops unless THIS thread already holds that card. + // Admission naming (a promoted root, a headless run) coined a name for a + // managed card — show it as the card title up front (turn-into-project then + // reuses the same name). Not thread-gated on chat_id: the broadcast carries + // only task_id, and applySuggestedName no-ops unless THIS thread holds that card. onWs('task_named', (msg) => { withRemoteActivity( () => applySuggestedName(msg?.task_id || '', msg?.suggested_name || ''),