Files
didactyl/plans/fix_local_file_persistence_and_stall_diagnostic.md

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 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:

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:

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 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