Conversation Pruner — C source
Plan how to fit a long chat history into a context budget — which turns to keep, fold into a summary, or drop, protecting system messages and the current request. 100% client-side.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* conversation-pruner — C port: compute a deterministic pruning plan for a
token-budgeted chat history. C11, stdlib only. Port of
src/lib/conversationPruner.ts — same decisions as this dir's
javascript.js; validation collapses to a 0/-1 status since C has no
exceptions. */
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Summary compression model: fixed framing tokens, then 10% of the folded
content's tokens, rounded up. */
#define CP_SUMMARY_FIXED_TOKENS 60L
#define CP_SUMMARY_RATIO 0.1
typedef enum { CP_SYSTEM, CP_USER, CP_ASSISTANT, CP_TOOL } cp_role;
typedef enum { CP_KEEP, CP_SUMMARIZE, CP_DROP } cp_action;
/* One message plus its prompt-side token count. `content` is borrowed,
never copied. `pinned` messages are never dropped or summarized (the
TypeScript's optional flag defaults to false). */
typedef struct {
cp_role role;
const char *content;
long tokens;
int pinned;
} cp_message;
typedef struct {
size_t index;
cp_role role;
cp_action action;
long tokens;
} cp_decision;
/* The plan only ever produces two warnings (protected overflow, summary
does not fit); the small matrix keeps the struct self-contained. */
#define CP_MAX_WARNINGS 4
#define CP_WARNING_LEN 192
typedef struct {
cp_decision *decisions; /* malloc'd; caller frees */
size_t n;
long kept_tokens;
long summarized_tokens;
long dropped_tokens;
long summary_cost_tokens; /* tokens the summary placeholder itself costs */
long projected_tokens;
int fits_budget;
char warnings[CP_MAX_WARNINGS][CP_WARNING_LEN];
size_t n_warnings;
} cp_plan;
static const char *cp_role_name(cp_role r) {
switch (r) {
case CP_SYSTEM: return "system";
case CP_USER: return "user";
case CP_ASSISTANT: return "assistant";
case CP_TOOL: return "tool";
}
return "?";
}
static const char *cp_action_name(cp_action a) {
switch (a) {
case CP_KEEP: return "keep";
case CP_SUMMARIZE: return "summarize";
case CP_DROP: return "drop";
}
return "?";
}
/* 1234567 -> "1,234,567", matching toLocaleString('en-US'). */
static void cp_thousands(long v, char *buf, size_t cap) {
char digits[24];
int len = 0;
size_t pos = 0;
unsigned long a = (unsigned long)(v < 0 ? -v : v);
if (a == 0) digits[len++] = '0';
while (a > 0) {
digits[len++] = (char)('0' + (int)(a % 10));
a /= 10;
}
if (v < 0 && pos + 1 < cap) buf[pos++] = '-';
for (int i = len - 1; i >= 0; i--) {
int from_left = len - 1 - i; /* comma when the digits after it group by 3 */
if (from_left > 0 && (len - from_left) % 3 == 0 && pos + 1 < cap) buf[pos++] = ',';
if (pos + 1 < cap) buf[pos++] = digits[i];
}
buf[pos] = '\0';
}
/* Append printf-style at `pos`, clamped so callers stay in range. */
static size_t cp_appendf(char *buf, size_t cap, size_t pos, const char *fmt, ...) {
va_list ap;
int w;
if (pos >= cap) return pos;
va_start(ap, fmt);
w = vsnprintf(buf + pos, cap - pos, fmt, ap);
va_end(ap);
if (w < 0) return pos;
pos += (size_t)w;
return pos > cap ? cap : pos;
}
/* Compute the pruning plan. Returns 0 on success, -1 on a negative budget,
a negative per-message token count, or allocation failure (the
TypeScript throws RangeError for the first two). `out->decisions` is
malloc'd — free it when done. */
int cp_plan_prune(const cp_message *msgs, size_t n, long budget_tokens, cp_plan *out) {
long last_user = -1, first_turn = -1, protected_tokens, summarize_tokens, attempted;
long used, summary_cost, kept, dropped, folded;
size_t summarize_count, tail_start, i;
unsigned char *prot;
cp_action *actions;
cp_decision *decisions;
if (out == NULL) return -1;
memset(out, 0, sizeof(*out));
if (budget_tokens < 0) return -1;
for (i = 0; i < n; i++) {
if (msgs[i].tokens < 0) return -1;
}
for (i = n; i-- > 0;) {
if (msgs[i].role == CP_USER) {
last_user = (long)i;
break;
}
}
/* Untouchable: every system message, pinned messages, the first turn
(the opening user request that anchors the conversation), and the
current request (the last user message and everything after it). */
prot = calloc(n > 0 ? n : 1, 1);
actions = malloc((n > 0 ? n : 1) * sizeof(*actions));
decisions = malloc((n > 0 ? n : 1) * sizeof(*decisions));
if (prot == NULL || actions == NULL || decisions == NULL) {
free(prot);
free(actions);
free(decisions);
return -1;
}
for (i = 0; i < n; i++) {
if (msgs[i].role == CP_SYSTEM || msgs[i].pinned) prot[i] = 1;
}
if (n > 0) prot[0] = 1;
for (i = 0; i < n; i++) {
if (msgs[i].role != CP_SYSTEM) {
first_turn = (long)i;
break;
}
}
if (first_turn != -1) prot[first_turn] = 1;
tail_start = last_user == -1 ? (n > 0 ? n - 1 : 0) : (size_t)last_user;
for (i = tail_start; i < n; i++) prot[i] = 1;
protected_tokens = 0;
for (i = 0; i < n; i++) {
if (prot[i]) protected_tokens += msgs[i].tokens;
}
if (protected_tokens > budget_tokens) {
char a[32], b[32];
cp_thousands(protected_tokens, a, sizeof a);
cp_thousands(budget_tokens, b, sizeof b);
snprintf(out->warnings[out->n_warnings++], CP_WARNING_LEN,
"Protected messages alone are %s tokens against a %s budget — raise the "
"budget (or reserve less for the reply) before pruning anything else.",
a, b);
}
/* Fill the remaining budget newest-to-oldest through the middle. */
for (i = 0; i < n; i++) actions[i] = prot[i] ? CP_KEEP : CP_DROP;
used = protected_tokens;
for (i = n; i-- > 0;) {
if (actions[i] != CP_DROP) continue;
if (used + msgs[i].tokens <= budget_tokens) {
actions[i] = CP_KEEP;
used += msgs[i].tokens;
} else {
break; /* oldest-unfilled remain drop/summarize candidates, newest first stopped */
}
}
/* Everything still 'drop' in the middle folds into ONE running summary
when the compressed form fits where the raw turns did not. */
summarize_count = 0;
summarize_tokens = 0;
for (i = 0; i < n; i++) {
if (actions[i] == CP_DROP && !prot[i]) {
summarize_tokens += msgs[i].tokens;
summarize_count++;
}
}
attempted = summarize_count > 0
? CP_SUMMARY_FIXED_TOKENS + (long)ceil(summarize_tokens * CP_SUMMARY_RATIO)
: 0;
/* The summary only costs anything when it is actually applied —
otherwise those turns drop and cost zero. */
summary_cost = 0;
if (attempted > 0 && used + attempted <= budget_tokens) {
for (i = 0; i < n; i++) {
if (actions[i] == CP_DROP && !prot[i]) actions[i] = CP_SUMMARIZE;
}
summary_cost = attempted;
used += summary_cost; /* mirrors the TS; never read again */
} else if (attempted > 0) {
char a[32];
cp_thousands(attempted, a, sizeof a);
snprintf(out->warnings[out->n_warnings++], CP_WARNING_LEN,
"Even the compressed summary (%s tokens) does not fit the remaining budget — "
"the oldest turns are dropped instead.",
a);
}
for (i = 0; i < n; i++) {
decisions[i].index = i;
decisions[i].role = msgs[i].role;
decisions[i].action = actions[i];
decisions[i].tokens = msgs[i].tokens;
}
kept = dropped = folded = 0;
for (i = 0; i < n; i++) {
if (actions[i] == CP_KEEP) kept += msgs[i].tokens;
else if (actions[i] == CP_DROP) dropped += msgs[i].tokens;
else folded += msgs[i].tokens;
}
out->decisions = decisions;
out->n = n;
out->kept_tokens = kept;
out->summarized_tokens = folded;
out->dropped_tokens = dropped;
out->summary_cost_tokens = summary_cost;
out->projected_tokens = kept + summary_cost;
out->fits_budget = (kept + summary_cost <= budget_tokens);
free(prot);
free(actions);
return 0;
}
/* Human-readable one-line summary of a plan, written into buf. */
void cp_describe_prune(const cp_plan *plan, char *buf, size_t cap) {
size_t pos = 0;
char k[32], s[32], c[32], d[32];
if (buf == NULL || cap == 0) return;
if (!plan->fits_budget) {
char p[32];
cp_thousands(plan->projected_tokens, p, sizeof p);
snprintf(buf, cap, "Does not fit: %s tokens projected against the budget.", p);
return;
}
cp_thousands(plan->kept_tokens, k, sizeof k);
pos = cp_appendf(buf, cap, pos, "%s kept", k);
if (plan->summarized_tokens > 0) {
cp_thousands(plan->summarized_tokens, s, sizeof s);
cp_thousands(plan->summary_cost_tokens, c, sizeof c);
pos = cp_appendf(buf, cap, pos, " · %s folded into a %s-token summary", s, c);
}
if (plan->dropped_tokens > 0) {
cp_thousands(plan->dropped_tokens, d, sizeof d);
pos = cp_appendf(buf, cap, pos, " · %s dropped", d);
}
cp_appendf(buf, cap, pos, " — fits the budget.");
}
int main(void) {
cp_message msgs[] = {
{CP_SYSTEM, "You are CosmoDev's assistant.", 80, 0},
{CP_USER, "Please review my repo layout.", 250, 0},
{CP_ASSISTANT, "(long review of the tree)", 900, 0},
{CP_TOOL, "git status output", 400, 0},
{CP_ASSISTANT, "summary of the findings", 700, 0},
{CP_USER, "Now prune this conversation.", 350, 0},
};
size_t n = sizeof msgs / sizeof msgs[0];
cp_plan plan;
char line[256];
size_t i;
if (cp_plan_prune(msgs, n, 1000, &plan) != 0) {
fprintf(stderr, "plan failed\n");
return 1;
}
printf("budget 1,000:\n");
for (i = 0; i < plan.n; i++) {
printf(" %zu %-9s %-9s %5ld\n", plan.decisions[i].index,
cp_role_name(plan.decisions[i].role), cp_action_name(plan.decisions[i].action),
plan.decisions[i].tokens);
}
for (i = 0; i < plan.n_warnings; i++) printf(" warning: %s\n", plan.warnings[i]);
cp_describe_prune(&plan, line, sizeof line);
printf(" %s\n", line);
free(plan.decisions);
/* Squeezed budget: protected turns alone overflow, and even the
compressed summary does not fit. */
if (cp_plan_prune(msgs, n, 50, &plan) != 0) {
fprintf(stderr, "plan failed\n");
return 1;
}
printf("budget 50:\n");
for (i = 0; i < plan.n_warnings; i++) printf(" warning: %s\n", plan.warnings[i]);
cp_describe_prune(&plan, line, sizeof line);
printf(" %s\n", line);
free(plan.decisions);
return 0;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →