# Fix: Local File Persistence & Stall Diagnostic Improvements ## Context The agent got stuck in a stall loop when trying to create a skill via DM. The root cause is `max_tokens=512` truncating the LLM's tool call JSON. The admin tried to fix it with `/model_set {"max_tokens": 10000}` but got `"failed to persist llm config to config file"` because `model_set` tries to write to a local config file that doesn't exist in the Nostr-native deployment model. Investigation revealed two tools that incorrectly persist state to local files instead of kind 30078 Nostr events, plus missing diagnostic information in the stall detector. ## Hardcoded File Locations to Remove ### tool_model.c - `ctx->cfg->config_path` — references a local config file (genesis.jsonc or config.jsonc) - The entire `persist_llm_config()` function (lines 82-139) reads/writes this file - The `read_entire_file_local()` helper (lines 42-80) exists only for this purpose ### tool_task.c - `"tasks.json"` — hardcoded at line 188: `build_tool_path_local(ctx, "tasks.json", ...)` - `tasks_load_root_local()` (lines 82-141) reads from this file - `tasks_save_root_local()` (lines 143-159) writes to this file ### tools_schema.c - Line 985: `"Update active LLM configuration and persist it to config.jsonc"` — mentions config.jsonc - Line 1285: `"Build current task list context block from tasks.json"` — mentions tasks.json - Line 1314: `"Manage agent short-term task memory stored in tasks.json"` — mentions tasks.json ## Fixes ### Fix 1: model_set — Persist to Kind 30078 **File:** `src/tools/tool_model.c` Replace `persist_llm_config()` (lines 82-139) with a function that: 1. Builds a JSON object with LLM config fields (provider, model, base_url, max_tokens, temperature — NOT api_key for security) 2. NIP-44 self-encrypts it using the same pattern as `config_store` in `tool_config.c` 3. Publishes as kind 30078 with tags `["d", "llm_config"]` and `["app", "didactyl"]` Delete the `read_entire_file_local()` helper (lines 42-80) and the `json_object_set_string()` helper (lines 33-40) — they only exist for the file-based persist. Also update `execute_model_set()` (line 234) to call the new Nostr-based persist, and return success with a warning if persist fails (since runtime update already succeeded at line 229). ### Fix 2: model_set — Don't Fail When Only Persist Fails **File:** `src/tools/tool_model.c`, lines 229-236 Currently: ```c if (llm_set_config(&cfg) != 0) { return json_error_local("failed to update runtime llm config"); } ctx->cfg->llm = cfg; if (persist_llm_config(ctx, &cfg) != 0) { return json_error_local("failed to persist llm config to config file"); } ``` Change to: return success with a `"persist_warning"` field if persist fails, since the runtime update already took effect. ### Fix 3: task_manage — Persist to Kind 30078 **File:** `src/tools/tool_task.c` Replace `tasks_load_root_local()` and `tasks_save_root_local()` with: - `tasks_load_root_nostr()` — queries kind 30078 `d=tasks` from self, NIP-44 decrypts, parses JSON - `tasks_save_root_nostr()` — serializes JSON, NIP-44 self-encrypts, publishes kind 30078 `d=tasks` This follows the exact same pattern as `config_store`/`config_recall` in `tool_config.c` and `memory_save`/`memory_recall` in `tool_memory.c`. Remove the hardcoded `"tasks.json"` string at line 188 and the `build_tool_path_local()` call for it. Remove `tasks_path` from the response JSON at line 424. ### Fix 4: Update Schema Descriptions **File:** `src/tools/tools_schema.c` - Line 985: Change `"Update active LLM configuration and persist it to config.jsonc"` to `"Update active LLM configuration and persist it to Nostr"` - Line 1285: Change `"Build current task list context block from tasks.json"` to `"Build current task list context block from agent task memory"` - Line 1314: Change `"Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)"` to `"Manage agent short-term task memory stored on Nostr (list/add/update/remove/clear/replace)"` ### Fix 5: Add finish_reason to LLM Response **File:** `src/llm.h` Add `char* finish_reason;` to `llm_response_t`: ```c typedef struct { char* content; llm_tool_call_t* tool_calls; int tool_call_count; char* finish_reason; } llm_response_t; ``` **File:** `src/llm.c` In `parse_llm_response()` (line 276), after extracting `msg` from `choices[0]`, extract `finish_reason`: ```c cJSON* fr = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL; if (fr && cJSON_IsString(fr) && fr->valuestring) { out->finish_reason = strdup(fr->valuestring); } ``` In `llm_response_free()`, add `free(response->finish_reason);`. ### Fix 6: Add max_tokens Truncation Hint to Stall Diagnostic **File:** `src/agent.c` In the stall detection block at line 2217, before freeing `resp`, check `finish_reason`: ```c if (repeated_tool_turns >= stall_repeat_threshold) { int truncated = resp.finish_reason && strcmp(resp.finish_reason, "length") == 0; exited_on_stall = 1; llm_response_free(&resp); break; } ``` In `notify_admin_limit_diagnostic()` (line 106), add fields: - `possible_cause` — "max_tokens_truncation" when finish_reason was "length" - `current_max_tokens` — the current max_tokens value - `hint` — "Increase max_tokens: /model_set {\"max_tokens\": 4096}" **File:** `src/http_api.c` Same changes in the HTTP API tool loop stall detection at line 966. ## Execution Order 1. Fix 5 (finish_reason in llm_response_t) — no dependencies 2. Fix 6 (stall diagnostic) — depends on Fix 5 3. Fix 1 (model_set persist to Nostr) — independent 4. Fix 2 (model_set graceful failure) — can be done with Fix 1 5. Fix 3 (task_manage persist to Nostr) — independent 6. Fix 4 (schema descriptions) — do last, trivial ## Files Modified - `src/llm.h` — add finish_reason field - `src/llm.c` — parse finish_reason, free it - `src/agent.c` — stall diagnostic with truncation hint - `src/http_api.c` — stall diagnostic with truncation hint - `src/tools/tool_model.c` — replace file persist with kind 30078, graceful failure - `src/tools/tool_task.c` — replace file I/O with kind 30078 - `src/tools/tools_schema.c` — update 3 descriptions