Skip to content

Commit 26baa45

Browse files
fix: harden dispatch id validation and priority docs
1 parent 77eed93 commit 26baa45

3 files changed

Lines changed: 26 additions & 7 deletions

File tree

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ The UI is a **hash-routed** SPA with ES modules under `static/js/`:
105105

106106
- `app.js` — routing and boot
107107
- `projects.js`, `sessions.js`, `search.js`, `export.js` — route handlers
108-
- `render/registry.js`**tool dispatch registry** for session UI: `TOOL_USE_RENDERERS` and `TOOL_RESULT_RENDERERS` map tool name / `result_type` → render function (one module per type under `render/tool_use/` and `render/tool_result/`). Parallels backend `utils/tool_dispatch.py` (backend uses ordered predicates; frontend uses direct key lookup + fallback).
108+
- `render/registry.js`**tool dispatch registry** for session UI: `TOOL_USE_RENDERERS` and `TOOL_RESULT_RENDERERS` map tool name / `result_type` → render function (one module per type under `render/tool_use/` and `render/tool_result/`). Parallels backend `utils/tool_dispatch.py` (backend uses priority-based predicate matching; frontend uses direct key lookup + fallback).
109109
- `static/tool_types.json` — generated manifest of backend tool-use names (`python scripts/gen_tool_types_manifest.py` from `KNOWN_TOOL_TYPES`). Fetched non-blocking at boot by `render/tool_types_manifest.js` (`void initToolTypesManifest()` in `app.js`), which cross-checks `TOOL_USE_RENDERERS` and logs `console.warn` on drift.
110110
- `shared/markdown.js` — markdown + **DOMPurify** sanitization (do not render raw LLM HTML)
111111
- `shared/state.js`, `shared/utils.js`, `shared/theme.js` — shared UI state and helpers

tests/test_real_session_fixtures.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,12 @@ def test_task_retrieval_not_misclassified_as_task_message() -> None:
142142

143143

144144
def test_task_completed_with_message_key_matches_task_message_first() -> None:
145-
"""Legacy dispatch: broad task_message runs before task_completed when ``message`` present.
145+
"""task_message outranks task_completed when ``message`` key is present.
146146
147147
``is_task_message_tool_result`` matches any dict with a ``message`` or ``task_id``
148-
key. Future tool shapes that add ``message`` for status text (e.g. web-fetch) would
149-
be misclassified as task until dispatch order is refined — this test locks that
148+
key and holds priority 1, beating the narrower task predicates at priority 0.
149+
Future tool shapes that add ``message`` for status text (e.g. web-fetch) would
150+
be misclassified as task unless given higher priority — this test locks that
150151
known false-positive surface.
151152
"""
152153
tr = {

utils/tool_dispatch.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ def _tool_result_build_task_retrieval(
187187
tr: ToolResultDict, base: dict[str, object]
188188
) -> dict[str, object]:
189189
result = dict(base)
190-
task_obj = tr["task"] if isinstance(tr["task"], dict) else {}
190+
raw_task = tr.get("task")
191+
task_obj = raw_task if isinstance(raw_task, dict) else {}
191192
result["result_type"] = "task"
192193
result["retrieval_status"] = tr.get("retrieval_status")
193194
result["task_id"] = task_obj.get("task_id")
@@ -266,7 +267,23 @@ def _tool_result_build_user_input(tr: ToolResultDict, base: dict[str, object]) -
266267
ToolResultDispatchEntry("user_input", is_user_input_tool_result, _tool_result_build_user_input),
267268
)
268269

269-
_DISPATCH_ORDER = {entry.id: index for index, entry in enumerate(_TOOL_RESULT_DISPATCH)}
270+
271+
def _validate_dispatch_ids(
272+
table: tuple[ToolResultDispatchEntry, ...],
273+
) -> dict[str, int]:
274+
"""Fail fast on duplicate entry IDs and return the registration-order index."""
275+
order: dict[str, int] = {}
276+
for index, entry in enumerate(table):
277+
if entry.id in order:
278+
raise ValueError(
279+
f"duplicate ToolResultDispatchEntry id {entry.id!r} "
280+
f"(indices {order[entry.id]} and {index})"
281+
)
282+
order[entry.id] = index
283+
return order
284+
285+
286+
_DISPATCH_ORDER = _validate_dispatch_ids(_TOOL_RESULT_DISPATCH)
270287

271288
# Claude Code assistant tool_use ``name`` values coordinated across parser file
272289
# activity, Markdown export, and the SPA ``TOOL_USE_RENDERERS`` map.
@@ -343,9 +360,10 @@ def _winning_dispatch_entry(tr: ToolResultDict) -> ToolResultDispatchEntry | Non
343360
matches = _matching_dispatch_entries(tr)
344361
if not matches:
345362
return None
363+
order = _validate_dispatch_ids(_TOOL_RESULT_DISPATCH)
346364
return max(
347365
matches,
348-
key=lambda entry: (entry.priority, -_DISPATCH_ORDER[entry.id]),
366+
key=lambda entry: (entry.priority, -order[entry.id]),
349367
)
350368

351369

0 commit comments

Comments
 (0)