v0.0.25 - Add model_get/model_set/model_list tools with persisted LLM config updates and model discovery
This commit is contained in:
@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.0.24
|
||||
## Current Status — v0.0.25
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.24 — Handle SIGPIPE disconnect crash and fix nostr_list_manage use-after-free
|
||||
> Last release update: v0.0.25 — Add model_get/model_set/model_list tools with persisted LLM config updates and model discovery
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
268
docs/CRASH_FIXES.md
Normal file
268
docs/CRASH_FIXES.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Crash Fixes Reference
|
||||
|
||||
This document catalogues crashes that have been diagnosed and fixed in Didactyl.
|
||||
Use it as a reference when investigating future crashes — the symptoms, root causes,
|
||||
and investigation techniques described here may save significant debugging time.
|
||||
|
||||
---
|
||||
|
||||
## 1. Silent process exit on relay disconnect — SIGPIPE (v0.0.24)
|
||||
|
||||
**Date**: 2026-03-02
|
||||
**Version**: v0.0.23 → fixed in v0.0.24
|
||||
**Severity**: Critical — process terminates without any log output
|
||||
**Files changed**: `src/main.c`
|
||||
|
||||
### Symptoms
|
||||
|
||||
- The process exits cleanly to the shell prompt with **no error message**.
|
||||
- The `[didactyl] shutting down` log line does **not** appear.
|
||||
- No coredump is generated.
|
||||
- The last log lines show all relays transitioning from `connected → disconnected`
|
||||
and then immediately `disconnected → connected`.
|
||||
- The crash typically follows a period of network instability or DNS resolution
|
||||
failure — for example, an LLM HTTP request failing with
|
||||
`curl=Could not resolve hostname`.
|
||||
|
||||
### Root cause
|
||||
|
||||
`SIGPIPE` was never handled. The default OS action for `SIGPIPE` is to terminate
|
||||
the process immediately — no signal handler runs, no cleanup occurs, no coredump
|
||||
is written.
|
||||
|
||||
The TLS write path in `nostr_core_lib` uses `SSL_write()`, which internally calls
|
||||
`write()` on the underlying socket. When a `wss://` relay drops its TCP connection
|
||||
and the relay pool attempts to write to that socket, the kernel delivers `SIGPIPE`.
|
||||
|
||||
The plain-TCP path for `ws://` connections was already protected by using
|
||||
`MSG_NOSIGNAL` on `send()` calls, but the TLS path had no equivalent protection.
|
||||
|
||||
### How it was diagnosed
|
||||
|
||||
1. **No crash message or coredump** ruled out `SIGSEGV`, `SIGABRT`, and OOM.
|
||||
2. **No shutdown log** ruled out a graceful exit via the signal handler.
|
||||
3. `dmesg` and `journalctl` showed no OOM-kill or segfault entries.
|
||||
4. Searching the entire codebase for `SIGPIPE` or `SIG_IGN` returned zero results.
|
||||
5. The websocket TLS transport was confirmed to use `SSL_write()` without
|
||||
`MSG_NOSIGNAL` or any per-thread signal mask.
|
||||
6. The timeline matched: relay disconnects occurred, the next poll cycle attempted
|
||||
a write to a dead TLS socket, and the process vanished.
|
||||
|
||||
### Fix
|
||||
|
||||
Added `signal(SIGPIPE, SIG_IGN)` in `main()` alongside the existing `SIGINT` and
|
||||
`SIGTERM` handlers:
|
||||
|
||||
```c
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN); /* ← added */
|
||||
```
|
||||
|
||||
This causes `SSL_write()` and any other write to a broken pipe to return `-1` with
|
||||
`errno = EPIPE` instead of killing the process. The relay pool and libcurl already
|
||||
handle write errors gracefully.
|
||||
|
||||
### How to recognise this class of bug in the future
|
||||
|
||||
- Process disappears without any log output or coredump.
|
||||
- Happens after network disruption or relay disconnects.
|
||||
- `ulimit -c` may be 0, but even with unlimited core size, `SIGPIPE` does not
|
||||
produce a coredump by default.
|
||||
- Any new network transport layer added to the project should be audited for
|
||||
`SIGPIPE` protection.
|
||||
|
||||
---
|
||||
|
||||
## 2. Use-after-free in `execute_nostr_list_manage` — SIGSEGV (v0.0.24)
|
||||
|
||||
**Date**: 2026-03-01 (coredump), fixed 2026-03-02
|
||||
**Version**: v0.0.23 → fixed in v0.0.24
|
||||
**Severity**: Critical — segmentation fault during tool execution
|
||||
**Files changed**: `src/tools.c`
|
||||
|
||||
### Symptoms
|
||||
|
||||
- `SIGSEGV` crash during execution of the `nostr_list_manage` tool.
|
||||
- Coredump shows the crash at `src/tools.c:29` inside `json_error()`, but with
|
||||
heavily corrupted stack frames — the real crash site is in
|
||||
`execute_nostr_list_manage()`.
|
||||
- The corrupted stack is characteristic of heap corruption from use-after-free.
|
||||
|
||||
### Root cause
|
||||
|
||||
In `execute_nostr_list_manage()`, the `action` pointer was obtained from the
|
||||
`args` cJSON tree:
|
||||
|
||||
```c
|
||||
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
|
||||
```
|
||||
|
||||
Later, `args` was freed:
|
||||
|
||||
```c
|
||||
cJSON_Delete(args); /* frees the entire tree including action */
|
||||
```
|
||||
|
||||
But `action->valuestring` was still accessed afterwards:
|
||||
|
||||
```c
|
||||
cJSON_AddStringToObject(out, "action", action->valuestring); /* dangling pointer */
|
||||
```
|
||||
|
||||
After `cJSON_Delete(args)`, the `action` pointer is dangling. Accessing
|
||||
`action->valuestring` reads freed heap memory, which may contain arbitrary data
|
||||
or may have been reallocated for another purpose.
|
||||
|
||||
### How it was diagnosed
|
||||
|
||||
1. The coredump was extracted from systemd-coredump storage:
|
||||
```
|
||||
zstd -d /var/lib/systemd/coredump/core.didactyl_static.*.zst -o /tmp/core.bin
|
||||
gdb ./didactyl_static_x86_64_debug /tmp/core.bin -batch -ex "bt"
|
||||
```
|
||||
2. The backtrace showed `execute_nostr_list_manage` with corrupted frames.
|
||||
3. Code review of the function identified the `cJSON_Delete(args)` call occurring
|
||||
before the last use of `action->valuestring`.
|
||||
|
||||
### Fix
|
||||
|
||||
Deferred `cJSON_Delete(args)` until after `action->valuestring` has been consumed
|
||||
by `cJSON_AddStringToObject()`. The free now occurs immediately after the last use:
|
||||
|
||||
```c
|
||||
cJSON_AddStringToObject(out, "action", action->valuestring);
|
||||
cJSON_Delete(args); /* ← moved here, after last use of action */
|
||||
```
|
||||
|
||||
All early-return error paths before this point also received their own
|
||||
`cJSON_Delete(args)` call to prevent leaks.
|
||||
|
||||
### How to recognise this class of bug in the future
|
||||
|
||||
- `SIGSEGV` with corrupted or nonsensical stack frames.
|
||||
- Crash location reported by GDB does not match the actual buggy code — the
|
||||
corruption happened earlier.
|
||||
- Any function that calls `cJSON_Delete()` on a parent object should be audited
|
||||
to ensure no child pointers are used afterwards.
|
||||
- Pattern to watch for:
|
||||
```c
|
||||
cJSON* child = cJSON_GetObjectItemCaseSensitive(parent, "key");
|
||||
/* ... */
|
||||
cJSON_Delete(parent);
|
||||
/* ... */
|
||||
use(child->valuestring); /* BUG: child is dangling */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. DM subscription not receiving incoming kind 4 events — INVESTIGATION (v0.0.24)
|
||||
|
||||
**Date**: 2026-03-02
|
||||
**Version**: v0.0.24
|
||||
**Severity**: High — agent does not respond to incoming DMs
|
||||
**Status**: Under investigation — diagnostic logging added
|
||||
**Files changed**: `src/nostr_handler.c`
|
||||
|
||||
### Symptoms
|
||||
|
||||
- The agent starts up normally, connects to relays, and can **send** kind 4 DMs.
|
||||
- Incoming kind 4 messages posted to the same relays are never processed.
|
||||
- No error messages appear in the log — the agent simply sits idle after sending
|
||||
its startup DM.
|
||||
- The last log lines show successful outbound DM publishing but no inbound event
|
||||
processing.
|
||||
|
||||
### Possible root causes (under investigation)
|
||||
|
||||
1. **Events never reach `on_event()` callback** — The relay pool library only
|
||||
calls `on_event()` when it receives an `EVENT` message matching the
|
||||
subscription ID. If the subscription was silently closed, errored, or not
|
||||
re-established after a relay reconnect, no events would be delivered. The
|
||||
relay could also be rejecting the subscription filter.
|
||||
|
||||
2. **`since` filter timing mismatch** — `g_start_time` is set to `time(NULL)`
|
||||
during `nostr_handler_init()` (early in startup), but the DM subscription
|
||||
is created much later (after admin context subscription, startup event
|
||||
reconciliation, and startup DM sending). If the sender's clock is behind
|
||||
the server's clock, their `created_at` timestamp would be before
|
||||
`g_start_time` and the relay would filter them out.
|
||||
|
||||
3. **Sender tier filtering** — If the sender's pubkey doesn't match the admin
|
||||
pubkey and isn't in the WoT contact list, the message is classified as
|
||||
`DIDACTYL_SENDER_STRANGER` and silently dropped (only a DEBUG_LOG at
|
||||
level 4).
|
||||
|
||||
4. **Signature verification failure** — If `verify_signatures` is enabled and
|
||||
the event has an invalid signature, it is dropped with only a WARN log.
|
||||
|
||||
5. **Pool-level deduplication** — The relay pool's `is_event_seen()` cache is
|
||||
shared across all subscriptions. If an event ID was somehow marked as seen
|
||||
by another subscription, it would be silently dropped.
|
||||
|
||||
### Diagnostic logging added
|
||||
|
||||
TRACE-level (level 5 / `--debug 5`) logs were added at every decision point
|
||||
in the `on_event()` callback and the `nostr_handler_subscribe_dms()` setup:
|
||||
|
||||
| Log message prefix | What it tells you |
|
||||
|---|---|
|
||||
| `DEBUG on_event ENTRY` | Callback was called — events are reaching didactyl |
|
||||
| `DEBUG on_event NULL guard` | Event, config, or callback pointer is NULL |
|
||||
| `DEBUG on_event: missing required fields` | Event JSON is malformed |
|
||||
| `DEBUG on_event: kind=N id=... from=...` | Event kind, ID, and sender pubkey |
|
||||
| `DEBUG on_event: ignoring non-kind4` | Event is not kind 4 |
|
||||
| `DEBUG on_event: no p-tag found` | Kind 4 event has no `p` tag |
|
||||
| `DEBUG on_event: p-tag mismatch` | `p` tag doesn't match agent's pubkey |
|
||||
| `DEBUG on_event: sender=... tier=N` | Sender tier classification |
|
||||
| `DEBUG on_eose called` | EOSE received from relays |
|
||||
| `DEBUG DM subscription filter` | Full subscription filter JSON |
|
||||
| `DEBUG DM subscription g_start_time=...` | `since` timestamp and time delta |
|
||||
| `DEBUG DM subscription sub=...` | Subscription pointer (confirms creation) |
|
||||
|
||||
### How to use the diagnostic logs
|
||||
|
||||
1. Build with `./build_static.sh --debug`
|
||||
2. Run with `--debug 5` to enable TRACE output
|
||||
3. Send a kind 4 DM to the agent
|
||||
4. Check the output:
|
||||
- **No `on_event ENTRY` lines** → problem is at the relay/subscription level
|
||||
- **`on_event ENTRY` appears but processing stops** → the specific drop-point
|
||||
log identifies the exact filter that rejected the event
|
||||
5. Check the `DM subscription filter` log to verify the `since` timestamp and
|
||||
`#p` tag are correct
|
||||
|
||||
### How to recognise this class of bug in the future
|
||||
|
||||
- Agent can send but not receive — asymmetric connectivity.
|
||||
- No error messages in the log — silent event filtering.
|
||||
- The subscription filter (`since`, `#p`, `kinds`) may not match what the
|
||||
sender is actually publishing.
|
||||
- Clock skew between sender and receiver can cause `since` filter mismatches.
|
||||
- Relay reconnections may not automatically re-subscribe.
|
||||
|
||||
---
|
||||
|
||||
## General debugging checklist
|
||||
|
||||
When investigating a crash in Didactyl, work through these steps:
|
||||
|
||||
1. **Check for coredumps**: `ls /var/lib/systemd/coredump/ | grep didactyl`
|
||||
2. **Check dmesg/journalctl**: `dmesg | grep -i 'didactyl\|oom\|segfault'`
|
||||
3. **Check for the shutdown log line**: If `[didactyl] shutting down` is missing,
|
||||
the process was killed by a signal that bypassed the handler.
|
||||
4. **Check ulimit**: `ulimit -c` — if 0, coredumps are disabled.
|
||||
5. **Extract and analyse coredumps**:
|
||||
```bash
|
||||
zstd -d /var/lib/systemd/coredump/core.didactyl*.zst -o /tmp/core.bin
|
||||
gdb ./didactyl_static_x86_64_debug /tmp/core.bin -batch -ex "bt full"
|
||||
```
|
||||
6. **Common silent killers**:
|
||||
- `SIGPIPE` — process writes to a broken socket/pipe
|
||||
- `SIGKILL` — OOM killer or external kill
|
||||
- `SIGBUS` — memory-mapped file issues
|
||||
7. **Common crash causes**:
|
||||
- Use-after-free on cJSON child pointers after parent deletion
|
||||
- Buffer overflows in fixed-size stack buffers
|
||||
- NULL pointer dereference on failed allocations
|
||||
- Re-entrancy in callbacks during internal `nostr_relay_pool_poll()` calls
|
||||
@@ -602,6 +602,7 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config_set_error("unknown configuration error");
|
||||
|
||||
memset(config, 0, sizeof(*config));
|
||||
snprintf(config->config_path, sizeof(config->config_path), "%s", path);
|
||||
config->tools.enabled = 1;
|
||||
config->tools.max_turns = 8;
|
||||
config->tools.shell.enabled = 1;
|
||||
|
||||
@@ -92,6 +92,7 @@ typedef struct {
|
||||
triggers_config_t triggers;
|
||||
startup_event_t* startup_events;
|
||||
int startup_event_count;
|
||||
char config_path[OW_MAX_URL_LEN];
|
||||
} didactyl_config_t;
|
||||
|
||||
int config_load(const char* path, didactyl_config_t* config);
|
||||
|
||||
49
src/llm.c
49
src/llm.c
@@ -64,15 +64,13 @@ static const char* detect_ca_bundle_path(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char* perform_chat_request(const char* body) {
|
||||
static char* perform_http_request(const char* url, const char* body, int is_post) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl || !body) {
|
||||
if (!curl || !url) {
|
||||
if (curl) curl_easy_cleanup(curl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char url[OW_MAX_URL_LEN + 64];
|
||||
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
|
||||
|
||||
response_buffer_t rb = {0};
|
||||
struct curl_slist* headers = NULL;
|
||||
@@ -83,8 +81,11 @@ static char* perform_chat_request(const char* body) {
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, is_post ? 0L : 1L);
|
||||
if (is_post) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "{}");
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
|
||||
@@ -132,6 +133,12 @@ static char* perform_chat_request(const char* body) {
|
||||
return rb.data;
|
||||
}
|
||||
|
||||
static char* perform_chat_request(const char* body) {
|
||||
char url[OW_MAX_URL_LEN + 64];
|
||||
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
|
||||
return perform_http_request(url, body, 1);
|
||||
}
|
||||
|
||||
static char* build_request_json(const char* system_prompt, const char* user_message) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
@@ -388,6 +395,36 @@ void llm_response_free(llm_response_t* response) {
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
|
||||
int llm_get_config(llm_config_t* out_config) {
|
||||
if (!g_initialized || !out_config) {
|
||||
return -1;
|
||||
}
|
||||
*out_config = g_cfg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int llm_set_config(const llm_config_t* config) {
|
||||
if (!g_initialized || !config) {
|
||||
return -1;
|
||||
}
|
||||
g_cfg = *config;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* llm_list_models_json(const char* base_url_override) {
|
||||
if (!g_initialized) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* base_url = (base_url_override && base_url_override[0] != '\0')
|
||||
? base_url_override
|
||||
: g_cfg.base_url;
|
||||
|
||||
char url[OW_MAX_URL_LEN + 32];
|
||||
snprintf(url, sizeof(url), "%s/models", base_url);
|
||||
return perform_http_request(url, NULL, 0);
|
||||
}
|
||||
|
||||
void llm_cleanup(void) {
|
||||
if (!g_initialized) {
|
||||
return;
|
||||
|
||||
@@ -26,6 +26,9 @@ int llm_chat_with_tools_messages(const char* messages_json,
|
||||
const char* tool_choice,
|
||||
llm_response_t* out_response);
|
||||
void llm_response_free(llm_response_t* response);
|
||||
int llm_get_config(llm_config_t* out_config);
|
||||
int llm_set_config(const llm_config_t* config);
|
||||
char* llm_list_models_json(const char* base_url_override);
|
||||
void llm_cleanup(void);
|
||||
|
||||
#endif
|
||||
11
src/main.c
11
src/main.c
@@ -114,9 +114,17 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
if (dump_schemas || test_tool_name) {
|
||||
if (llm_init(&cfg.llm) != 0) {
|
||||
fprintf(stderr, "Failed to initialize llm client\n");
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nostr_handler_init(&cfg) != 0) {
|
||||
fprintf(stderr, "Failed to initialize nostr handler\n");
|
||||
config_free(&cfg);
|
||||
llm_cleanup();
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
@@ -126,6 +134,7 @@ int main(int argc, char** argv) {
|
||||
fprintf(stderr, "Failed to initialize tools context\n");
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
llm_cleanup();
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
@@ -142,6 +151,7 @@ int main(int argc, char** argv) {
|
||||
tools_cleanup(&tools_ctx);
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
llm_cleanup();
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
@@ -171,6 +181,7 @@ int main(int argc, char** argv) {
|
||||
tools_cleanup(&tools_ctx);
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
llm_cleanup();
|
||||
nostr_cleanup();
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 0
|
||||
#define DIDACTYL_VERSION_PATCH 24
|
||||
#define DIDACTYL_VERSION "v0.0.24"
|
||||
#define DIDACTYL_VERSION_PATCH 25
|
||||
#define DIDACTYL_VERSION "v0.0.25"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -429,7 +429,13 @@ static void trace_plaintext_dm(const char* prefix, const char* plaintext) {
|
||||
static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event ENTRY from %s (event=%p g_cfg=%p g_dm_callback=%s)",
|
||||
relay_url ? relay_url : "NULL", (void*)event, (void*)g_cfg,
|
||||
g_dm_callback ? "set" : "NULL");
|
||||
|
||||
if (!event || !g_cfg || !g_dm_callback) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event NULL guard: event=%p g_cfg=%p g_dm_callback=%s",
|
||||
(void*)event, (void*)g_cfg, g_dm_callback ? "set" : "NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -447,6 +453,8 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
|
||||
if (!kind || !pubkey || !content || !tags ||
|
||||
!cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !cJSON_IsString(content)) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: missing required fields (kind=%p pubkey=%p content=%p tags=%p)",
|
||||
(void*)kind, (void*)pubkey, (void*)content, (void*)tags);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -454,16 +462,27 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
? id->valuestring
|
||||
: NULL;
|
||||
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: kind=%d id=%.16s... from=%.16s... via %s",
|
||||
(int)kind->valuedouble,
|
||||
event_id_hex ? event_id_hex : "<no-id>",
|
||||
pubkey->valuestring ? pubkey->valuestring : "<no-pk>",
|
||||
relay_url ? relay_url : "unknown");
|
||||
|
||||
if ((int)kind->valuedouble != 4) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring non-kind4 (kind=%d)", (int)kind->valuedouble);
|
||||
return;
|
||||
}
|
||||
|
||||
char recipient_pubkey_hex[65] = {0};
|
||||
if (extract_first_p_tag(tags, recipient_pubkey_hex) != 0) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: no p-tag found in kind4 event %.16s...",
|
||||
event_id_hex ? event_id_hex : "<no-id>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(recipient_pubkey_hex, g_cfg->keys.public_key_hex) != 0) {
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: p-tag mismatch (got=%.16s... want=%.16s...)",
|
||||
recipient_pubkey_hex, g_cfg->keys.public_key_hex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -474,6 +493,9 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
tier = DIDACTYL_SENDER_WOT;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_event: sender=%.16s... tier=%d (admin=%.16s...)",
|
||||
pubkey->valuestring, (int)tier, g_cfg->admin.pubkey);
|
||||
|
||||
if (tier == DIDACTYL_SENDER_STRANGER) {
|
||||
if (!g_cfg->security.stranger.enabled) {
|
||||
DEBUG_LOG("[didactyl] ignored DM from stranger %.16s... via %s",
|
||||
@@ -531,8 +553,8 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
|
||||
static void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)events;
|
||||
(void)event_count;
|
||||
(void)user_data;
|
||||
DEBUG_TRACE("[didactyl] DEBUG on_eose called: event_count=%d", event_count);
|
||||
}
|
||||
|
||||
static void free_admin_context_locked(void) {
|
||||
@@ -867,6 +889,14 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
cJSON_AddNumberToObject(filter, "since", (double)g_start_time);
|
||||
cJSON_AddNumberToObject(filter, "limit", 100);
|
||||
|
||||
{
|
||||
char* filter_str = cJSON_PrintUnformatted(filter);
|
||||
DEBUG_TRACE("[didactyl] DEBUG DM subscription filter: %s", filter_str ? filter_str : "<null>");
|
||||
DEBUG_TRACE("[didactyl] DEBUG DM subscription g_start_time=%ld now=%ld delta=%ld relay_count=%d",
|
||||
(long)g_start_time, (long)time(NULL), (long)(time(NULL) - g_start_time), g_cfg->relay_count);
|
||||
free(filter_str);
|
||||
}
|
||||
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
g_pool,
|
||||
(const char**)g_cfg->relays,
|
||||
@@ -888,6 +918,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
||||
DEBUG_TRACE("[didactyl] DEBUG DM subscription sub=%p close_on_eose=0 dedup=1", (void*)sub);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
333
src/tools.c
333
src/tools.c
@@ -17,6 +17,7 @@
|
||||
#include "main.h"
|
||||
#include "nostr_handler.h"
|
||||
#include "trigger_manager.h"
|
||||
#include "llm.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
static char* json_error(const char* msg) {
|
||||
@@ -1707,6 +1708,79 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t29, "function", t29_fn);
|
||||
cJSON_AddItemToArray(tools, t29);
|
||||
|
||||
cJSON* t30 = cJSON_CreateObject();
|
||||
cJSON* t30_fn = cJSON_CreateObject();
|
||||
cJSON* t30_params = cJSON_CreateObject();
|
||||
cJSON* t30_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t30, "type", "function");
|
||||
cJSON_AddStringToObject(t30_fn, "name", "model_get");
|
||||
cJSON_AddStringToObject(t30_fn, "description", "Get current active LLM runtime configuration (excluding API key)");
|
||||
cJSON_AddStringToObject(t30_params, "type", "object");
|
||||
cJSON_AddItemToObject(t30_params, "properties", t30_props);
|
||||
|
||||
cJSON_AddItemToObject(t30_fn, "parameters", t30_params);
|
||||
cJSON_AddItemToObject(t30, "function", t30_fn);
|
||||
cJSON_AddItemToArray(tools, t30);
|
||||
|
||||
cJSON* t31 = cJSON_CreateObject();
|
||||
cJSON* t31_fn = cJSON_CreateObject();
|
||||
cJSON* t31_params = cJSON_CreateObject();
|
||||
cJSON* t31_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t31, "type", "function");
|
||||
cJSON_AddStringToObject(t31_fn, "name", "model_set");
|
||||
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to config.json");
|
||||
cJSON_AddStringToObject(t31_params, "type", "object");
|
||||
cJSON_AddItemToObject(t31_params, "properties", t31_props);
|
||||
|
||||
cJSON* p_model_set_provider = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_provider, "type", "string");
|
||||
cJSON_AddItemToObject(t31_props, "provider", p_model_set_provider);
|
||||
|
||||
cJSON* p_model_set_api_key = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_api_key, "type", "string");
|
||||
cJSON_AddItemToObject(t31_props, "api_key", p_model_set_api_key);
|
||||
|
||||
cJSON* p_model_set_model = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_model, "type", "string");
|
||||
cJSON_AddItemToObject(t31_props, "model", p_model_set_model);
|
||||
|
||||
cJSON* p_model_set_base_url = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_base_url, "type", "string");
|
||||
cJSON_AddItemToObject(t31_props, "base_url", p_model_set_base_url);
|
||||
|
||||
cJSON* p_model_set_max_tokens = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_max_tokens, "type", "integer");
|
||||
cJSON_AddItemToObject(t31_props, "max_tokens", p_model_set_max_tokens);
|
||||
|
||||
cJSON* p_model_set_temperature = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_set_temperature, "type", "number");
|
||||
cJSON_AddItemToObject(t31_props, "temperature", p_model_set_temperature);
|
||||
|
||||
cJSON_AddItemToObject(t31_fn, "parameters", t31_params);
|
||||
cJSON_AddItemToObject(t31, "function", t31_fn);
|
||||
cJSON_AddItemToArray(tools, t31);
|
||||
|
||||
cJSON* t32 = cJSON_CreateObject();
|
||||
cJSON* t32_fn = cJSON_CreateObject();
|
||||
cJSON* t32_params = cJSON_CreateObject();
|
||||
cJSON* t32_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t32, "type", "function");
|
||||
cJSON_AddStringToObject(t32_fn, "name", "model_list");
|
||||
cJSON_AddStringToObject(t32_fn, "description", "List available model IDs using provider OpenAI-compatible /models endpoint");
|
||||
cJSON_AddStringToObject(t32_params, "type", "object");
|
||||
cJSON_AddItemToObject(t32_params, "properties", t32_props);
|
||||
|
||||
cJSON* p_model_list_base_url = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_model_list_base_url, "type", "string");
|
||||
cJSON_AddItemToObject(t32_props, "base_url", p_model_list_base_url);
|
||||
|
||||
cJSON_AddItemToObject(t32_fn, "parameters", t32_params);
|
||||
cJSON_AddItemToObject(t32, "function", t32_fn);
|
||||
cJSON_AddItemToArray(tools, t32);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
@@ -4278,6 +4352,256 @@ static char* execute_trigger_list(tools_context_t* ctx, const char* args_json) {
|
||||
return status;
|
||||
}
|
||||
|
||||
static int json_object_set_string(cJSON* obj, const char* key, const char* value) {
|
||||
if (!obj || !key || !value) return -1;
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(obj, key);
|
||||
cJSON* v = cJSON_CreateString(value);
|
||||
if (!v) return -1;
|
||||
cJSON_AddItemToObject(obj, key, v);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int persist_llm_config(tools_context_t* ctx, const llm_config_t* cfg) {
|
||||
if (!ctx || !ctx->cfg || !cfg) return -1;
|
||||
if (ctx->cfg->config_path[0] == '\0') return -1;
|
||||
|
||||
size_t src_len = 0;
|
||||
char* src = read_entire_file(ctx->cfg->config_path, &src_len);
|
||||
if (!src) return -1;
|
||||
|
||||
cJSON* root = cJSON_ParseWithLength(src, src_len);
|
||||
free(src);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
|
||||
if (!llm || !cJSON_IsObject(llm)) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "llm");
|
||||
llm = cJSON_CreateObject();
|
||||
if (!llm) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "llm", llm);
|
||||
}
|
||||
|
||||
if (json_object_set_string(llm, "provider", cfg->provider) != 0 ||
|
||||
json_object_set_string(llm, "api_key", cfg->api_key) != 0 ||
|
||||
json_object_set_string(llm, "model", cfg->model) != 0 ||
|
||||
json_object_set_string(llm, "base_url", cfg->base_url) != 0) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "max_tokens");
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->max_tokens);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "temperature");
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->temperature);
|
||||
|
||||
char* out = cJSON_Print(root);
|
||||
cJSON_Delete(root);
|
||||
if (!out) return -1;
|
||||
|
||||
FILE* fp = fopen(ctx->cfg->config_path, "wb");
|
||||
if (!fp) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t out_len = strlen(out);
|
||||
size_t n = fwrite(out, 1, out_len, fp);
|
||||
fclose(fp);
|
||||
free(out);
|
||||
return n == out_len ? 0 : -1;
|
||||
}
|
||||
|
||||
static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) {
|
||||
if (!item) return 0;
|
||||
if (!cJSON_IsString(item) || !item->valuestring) return -1;
|
||||
size_t n = strlen(item->valuestring);
|
||||
if (n >= dst_size) return -1;
|
||||
memcpy(dst, item->valuestring, n + 1U);
|
||||
if (changed) *changed = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* execute_model_get(const char* args_json) {
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
llm_config_t cfg;
|
||||
if (llm_get_config(&cfg) != 0) {
|
||||
return json_error("llm runtime unavailable");
|
||||
}
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "provider", cfg.provider);
|
||||
cJSON_AddStringToObject(out, "model", cfg.model);
|
||||
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
|
||||
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
|
||||
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_model_set(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
|
||||
llm_config_t cfg;
|
||||
if (llm_get_config(&cfg) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error("llm runtime unavailable");
|
||||
}
|
||||
|
||||
int changed = 0;
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(args, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(args, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(args, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(args, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature");
|
||||
|
||||
if (assign_string_field(provider, cfg.provider, sizeof(cfg.provider), &changed) != 0 ||
|
||||
assign_string_field(api_key, cfg.api_key, sizeof(cfg.api_key), &changed) != 0 ||
|
||||
assign_string_field(model, cfg.model, sizeof(cfg.model), &changed) != 0 ||
|
||||
assign_string_field(base_url, cfg.base_url, sizeof(cfg.base_url), &changed) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error("model_set string field invalid or too long");
|
||||
}
|
||||
|
||||
if (max_tokens) {
|
||||
if (!cJSON_IsNumber(max_tokens)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error("model_set max_tokens must be a number");
|
||||
}
|
||||
cfg.max_tokens = (int)max_tokens->valuedouble;
|
||||
changed = 1;
|
||||
}
|
||||
|
||||
if (temperature) {
|
||||
if (!cJSON_IsNumber(temperature)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error("model_set temperature must be a number");
|
||||
}
|
||||
cfg.temperature = temperature->valuedouble;
|
||||
changed = 1;
|
||||
}
|
||||
|
||||
cJSON_Delete(args);
|
||||
|
||||
if (!changed) {
|
||||
return json_error("model_set requires at least one field to update");
|
||||
}
|
||||
|
||||
if (llm_set_config(&cfg) != 0) {
|
||||
return json_error("failed to update runtime llm config");
|
||||
}
|
||||
|
||||
ctx->cfg->llm = cfg;
|
||||
if (persist_llm_config(ctx, &cfg) != 0) {
|
||||
return json_error("failed to persist llm config to config file");
|
||||
}
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "provider", cfg.provider);
|
||||
cJSON_AddStringToObject(out, "model", cfg.model);
|
||||
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
|
||||
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
|
||||
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
static void append_model_id(cJSON* ids, cJSON* item) {
|
||||
if (!ids || !item) return;
|
||||
if (cJSON_IsString(item) && item->valuestring) {
|
||||
cJSON_AddItemToArray(ids, cJSON_CreateString(item->valuestring));
|
||||
return;
|
||||
}
|
||||
if (cJSON_IsObject(item)) {
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id");
|
||||
if (id && cJSON_IsString(id) && id->valuestring) {
|
||||
cJSON_AddItemToArray(ids, cJSON_CreateString(id->valuestring));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static char* execute_model_list(const char* args_json) {
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(args, "base_url");
|
||||
if (base_url && (!cJSON_IsString(base_url) || !base_url->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error("model_list base_url must be a string");
|
||||
}
|
||||
|
||||
const char* base_url_override = (base_url && base_url->valuestring && base_url->valuestring[0] != '\0')
|
||||
? base_url->valuestring
|
||||
: NULL;
|
||||
|
||||
char* raw = llm_list_models_json(base_url_override);
|
||||
cJSON_Delete(args);
|
||||
if (!raw) {
|
||||
return json_error("model_list request failed");
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(raw);
|
||||
free(raw);
|
||||
if (!root) {
|
||||
cJSON_Delete(root);
|
||||
return json_error("model_list returned invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* ids = cJSON_CreateArray();
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!ids || !out) {
|
||||
cJSON_Delete(ids);
|
||||
cJSON_Delete(out);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (cJSON_IsArray(root)) {
|
||||
int n = cJSON_GetArraySize(root);
|
||||
for (int i = 0; i < n; i++) {
|
||||
append_model_id(ids, cJSON_GetArrayItem(root, i));
|
||||
}
|
||||
} else if (cJSON_IsObject(root)) {
|
||||
cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
||||
if (data && cJSON_IsArray(data)) {
|
||||
int n = cJSON_GetArraySize(data);
|
||||
for (int i = 0; i < n; i++) {
|
||||
append_model_id(ids, cJSON_GetArrayItem(data, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(ids));
|
||||
cJSON_AddItemToObject(out, "models", ids);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
cJSON_Delete(root);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json) {
|
||||
if (!tool_name) return json_error("missing tool name");
|
||||
|
||||
@@ -4362,6 +4686,15 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
|
||||
if (strcmp(tool_name, "tool_list") == 0) {
|
||||
return execute_tool_list(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "model_get") == 0) {
|
||||
return execute_model_get(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "model_set") == 0) {
|
||||
return execute_model_set(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "model_list") == 0) {
|
||||
return execute_model_list(args_json);
|
||||
}
|
||||
|
||||
if (strcmp(tool_name, "nostr_post_readme") == 0) {
|
||||
return execute_nostr_post_readme(ctx, args_json);
|
||||
|
||||
Reference in New Issue
Block a user