Skip to content

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 →