6.1 KiB
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 filetasks_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:
- Builds a JSON object with LLM config fields (provider, model, base_url, max_tokens, temperature — NOT api_key for security)
- NIP-44 self-encrypts it using the same pattern as
config_storeintool_config.c - 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:
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 30078d=tasksfrom self, NIP-44 decrypts, parses JSONtasks_save_root_nostr()— serializes JSON, NIP-44 self-encrypts, publishes kind 30078d=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:
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:
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:
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 valuehint— "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
- Fix 5 (finish_reason in llm_response_t) — no dependencies
- Fix 6 (stall diagnostic) — depends on Fix 5
- Fix 1 (model_set persist to Nostr) — independent
- Fix 2 (model_set graceful failure) — can be done with Fix 1
- Fix 3 (task_manage persist to Nostr) — independent
- Fix 4 (schema descriptions) — do last, trivial
Files Modified
src/llm.h— add finish_reason fieldsrc/llm.c— parse finish_reason, free itsrc/agent.c— stall diagnostic with truncation hintsrc/http_api.c— stall diagnostic with truncation hintsrc/tools/tool_model.c— replace file persist with kind 30078, graceful failuresrc/tools/tool_task.c— replace file I/O with kind 30078src/tools/tools_schema.c— update 3 descriptions