Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha
## [Unreleased]

### Fixed
- Session JSON that would exceed the 128 KiB cap is refused instead of truncated, so the next parse cannot wipe history. An oversized stored blob is left in place (distinct `SESSION_LOAD_TOO_LARGE`) rather than replaced by a later small turn.
- Multi-round ReAct copies tool results into the in-flight message list so a later round cannot overwrite earlier outputs.
- `memory_init` no longer deletes an existing SQLite DB when `sqlite3_open` fails (permissions or transient I/O).
- Anthropic `content` parse fails closed when growing the text buffer or `tool_use` array cannot `realloc`, instead of copying against an inflated cap.
Expand Down
55 changes: 38 additions & 17 deletions src/core/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ int agent_mutex_is_locked_for_test(void)
#define SUMMARY_SOURCE_MAX (64 * 1024)
#define SUMMARY_RESULT_MAX 4096

/** Copy cJSON_PrintUnformatted output or refuse mid-JSON truncation (Refs: #70). */
static int copy_printed_session_json(char *dst, size_t dst_size, char *printed,
const char *session_id)
{
size_t len;
if (!printed)
return -1;
len = strlen(printed);
if (len >= dst_size) {
fprintf(stderr,
"agent: refuse session JSON truncation session_id=%s len=%zu cap=%zu\n",
session_id ? session_id : "", len, dst_size);
cJSON_free(printed);
return -1;
}
memcpy(dst, printed, len + 1);
cJSON_free(printed);
return 0;
}

static const char SUMMARIZE_SYSTEM[] = "Summarize the following conversation in one short paragraph. Output only the summary, no preamble.";

/** Summarize oldest messages when over max_ctx; replace with one summary + trailing. */
Expand Down Expand Up @@ -128,12 +148,8 @@ static int compact_session_via_llm(const char *session_id, char *session_buf, si
cJSON_Delete(root);
char *printed = cJSON_PrintUnformatted(new_arr);
cJSON_Delete(new_arr);
if (!printed) return -1;
size_t plen = strlen(printed);
if (plen >= session_buf_size) plen = session_buf_size - 1;
memcpy(session_buf, printed, plen);
session_buf[plen] = '\0';
cJSON_free(printed);
if (copy_printed_session_json(session_buf, session_buf_size, printed, session_id) != 0)
return -1;
session_save(session_id, session_buf);
return 0;
}
Expand Down Expand Up @@ -280,7 +296,7 @@ static void free_tool_calls_copy(const provider_tool_call_t *copy, size_t n)

/** Append user+assistant exchange to session JSON. Invalid or empty existing becomes []. */
static int append_exchange_to_session_json(const char *existing_json, const char *user_message,
const char *assistant_content, char *out_buf, size_t out_size)
const char *assistant_content, char *out_buf, size_t out_size, const char *session_id)
{
cJSON *arr = NULL;
if (existing_json && existing_json[0] == '[') {
Expand All @@ -306,13 +322,7 @@ static int append_exchange_to_session_json(const char *existing_json, const char
}
char *printed = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!printed) return -1;
size_t len = strlen(printed);
if (len >= out_size) len = out_size - 1;
memcpy(out_buf, printed, len);
out_buf[len] = '\0';
cJSON_free(printed);
return 0;
return copy_printed_session_json(out_buf, out_size, printed, session_id);
}

static provider_tool_call_t *copy_tool_calls(const provider_tool_call_t *src, size_t n)
Expand Down Expand Up @@ -357,6 +367,7 @@ typedef struct agent_run_ctx {
int max_iter;
int max_ctx;
int ret;
int skip_session_persist;
} agent_run_ctx_t;

static void agent_oom_msg(agent_run_ctx_t *ctx)
Expand Down Expand Up @@ -420,7 +431,10 @@ static int agent_prepare_context(agent_run_ctx_t *ctx)
if (ctx->max_ctx <= 0 || ctx->max_ctx > MAX_HISTORY_MESSAGES)
ctx->max_ctx = MAX_HISTORY_MESSAGES;
ctx->session_buf[0] = '\0';
session_load(ctx->session_id, ctx->session_buf, SESSION_JSON_MAX);
{
int load_rc = session_load(ctx->session_id, ctx->session_buf, SESSION_JSON_MAX);
ctx->skip_session_persist = (load_rc == SESSION_LOAD_TOO_LARGE);
}
parsed = cJSON_Parse(ctx->session_buf);
msg_count = (parsed && cJSON_IsArray(parsed)) ? cJSON_GetArraySize(parsed) : 0;
if (parsed)
Expand Down Expand Up @@ -462,11 +476,18 @@ static int agent_build_messages(agent_run_ctx_t *ctx)

static void agent_persist_session(agent_run_ctx_t *ctx, const char *assistant_content)
{
char *updated = malloc(SESSION_JSON_MAX);
char *updated;
if (ctx->skip_session_persist) {
fprintf(stderr,
"agent: skip session persist session_id=%s (stored blob exceeds cap)\n",
ctx->session_id ? ctx->session_id : "");
return;
}
updated = malloc(SESSION_JSON_MAX);
if (!updated)
return;
if (append_exchange_to_session_json(ctx->session_buf, ctx->user_message, assistant_content,
updated, SESSION_JSON_MAX) == 0)
updated, SESSION_JSON_MAX, ctx->session_id) == 0)
session_save(ctx->session_id, updated);
free(updated);
}
Expand Down
10 changes: 7 additions & 3 deletions src/core/memory.c
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,13 @@ int session_load(const char *session_id, char *messages_out, size_t max_len)
const char *msg = (const char *)sqlite3_column_text(stmt, 0);
if (msg) {
size_t n = strlen(msg);
if (n >= max_len) n = max_len - 1;
memcpy(messages_out, msg, n);
messages_out[n] = '\0';
/* Refuse silent truncation: a clipped messages blob is invalid JSON
* and agent_run would treat the session as empty history. */
if (n >= max_len) {
sqlite3_finalize(stmt);
return SESSION_LOAD_TOO_LARGE;
}
memcpy(messages_out, msg, n + 1);
ret = 0;
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/core/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,22 @@ int memory_save(const char *key, const char *content, const char *metadata);
*/
int memory_recall(const char *query, char *results, size_t max_len, int limit);

/**
* Stored session JSON does not fit the caller buffer. messages_out is left empty.
* Distinct from "not found" so callers can skip persist instead of overwriting.
*/
#define SESSION_LOAD_TOO_LARGE (-2)

/**
* Load session messages by session ID (e.g. "cli:default" or "telegram:123456789").
*
* @param session_id Session identifier.
* @param messages_out Output buffer for JSON array of messages; caller must free if allocated.
* @param max_len Size of messages_out buffer (or 0 if messages_out is to be allocated by implementation).
* @return 0 on success, non-zero if not found or error.
* @return 0 on success, SESSION_LOAD_TOO_LARGE if the blob does not fit max_len,
* -1 if not found or error.
*
* Example: `if (session_load(id, buf, sizeof(buf)) == SESSION_LOAD_TOO_LARGE) skip_save;`
*/
int session_load(const char *session_id, char *messages_out, size_t max_len);

Expand Down
Loading
Loading