Skip to content

Playlist usability update#17

Merged
KerseyFabrications merged 5 commits into
mainfrom
playlist_usability_update
Jun 8, 2026
Merged

Playlist usability update#17
KerseyFabrications merged 5 commits into
mainfrom
playlist_usability_update

Conversation

@KerseyFabrications

@KerseyFabrications KerseyFabrications commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Music playlist tooling + array params, interpolation fix, and calculator exact mode
✨ Enhancement 🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

User Description

Make the music tool usable for building playlists

Why

A real session (conv 811) exposed that the music tool couldn't support the workflow an LLM naturally reaches for. Asked to "build a diverse 80s playlist, then add more artists," Friday hit wall after wall — and each failed attempt was a spec for a missing affordance:

  • No append. Every play wiped the queue, so "add to the existing playlist" was impossible — even though the WebUI queue backend already supported add/remove/clear (only the browser could reach it).
  • No track lists. She jammed 30 "Artist Title" pairs into one query string → one substring search → "No music found."
  • No batch search. She fanned out ~48 individual searches because there was no multi-query.
  • Blind shuffle. shuffle was a toggle; she wanted "on" and couldn't deterministically get there, and queue state was invisible between calls.
  • Plan-executor bug. The tool docs said use {{var}} interpolation, but the engine only handled $var — so her loop emitted literal {{artist}}.

What changed

New LLM-facing music actions (WebUI):

  • enqueue — append to the queue without interrupting playback
  • play / enqueue accept an items[] list of track names or paths
  • search accepts an items[] batch and returns each result's path (so the LLM can enqueue exact tracks)
  • clear / remove queue editing
  • deterministic shuffle on|off / repeat none|all|one (no-arg reports current state)
  • a one-line state footer ([queue: N | now #X … | shuffle … | repeat …]) on every mutating result, so the model always knows the state it's acting on

Framework + infra:

  • new TOOL_PARAM_TYPE_ARRAY param type — the LLM emits a native JSON array, delivered through the existing arg-packing via a terminal-slot tail extractor; a registration guard enforces "array param must be last"
  • plan_interpolate() now supports {{var}} / {{var.field}} (keeps $var)
  • split the oversized webui_music_handlers.c into a new webui_music_tool.c, and extracted shared transport-free queue helpers used by both the browser and LLM paths
  • relevance ranking (music_db_pick_best_match) so a bare/generic title resolves to the right track (e.g. "Africa" → Toto, not the alphabetical top row)
  • description rewrite documenting one-field search semantics + a themed-playlist recipe

Parity: full feature set on WebUI; the local-speaker path gets the cheap wins (play items[], batch search) plus honest "available on the web interface" fallbacks for queue editing it has no model for.

Behavior fixes from live testing

  • conv 812: end-to-end playlist build verified — play items[] (27/27 resolved) and enqueue items[] appended 27→46 with no wipe.
  • conv 813: bare "Africa" was queuing the Bo Burnham track → fixed by relevance ranking.
  • conv 814: WebUI enqueue was auto-starting playback when idle → removed; "add to queue" now just adds (local play still autoplays for voice).

Testing

  • 67/67 CI tests pass; zero warnings; format clean
  • new unit coverage: array-param schema + terminal-slot extraction + registration guard (test_tool_registry), {{var}} interpolation incl. dot-access (test_plan_executor), and the pure relevance-ranker incl. the conv-813 regression (test_music_db)
  • reviewed by architecture / efficiency / security agents; findings applied (batch-search DoS cap, over-cap reporting, path-validation defense-in-depth, stack trimming)

Addendum — external review pass (Copilot + Qodo)

Two follow-up commits address findings from the automated reviews. All six were real and triaged on merit (none skipped as "pre-existing").

Music tooling (commit 3):

  • Resolvers now accept exact library paths from search results — WebUI gates on webui_music_is_path_valid (covers local and plex: paths), local adds an absolute-path music_db_get_by_path fast path. Fixes the "search → enqueue returned paths" workflow for Plex-backed libraries and the local backend.
  • tool_param_extract_custom_tail/_custom reject out_len == 0 (a SIZE_MAX underflow would otherwise drive a huge memcpy). +1 unit test.
  • append_state_footer + list snapshot queue_index under state_mutex (nested in queue_mutex) instead of reading it racily.
  • webui_music_execute_tool uses SUCCESS/FAILURE instead of raw 0/1.

Adjacent safety fixes (commit 4) — unrelated subsystems, folded in rather than deferred:

  • build_system_instructions_to_buffer: len += snprintf(...) could pass buffer_size on truncation (snprintf returns would-have-written), making later buffer+len/size-len writes go out of bounds. Added a clamping append helper + bounded final memcpy.
  • TTS no longer logs fully preprocessed user text at INFO on every request / streaming chunk (→ DEBUG); reduces log noise and user-content exposure.

Verified: 67/67 CI, zero warnings, format clean. Commits kept separate (no squash) so the review-fix history stays legible.

Notes / follow-ups

  • Two music backends (local speaker vs WebUI streaming) still maintain parallel queue logic — unifying behind a shared queue core is deferred until local-speaker feature parity or a third consumer justifies it.
  • Unrelated: the satellite build-min is broken on main (commit 6606717 made a public header depend on a src/ui/ header); fix tracked separately in the satellite tree.

Stats: 14 files, +2008 / −576 (the big deletion is the handler split). 2 commits.

AI Description
• Add WebUI music actions for playlist workflows: enqueue, batch search, queue edit, state footer.
• Introduce TOOL_PARAM_TYPE_ARRAY and fix plan interpolation to support {{var}} and dot-access.
• Add calculator exact big-integer mode and TTS large-number reading with new unit coverage.
Diagram
graph TD
LLM["LLM tool loop"] --> PLAN["Plan executor"] --> REG["Tool registry"] --> EXE["Tool exec & schema"]
EXE --> MUSICWEB["WebUI music tool"] --> DB[("Music DB")]
MUSICWEB --> QUEUE[("WebUI queue")]
EXE --> MUSICLOCAL["Local music tool"] --> DB
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Switch tool callbacks to accept structured JSON args
  • ➕ Eliminates the custom "::field::value" packing and the terminal-slot array constraint
  • ➕ Avoids delimiter-escaping issues and reduces truncation risks
  • ➕ Allows multiple array/object params without ordering restrictions
  • ➖ Larger invasive change: tool callback signatures and many existing tools would need refactors
  • ➖ Higher integration risk across local/remote tool execution paths and schema generation
2. Unify WebUI + local queue logic behind a shared queue core
  • ➕ Removes duplicated queue semantics and reduces drift between backends
  • ➕ Makes enqueue/remove/clear/shuffle/repeat naturally available to both backends
  • ➖ Bigger architectural lift; requires defining a shared playback/queue model for local speaker
  • ➖ More concurrency and persistence considerations to get right across consumers

Recommendation: Current approach is a pragmatic incremental step: it delivers LLM-usable playlist building immediately by extending the existing schema + arg-packing pipeline (ARRAY param as serialized JSON tail) and sharing transport-free WebUI queue mutation helpers. Keep this as-is for now; consider migrating to structured JSON callback args only if delimiter-based packing becomes a recurring limitation, and defer queue-core unification until local-speaker parity or a third consumer justifies it.

Grey Divider

File Changes

Enhancement (18)
number_to_words.h TTS API: add decimal number-to-words interface +73/-0

TTS API: add decimal number-to-words interface

• Introduces number_to_words() API, status codes, and buffer sizing guidance for converting large decimal strings to spoken words.

common/include/tts/number_to_words.h


number_to_words.c TTS: implement number_to_words through trigintillion +237/-0

TTS: implement number_to_words through trigintillion

• Implements short-scale grouping (thousands/millions/…/trigintillion), negative handling, and fractional digit-by-digit reading with bounded output writes.

common/src/tts/number_to_words.c


tts_preprocessing.cpp TTS preprocessing: speak very large numbers as words +102/-0

TTS preprocessing: speak very large numbers as words

• Detects long integer tokens (>=13 digits, with optional commas/decimal/sign) and replaces them with number_to_words output; keeps smaller numbers for espeak.

common/src/tts/tts_preprocessing.cpp


music_db.h Music DB API: expose relevance ranking helper +20/-0

Music DB API: expose relevance ranking helper

• Adds music_db_pick_best_match() declaration and documentation to choose the best candidate from alphabetically sorted search results.

include/audio/music_db.h


calculator.h Calculator API: add max expression length and exact-eval helper +27/-0

Calculator API: add max expression length and exact-eval helper

• Defines CALC_MAX_EXPR_LEN stack-safety bound and declares calculator_evaluate_exact_str() for digit-perfect integer results with fallback behavior.

include/tools/calculator.h


calculator_bignum.h Calculator exact mode: public bignum evaluator API +62/-0

Calculator exact mode: public bignum evaluator API

• Adds calculator_evaluate_exact() interface, status codes, and digit cap constants for arbitrary-precision integer expression evaluation.

include/tools/calculator_bignum.h


tool_registry.h Tool registry: add ARRAY param type and extraction helpers +105/-9

Tool registry: add ARRAY param type and extraction helpers

• Introduces TOOL_PARAM_TYPE_ARRAY with a terminal-slot delivery contract, adds tool_param_extract_custom_tail(), and adds metadata for repeatable (non-deterministic) actions plus helpers to query action param name and repeatability.

include/tools/tool_registry.h


llm_command_parser.c LLM instructions: add factorial prose formatting rule +12/-3

LLM instructions: add factorial prose formatting rule

• Adds global output-formatting guidance to write factorial as "N factorial" for TTS correctness, and preserves it even in tools-disabled mode.

src/llm/llm_command_parser.c


llm_tools.c Schema/args: emit array items and safely encode ARRAY params +63/-7

Schema/args: emit array items and safely encode ARRAY params

• Adds schema support for TOOL_PARAM_TYPE_ARRAY (items:{type:string}), enforces size checks to avoid silent truncation, and exempts repeatable actions from duplicate detection via tool_registry queries.

src/llm/llm_tools.c


calculator.c Calculator: postfix factorial support and exact-mode string wrapper +163/-1

Calculator: postfix factorial support and exact-mode string wrapper

• Rewrites postfix '!' into fac(...) for tinyexpr evaluation, adds input length cap, and introduces calculator_evaluate_exact_str() to return full digits with float fallback and oversize annotation.

src/tools/calculator.c


calculator_bignum.c Calculator: add arbitrary-precision integer evaluator for exact mode +733/-0

Calculator: add arbitrary-precision integer evaluator for exact mode

• Implements base-1e9 bignums and a recursive-descent parser for integer +,-,*,/,^,! with digit caps and inexact detection for fallback.

src/tools/calculator_bignum.c


calculator_tool.c Calculator tool: add exact action, schedulable flag, and input bounds +44/-14

Calculator tool: add exact action, schedulable flag, and input bounds

• Adds 'exact' action to the tool interface, marks random as repeatable for dup-call guard, makes calculator schedulable, and bounds untrusted expression length.

src/tools/calculator_tool.c


music_tool.c Local music tool: items[] play list, batch search, and honest WebUI-only actions +238/-47

Local music tool: items[] play list, batch search, and honest WebUI-only actions

• Adds items[] support for play (resolve+replace) and batch search; introduces relevance-ranked resolver and strips custom tail before single-query search. Returns explicit "WebUI only" messages for queue editing/shuffle/repeat in the local backend.

src/tools/music_tool.c


tool_registry.c Tool registry: enforce ARRAY terminal-slot rule and add repeatable-action helpers +72/-0

Tool registry: enforce ARRAY terminal-slot rule and add repeatable-action helpers

• Validates ARRAY params are last at registration, emits items schema for array params, and adds lookups for action param name + repeatable action checks.

src/tools/tool_registry.c


text_to_speech.cpp TTS: log preprocessed text for debugging +2/-0

TTS: log preprocessed text for debugging

• Logs the first 200 chars of preprocessed TTS text for both async and PCM synthesis paths.

src/tts/text_to_speech.cpp


webui_music_tool.c WebUI music tool: add LLM playlist-building actions and state footer +967/-0

WebUI music tool: add LLM playlist-building actions and state footer

• Introduces LLM-facing WebUI music integration with enqueue, batch search, clear/remove, deterministic shuffle/repeat, relevance-ranked item resolution, and a state footer appended to mutating results; ensures thread-safe WebSocket sending from worker threads.

src/webui/webui_music_tool.c


word_to_number.c ASR parsing: extend word-to-number magnitudes through trigintillion +37/-6

ASR parsing: extend word-to-number magnitudes through trigintillion

• Expands short-scale magnitude vocabulary to match number_to_words, with notes about double precision limitations at extreme magnitudes.

src/word_to_number.c


format.js WebUI rendering: compact unquoted “N factorial” back to “N!” +13/-2

WebUI rendering: compact unquoted “N factorial” back to “N!”

• Collapses unquoted "N factorial" into "N!" for on-screen display while keeping the raw text unchanged for TTS; preserves quoted phrases to avoid mangling explanations.

www/js/ui/format.js


Bug fix (7)
music_db.c Music DB: implement music_db_pick_best_match relevance ranking +65/-0

Music DB: implement music_db_pick_best_match relevance ranking

• Adds a pure ranking function that prefers exact/prefix/substring title matches and strongly boosts matching artists to avoid alphabetical mis-picks (e.g., "Africa").

src/audio/music_db.c


llm_openai_chat_completions.c Tool loop UX: rephrase duplicate-call hint to avoid injection framing +5/-2

Tool loop UX: rephrase duplicate-call hint to avoid injection framing

• Changes the duplicate-tool-call hint message to plain user instruction (no "[System:]" prefix) to reduce reasoning-model false positives.

src/llm/llm_openai_chat_completions.c


llm_streaming.c Streaming safety: gate thinking-as-response fallback to local models +12/-5

Streaming safety: gate thinking-as-response fallback to local models

• Prevents leaking cloud chain-of-thought by only substituting reasoning_content as response for LLM_LOCAL models when no tool calls are present.

src/llm/llm_streaming.c


llm_tool_loop.c Tool loop UX: rephrase duplicate-call hint (generic, non-injection) +6/-3

Tool loop UX: rephrase duplicate-call hint (generic, non-injection)

• Updates the duplicate-call guard hint to be tool-agnostic and not formatted as a system injection marker.

src/llm/llm_tool_loop.c


plan_executor.c Plan executor: support {{var}} and {{var.field}} interpolation +109/-49

Plan executor: support {{var}} and {{var.field}} interpolation

• Implements double-brace interpolation syntax with dot-access parity with $var.field and refactors variable resolution into a shared helper.

src/tools/plan_executor.c


tinyexpr.c tinyexpr factorial: fix overflow by accumulating in double +10/-7

tinyexpr factorial: fix overflow by accumulating in double

• Adjusts fac() to compute factorials in double up to 170! instead of overflowing an integer accumulator past 20!.

src/tools/tinyexpr.c


webui_message_dispatch.c WebUI logging: clarify model selection under OpenRouter gateway +9/-1

WebUI logging: clarify model selection under OpenRouter gateway

• Changes model log wording from "set" to "requested" and logs when bare model IDs are dropped by OpenRouter gateway resolution.

src/webui/webui_message_dispatch.c


Refactor (2)
webui_music_internal.h WebUI music internal: declare shared queue mutation helpers +60/-0

WebUI music internal: declare shared queue mutation helpers

• Defines transport-free queue helper APIs for apply/remove/clear operations shared between browser WebSocket handlers and the LLM tool execution path.

include/webui/webui_music_internal.h


webui_music_handlers.c WebUI music: extract shared queue mutation helpers and reuse them +167/-473

WebUI music: extract shared queue mutation helpers and reuse them

• Moves queue apply/remove/clear mutation bodies into transport-free helpers shared by browser handlers and the new LLM tool entrypoint, reducing duplication and keeping lock ordering consistent.

src/webui/webui_music_handlers.c


Tests (8)
CMakeLists.txt Tests build: wire in new calculator_bignum and number_to_words tests +8/-0

Tests build: wire in new calculator_bignum and number_to_words tests

• Adds calculator_bignum.c to calculator unit tests and introduces a dedicated test_number_to_words target; links number_to_words into TTS preprocessing tests.

tests/CMakeLists.txt


test_calculator.c Calculator tests: add factorial, exact mode, and regression coverage +170/-0

Calculator tests: add factorial, exact mode, and regression coverage

• Adds coverage for postfix factorial rewriting, large factorial evaluation, exact digit output (52!, 2^100), and edge cases.

tests/test_calculator.c


test_music_db.c Music DB tests: validate relevance ranking (conv-813 regression) +65/-0

Music DB tests: validate relevance ranking (conv-813 regression)

• Adds pure unit tests for music_db_pick_best_match ensuring generic titles pick the intended track and artist boosts dominate appropriately.

tests/test_music_db.c


test_number_to_words.c TTS tests: add number_to_words unit suite +176/-0

TTS tests: add number_to_words unit suite

• Introduces tests for number-to-words conversion across basic values, scale names, commas, negatives, and large magnitudes.

tests/test_number_to_words.c


test_plan_executor.c Plan executor tests: cover {{var}} and dot-access interpolation +44/-0

Plan executor tests: cover {{var}} and dot-access interpolation

• Adds tests for double-brace interpolation, dot-field extraction, malformed brace handling, and parity with $var syntax.

tests/test_plan_executor.c


test_tool_registry.c Tool registry tests: cover ARRAY schema, tail extraction, and guard +147/-0

Tool registry tests: cover ARRAY schema, tail extraction, and guard

• Adds tests ensuring ARRAY params emit items schema, require terminal-slot placement, and that tail extraction preserves embedded "::" in serialized arrays.

tests/test_tool_registry.c


test_tts_preprocessing.c TTS preprocessing tests: verify large-number conversion thresholding +66/-0

TTS preprocessing tests: verify large-number conversion thresholding

• Adds tests ensuring small numbers/years/times remain unchanged while >=13-digit tokens (including commas/negatives) become words, including factorial-sized digits.

tests/test_tts_preprocessing.c


test_word_to_number.c Word-to-number tests: cover extended magnitudes and symmetry cases +49/-0

Word-to-number tests: cover extended magnitudes and symmetry cases

• Adds tests for quadrillion through trigintillion (with tolerance bands) and validates the "unvigintillion" magnitude used by factorial speech.

tests/test_word_to_number.c


Other (2)
CMakeLists.txt Build: include new number_to_words, calculator_bignum, and WebUI music tool +3/-0

Build: include new number_to_words, calculator_bignum, and WebUI music tool

• Adds new source files to the main build: number_to_words, calculator_bignum, and the split-out webui_music_tool implementation.

CMakeLists.txt


CMakeLists.txt Build: link number_to_words into dawn_common_tts +1/-0

Build: link number_to_words into dawn_common_tts

• Includes the new number_to_words.c in the common TTS static library when TTS is enabled.

common/CMakeLists.txt


Grey Divider

Qodo Logo

…umbers

Calculator:
- New "exact" action: arbitrary-precision integer evaluator (calculator_bignum)
  over + - * / ^ ! ; falls back to the float path for non-integer expressions.
- Factorial: fix tinyexpr fac() to accumulate in double (overflowed past 20!),
  rewrite postfix "!" to fac(); calculator is now SCHEDULABLE (usable in plans).

TTS number reading:
- number_to_words (common/, scales through trigintillion) speaks large results
  as words instead of comma groups; wired into tts_preprocessing (>=13 digits).
- word_to_number extended to the same scale vocabulary (symmetry).
- Factorial reads naturally: prompt nudge writes "N factorial", WebUI compacts
  it back to "52!" on screen; espeak says the word.

LLM tool loop:
- Exempt non-deterministic actions (calculator random) from the duplicate-call
  guard; reword the dup hint so it isn't flagged as injection; gate the
  thinking-as-response fallback to local models; honest "model requested" log.

Security: bound parser recursion (depth guard + 1024-char input cap) to stop a
stack-exhaustion crash from deeply nested expressions on reduced worker stacks.

Tests: calculator 43, number_to_words 19, word_to_number 30, tts_preprocessing 44
— all pass (incl. deep-nesting + length-cap regressions). ASan-clean on the
bignum and TTS two-pass paths; build + format clean.
…arch

Adds the discover→curate→enqueue→edit→control workflow the music tool
lacked (surfaced by a real playlist-building session):
- enqueue (append) + play/enqueue items:[...] explicit track lists
- batch search items:[...] returning paths for exact enqueue
- clear/remove queue editing; deterministic shuffle/repeat (no blind
  toggle); per-action state footer so the LLM always sees queue state
- description rewrite: one-field search semantics + themed-playlist recipe

Infra:
- new TOOL_PARAM_TYPE_ARRAY (string array) param type; LLM emits a native
  JSON array, delivered via the terminal ::items:: slot + tail extractor;
  registration guard enforces ARRAY-must-be-last
- plan_executor: support {{var}}/{{var.field}} (matches the documented
  syntax; $var still works)
- split webui_music_tool.c out of webui_music_handlers.c; shared
  transport-free queue helpers used by both browser and LLM paths

Reviewed (arch/efficiency/security): batch DoS cap, over-cap reporting,
stack trim, path-valid defense-in-depth. 67/67 CI tests pass (+6 new),
zero warnings.
…plays

Two live-test fixes on the playlist tooling:

- Bare titles like "Africa" resolved to music_db_search's first row, which is
  alphabetical by artist (Bo Burnham before Toto). Add a pure, unit-tested
  music_db_pick_best_match() that ranks candidates by title closeness
  (exact > prefix > substring) with a dominant artist-match bonus and a
  shorter-title tiebreak; no match keeps the DB's first row. Both the WebUI
  and local resolvers now search once and rank instead of taking top-1.
  (conv 813: "add Africa" queued the Bo Burnham track.)

- WebUI enqueue only adds now — it no longer auto-starts playback when the
  queue was idle. "Add X to the queue" should append, not restart playback;
  the browser has its own play control. Local play (voice) still autoplays.
  (conv 814: enqueue restarted playback unexpectedly.)

Adds 5 test_music_db cases incl. the conv-813 regression. 67/67 CI, zero
warnings, format clean.
@qodo-code-review

qodo-code-review Bot commented Jun 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0)

Context used
✅ Compliance rules (platform): 27 rules

Grey Divider


Action required

1. Plex paths not resolved ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolve_item_to_path() only treats strings starting with / as exact paths, so valid plex:
paths returned by WebUI search cannot be enqueued/played via items[] and will incorrectly fall
back to name search or report “No match”. This breaks playlist building for Plex-backed libraries
even though Plex paths are explicitly supported by path validation.
Code

src/webui/webui_music_tool.c[R130-144]

+   /* Absolute path → exact lookup. Gate on the same library-prefix check the
+    * browser add path uses, so an attacker-influenced item can't reach a file
+    * outside the library even if the DB ever indexed one (defense in depth). */
+   if (item[0] == '/') {
+      bool found = false;
+      if (webui_music_is_path_valid(item) && music_db_get_by_path(item, &r[0], &found) == SUCCESS &&
+          found) {
+         safe_strncpy(path_out, r[0].path, path_len);
+         if (display_out) {
+            snprintf(display_out, display_len, "%s - %s", r[0].artist, r[0].title);
+         }
+         return SUCCESS;
+      }
+      return FAILURE;
+   }
Evidence
WebUI search prints and returns raw DB paths (intended for exact enqueue), path validation
explicitly supports plex: prefixes, but the resolver’s exact-path branch only triggers for /
paths so plex: never gets an exact lookup.

src/webui/webui_music_tool.c[118-145]
src/webui/webui_music_tool.c[867-907]
src/webui/webui_music.c[319-353]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The WebUI music tool’s item resolver only performs exact DB lookup when `item[0] == '/'`, which excludes valid Plex paths (`plex:...`). Since `search` returns `r[j].path` and `enqueue/play items[]` is intended to accept those exact paths, Plex-based queues will fail to build.

### Issue Context
`webui_music_is_path_valid()` already supports Plex paths and `music_db_get_by_path()` can retrieve entries by the stored path string.

### Fix Focus Areas
- src/webui/webui_music_tool.c[118-171]

### Suggested fix
- Change the “exact path” branch to trigger for Plex too (e.g. `strncmp(item,"plex:",5)==0`) or more generally: if `webui_music_is_path_valid(item)` then try `music_db_get_by_path(item, ...)` before falling back to `music_db_search()`.
- Keep the existing defense-in-depth validation (`webui_music_is_path_valid`) before any exact path lookup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. webui_music_execute_tool() returns 0/1 ✓ Resolved 📘 Rule violation ≡ Correctness
Description
webui_music_execute_tool() uses raw 0/1 return codes and compares against literal 0, instead
of the project-standard SUCCESS/FAILURE constants. This violates the required standardized
status return convention and makes call sites inconsistent/error-prone.
Code

src/webui/webui_music_tool.c[R312-328]

+int webui_music_execute_tool(ws_connection_t *conn,
+                             const char *action,
+                             const char *query,
+                             char **result_out) {
+   if (!conn || !action) {
+      return 1;
+   }
+
+   /* Ensure music session is initialized */
+   session_music_state_t *state = (session_music_state_t *)conn->music_state;
+   if (!state) {
+      if (webui_music_session_init(conn) != 0) {
+         if (result_out) {
+            *result_out = strdup("Failed to initialize music session");
+         }
+         return 1;
+      }
Evidence
PR Compliance ID 278936 requires using named SUCCESS/FAILURE constants instead of returning or
comparing against raw 0/1 literals for status code APIs. In webui_music_execute_tool(), the
new code returns 1 directly and checks webui_music_session_init(conn) != 0, demonstrating
literal status handling.

Rule 278936: Use standardized SUCCESS/FAILURE constants for function status returns
src/webui/webui_music_tool.c[312-328]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`webui_music_execute_tool()` returns raw `0`/`1` values and performs literal `!= 0` checks. Per project standards, status-returning functions must use `SUCCESS` and `FAILURE` constants (and callers should compare against those constants).

## Issue Context
This is a new WebUI music tool entry point, so we should align it with the repository-wide status-code convention immediately to avoid spreading literal return-code checks.

## Fix Focus Areas
- src/webui/webui_music_tool.c[312-328]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Local items[] ignores paths ✓ Resolved 🐞 Bug ≡ Correctness
Description
The local-speaker backend’s local_resolve_item() never does an exact lookup for absolute paths (it
only calls music_db_search()), so play/enqueue items:[paths] fails even though the tool contract
says paths from search are exact. This makes the new “batch search → enqueue returned paths”
workflow unreliable for the local backend.
Code

src/tools/music_tool.c[R355-383]

+static int local_resolve_item(const char *item, music_search_result_t *out) {
+   if (!item || !item[0]) {
+      return 0;
+   }
+   music_search_result_t r[MUSIC_RESOLVE_CANDIDATES];
+   int count = 0;
+
+   /* Split an optional "Artist - Title"; search the title (or whole item) and
+    * rank candidates by relevance so a bare/generic title ("Africa") doesn't
+    * pick the DB's alphabetical top row. */
+   const char *dash = strstr(item, " - ");
+   char artist[sizeof(out->artist)] = { 0 };
+   const char *title_query = item;
+   if (dash) {
+      size_t alen = (size_t)(dash - item);
+      if (alen >= sizeof(artist)) {
+         alen = sizeof(artist) - 1;
+      }
+      memcpy(artist, item, alen);
+      artist[alen] = '\0';
+      title_query = dash + 3;
+   }
+
+   if (music_db_search(title_query, r, MUSIC_RESOLVE_CANDIDATES, &count) == SUCCESS && count > 0) {
+      int pick = music_db_pick_best_match(r, count, title_query, dash ? artist : NULL);
+      *out = r[pick];
+      return 1;
+   }
+   return 0;
Evidence
The music tool explicitly documents that items[] may contain file paths and that paths from
search are exact, and search indeed returns paths; however the local resolver only searches by
text and never resolves absolute paths via music_db_get_by_path().

src/tools/music_tool.c[149-162]
src/tools/music_tool.c[348-384]
src/tools/music_tool.c[808-842]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`local_resolve_item()` claims to resolve “free-text item (or path)” but it only performs a title search and relevance ranking; it never handles absolute paths via `music_db_get_by_path()`. Since `search` outputs `[path]` and `items[]` explicitly documents that paths are exact, path-based playlist building fails.

### Issue Context
Local batch search emits `display_name [path]`, encouraging the model to pass those paths back into `items[]`.

### Fix Focus Areas
- src/tools/music_tool.c[348-384]
- src/tools/music_tool.c[149-162]
- src/tools/music_tool.c[808-842]

### Suggested fix
- In `local_resolve_item()`, add a fast-path: if `item` looks like an absolute local path (e.g. `item[0] == '/'`), call `music_db_get_by_path(item, out, &found)` and return success when found.
- (Optional, if applicable) If the DB can contain non-local sources (e.g. Plex) and local playback can’t play them, reject those sources explicitly based on `out->source`.
- Keep the existing free-text resolution for non-path items.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Tail extractor underflow ✓ Resolved 🐞 Bug ⛨ Security
Description
tool_param_extract_custom_tail() can underflow out_len - 1 when out_len == 0, causing
memcpy() to attempt an unbounded copy length. Current call sites pass non-zero buffers, but as a
header inline utility this is a latent memory-safety footgun for future callers.
Code

include/tools/tool_registry.h[R829-851]

+static inline bool tool_param_extract_custom_tail(const char *value,
+                                                  const char *field_name,
+                                                  char *out_value,
+                                                  size_t out_len) {
+   if (!value || !field_name || !out_value)
+      return false;
+
+   char pattern[64];
+   snprintf(pattern, sizeof(pattern), "::%s::", field_name);
+
+   const char *pos = strstr(value, pattern);
+   if (!pos)
+      return false;
+
+   const char *val_start = pos + strlen(pattern);
+   size_t val_len = strlen(val_start);
+
+   if (val_len >= out_len)
+      val_len = out_len - 1;
+
+   memcpy(out_value, val_start, val_len);
+   out_value[val_len] = '\0';
+   return true;
Evidence
The function subtracts 1 from out_len on truncation without checking for out_len == 0, which
underflows size_t and can feed a huge length into memcpy().

include/tools/tool_registry.h[829-851]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`tool_param_extract_custom_tail()` truncation logic assumes `out_len >= 1` and does `out_len - 1` without checking. If a future caller passes `out_len == 0`, this underflows `size_t` and can lead to memory corruption.

### Issue Context
This helper is newly added to support `TOOL_PARAM_TYPE_ARRAY` terminal-slot decoding and is defined inline in a public header, making it easy to reuse incorrectly.

### Fix Focus Areas
- include/tools/tool_registry.h[829-851]

### Suggested fix
- Add an explicit guard early: `if (out_len == 0) return false;`.
- (Optional hardening) Also guard `out_len == 1` by writing `out_value[0] = '\0'; return true/false appropriately`.
- (Optional hardening) If `field_name` is too long to fit in `pattern[64]`, return false instead of truncating the pattern.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves LLM tool usability for building and iteratively editing playlists (primarily via the WebUI music backend), while also extending the broader tool framework to support array parameters and more robust plan interpolation. It additionally enhances numeric/factorial handling across the calculator and TTS pipeline to better support “large result” workflows.

Changes:

  • Add WebUI music playlist affordances (append/enqueue, batch search, queue editing, deterministic shuffle/repeat, state footer) and refactor shared queue mutation helpers.
  • Extend the tool framework with TOOL_PARAM_TYPE_ARRAY (schema + packed-arg encoding/decoding contract) and support {{var}} / {{var.field}} interpolation in the plan executor.
  • Improve factorial + large-number handling end-to-end (tinyexpr factorial fix, calculator exact big-int evaluator, number-to-words for TTS, and UI display compaction of “N factorial” → “N!”).

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
www/js/ui/format.js Display-only cleanup for rendered markdown (strip <command> tags; compact unquoted “N factorial” to “N!”).
tests/test_word_to_number.c Add unit coverage for extended short-scale magnitudes.
tests/test_tts_preprocessing.c Add tests for large-number preprocessing into spoken words.
tests/test_tool_registry.c Add tests for array-param schema emission and terminal-slot extraction behavior.
tests/test_plan_executor.c Add tests for {{var}} and dot-access interpolation parity with $var.
tests/test_number_to_words.c New unit tests for number-to-words conversion (incl. factorial-scale coverage).
tests/test_music_db.c Add tests for the new pure relevance ranking music_db_pick_best_match.
tests/test_calculator.c Add tests for postfix factorial and exact-mode big-integer evaluation.
tests/CMakeLists.txt Wire new calculator bignum + number_to_words sources into tests.
src/word_to_number.c Extend magnitude vocabulary to match number_to_words short-scale table.
src/webui/webui_music_tool.c New WebUI LLM-tool entry points (enqueue/batch items/search, queue editing, deterministic shuffle/repeat, state footer).
src/webui/webui_music_handlers.c Extract shared, transport-free queue mutation helpers and reuse them in WS handlers.
src/webui/webui_message_dispatch.c Clarify model logging (“requested” vs “set”) and log OpenRouter bare-model drop behavior.
src/tts/text_to_speech.cpp Add logging of preprocessed TTS text (currently at INFO).
src/tools/tool_registry.c Enforce array terminal-slot contract at registration; emit JSON schema type:array + items.
src/tools/tinyexpr.c Fix factorial accumulation overflow by using double accumulator and clamp at 170!.
src/tools/plan_executor.c Implement {{var}} / {{var.field}} interpolation using shared resolver logic.
src/tools/music_tool.c Expand music tool actions, add items[] support, batch search output with paths, WebUI routing improvements.
src/tools/calculator.c Add postfix factorial rewrite for tinyexpr + implement calculator_evaluate_exact_str() with exact→float fallback.
src/tools/calculator_tool.c Add new exact action, mark tool schedulable, and adjust duplicate-call behavior for random.
src/tools/calculator_bignum.c New arbitrary-precision integer evaluator/parser for calculator exact mode.
src/llm/llm_tools.c Add array schema emission; enforce packed-arg size bounds; exempt repeatable actions from duplicate-call guard.
src/llm/llm_tool_loop.c Reword duplicate-call hint to avoid “[System:]” phrasing.
src/llm/llm_streaming.c Prevent cloud chain-of-thought leakage by gating “thinking-as-response” fallback to local LLMs only.
src/llm/llm_openai_chat_completions.c Align duplicate-call hint phrasing with the tool loop change.
src/llm/llm_command_parser.c Add always-on prose formatting rule for factorial wording; preserve it even when tools are disabled.
src/audio/music_db.c Add music_db_pick_best_match() relevance ranking helper.
include/webui/webui_music_internal.h Declare shared queue mutation helpers + outcome enum for WebUI music.
include/tools/tool_registry.h Document array delivery contract and add tool_param_extract_custom_tail() helper.
include/tools/calculator.h Document + expose calculator_evaluate_exact_str() and add expression length cap constant.
include/tools/calculator_bignum.h New public header for exact-mode evaluator + digit cap constant.
include/audio/music_db.h Declare music_db_pick_best_match() API.
common/src/tts/tts_preprocessing.cpp Add large-number token detection and conversion via number_to_words for TTS.
common/src/tts/number_to_words.c New number-to-words implementation with short-scale names through trigintillion.
common/include/tts/number_to_words.h New public API for number-to-words conversion.
common/CMakeLists.txt Add number_to_words to common TTS library build.
CMakeLists.txt Add new sources (number_to_words, calculator_bignum, webui_music_tool) to main build.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +82
pthread_mutex_lock(&uq->queue_mutex);
int len = uq->queue_length;
bool shuffle = uq->shuffle;
music_repeat_mode_t rep = uq->repeat_mode;
int idx = state->queue_index;
if (len > 0 && idx >= 0 && idx < len) {
safe_strncpy(now_title, uq->queue[idx].title, sizeof(now_title));
safe_strncpy(now_artist, uq->queue[idx].artist, sizeof(now_artist));
}
pthread_mutex_unlock(&uq->queue_mutex);
Comment on lines +838 to +855
/* Build queue list — strbuf so a long queue cannot silently truncate
* mid-row the way the prior fixed sizing did when artist/title strings
* exceeded the per-row estimate. */
strbuf_t sb;
strbuf_init(&sb, 1024 + (size_t)uq->queue_length * 64);
strbuf_appendf(&sb, "Queue (%d tracks, currently #%d):\n", uq->queue_length,
state->queue_index + 1);
const char *rep_s = uq->repeat_mode == MUSIC_REPEAT_ALL
? "all"
: (uq->repeat_mode == MUSIC_REPEAT_ONE ? "one" : "none");
strbuf_appendf(&sb, "Shuffle: %s | Repeat: %s\n", uq->shuffle ? "on" : "off", rep_s);
for (int i = 0; i < uq->queue_length; i++) {
const char *marker = (i == state->queue_index) ? "\xE2\x96\xB6 " : " ";
if (strbuf_appendf(&sb, "%s%d. %s - %s\n", marker, i + 1,
uq->queue[i].artist[0] ? uq->queue[i].artist : "Unknown",
uq->queue[i].title[0] ? uq->queue[i].title : "Unknown") < 0)
break;
}
Comment thread src/tools/music_tool.c
Comment on lines +355 to +361
static int local_resolve_item(const char *item, music_search_result_t *out) {
if (!item || !item[0]) {
return 0;
}
music_search_result_t r[MUSIC_RESOLVE_CANDIDATES];
int count = 0;

Comment on lines 466 to +471
int len = 0;
int remaining = (int)buffer_size;

/* Prose output-format rules first — apply regardless of tool mode. */
len += snprintf(buffer + len, remaining - len, "%s", OUTPUT_FORMATTING_RULES);

Comment on lines 827 to 830
assert(text != nullptr && "Received a null pointer");
std::string inputText = preprocess_text_for_tts(std::string(text));
OLOG_INFO("TTS preprocessed: \"%.200s\"", inputText.c_str());

Comment on lines 868 to 871
// Preprocess text for better TTS output
std::string processedText = preprocess_text_for_tts(std::string(text));
OLOG_INFO("TTS preprocessed: \"%.200s\"", processedText.c_str());

Comment thread include/tools/tool_registry.h Outdated
Comment thread src/webui/webui_music_tool.c
Comment thread src/webui/webui_music_tool.c Outdated
Comment thread src/tools/music_tool.c
…, lock + status

Follow-up from Copilot/Qodo review:
- Resolvers now accept exact library paths from `search` results: WebUI gates on
  webui_music_is_path_valid (covers local AND plex: paths), local adds an
  absolute-path music_db_get_by_path fast path. Fixes "search → enqueue returned
  paths" for Plex-backed libraries and the local backend.
- tool_param_extract_custom_tail/_custom reject out_len==0 (a SIZE_MAX underflow
  would otherwise drive a huge memcpy).
- append_state_footer + list snapshot queue_index under state_mutex (nested in
  queue_mutex, correct order) instead of reading it racily.
- webui_music_execute_tool returns SUCCESS/FAILURE instead of raw 0/1.

+1 unit test (extractor out_len==0). 67/67 CI, zero warnings, format clean.
…logs

From external review (Copilot) of PR #17 — pre-existing issues in unrelated
subsystems, fixed separately off main:

- build_system_instructions_to_buffer() did `len += snprintf(...)`, which returns
  the would-have-written length; on truncation `len` could exceed buffer_size and
  later `buffer+len` / `size-len` writes go out of bounds. Add a clamping
  instr_appendf() helper and a bounded final memcpy.
- text_to_speech logged the fully preprocessed TTS text at INFO on every request
  (and every WebUI streaming chunk), surfacing user content in operator logs.
  Downgrade both to DEBUG.
@KerseyFabrications KerseyFabrications merged commit bfb2460 into main Jun 8, 2026
3 checks passed
KerseyFabrications added a commit that referenced this pull request Jun 8, 2026
…, lock + status

Follow-up from Copilot/Qodo review:
- Resolvers now accept exact library paths from `search` results: WebUI gates on
  webui_music_is_path_valid (covers local AND plex: paths), local adds an
  absolute-path music_db_get_by_path fast path. Fixes "search → enqueue returned
  paths" for Plex-backed libraries and the local backend.
- tool_param_extract_custom_tail/_custom reject out_len==0 (a SIZE_MAX underflow
  would otherwise drive a huge memcpy).
- append_state_footer + list snapshot queue_index under state_mutex (nested in
  queue_mutex, correct order) instead of reading it racily.
- webui_music_execute_tool returns SUCCESS/FAILURE instead of raw 0/1.

+1 unit test (extractor out_len==0). 67/67 CI, zero warnings, format clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants