commit 5ba13c5d7937caa643a04594bd9be7ec3f26a4bc from: Isaac Meerleo date: Fri Jul 17 21:27:28 2026 UTC Track files across Compaction summaries commit - 83f2c983cb279c2381c039c3947997ebde1ce2e1 commit + 5ba13c5d7937caa643a04594bd9be7ec3f26a4bc blob - 8b519d83481110bc2d8732230afc0ad6207089ef blob + 2ff2caa01bbf79cd0afb7e9e32e39d4fd23b33a5 --- regress/compaction/compaction_test.c +++ regress/compaction/compaction_test.c @@ -416,6 +416,226 @@ test_document_bound(void) msglist_free(&messages); } +static void +test_file_collection_and_rendering(void) +{ + static const char expected[] = + "model continuation notes\n\n" + "\n" + "a.c\n" + "\n\n" + "\n" + "m.c\n" + "z.c\n" + ""; + struct compaction_files *files; + struct msglist messages; + struct msg *message; + struct buf out; + const void *path; + size_t pathlen; + + TAILQ_INIT(&messages); + message = msg_new(ROLE_ASSISTANT); + msg_add_tool_use(message, "1", "read", "{\"path\":\"z.c\"}", + strlen("{\"path\":\"z.c\"}")); + msg_add_tool_use(message, "2", "read", "{\"path\":\"a.c\"}", + strlen("{\"path\":\"a.c\"}")); + msg_add_tool_use(message, "3", "read", "{\"path\":\"z.c\"}", + strlen("{\"path\":\"z.c\"}")); + msg_add_tool_use(message, "4", "edit", "{\"path\":\"z.c\"}", + strlen("{\"path\":\"z.c\"}")); + msg_add_tool_use(message, "5", "write", "{\"path\":\"m.c\"}", + strlen("{\"path\":\"m.c\"}")); + msg_add_tool_use(message, "6", "multi_edit", + "{\"path\":\"m.c\"}", strlen("{\"path\":\"m.c\"}")); + msg_add_tool_use(message, "7", "shell", + "{\"path\":\"never.c\"}", strlen("{\"path\":\"never.c\"}")); + append_message(&messages, message); + message = msg_new(ROLE_USER); + msg_add_tool_result(message, "5", "write failed", 12, 1); + append_message(&messages, message); + + files = compaction_files_new(); + CHECK(compaction_files_collect(files, &messages) == 0); + CHECK(compaction_files_count(files, COMPACTION_FILE_READ) == 1); + CHECK(compaction_files_count(files, COMPACTION_FILE_MODIFIED) == 2); + CHECK(compaction_files_get(files, COMPACTION_FILE_READ, 0, &path, + &pathlen) == 0); + CHECK(pathlen == 3 && memcmp(path, "a.c", 3) == 0); + CHECK(compaction_files_get(files, COMPACTION_FILE_MODIFIED, 1, &path, + &pathlen) == 0); + CHECK(pathlen == 3 && memcmp(path, "z.c", 3) == 0); + + buf_init(&out); + buf_addstr(&out, "model continuation notes"); + CHECK(compaction_files_append(&out, files, SIZE_MAX) == 0); + CHECK(buf_is(&out, expected, sizeof(expected) - 1)); + + buf_free(&out); + compaction_files_free(files); + msglist_free(&messages); +} + +static void +test_file_cumulative_merge(void) +{ + static const char expected[] = + "\n" + "new.c\n" + "old.c\n" + "\n\n" + "\n" + "prior-mod.c\n" + "promote.c\n" + ""; + struct compaction_files *current, *previous; + struct buf out; + + previous = compaction_files_new(); + CHECK(compaction_files_add(previous, COMPACTION_FILE_READ, + "old.c", 5) == 0); + CHECK(compaction_files_add(previous, COMPACTION_FILE_READ, + "promote.c", 9) == 0); + CHECK(compaction_files_add(previous, COMPACTION_FILE_MODIFIED, + "prior-mod.c", 11) == 0); + current = compaction_files_new(); + CHECK(compaction_files_add(current, COMPACTION_FILE_READ, + "new.c", 5) == 0); + CHECK(compaction_files_add(current, COMPACTION_FILE_READ, + "prior-mod.c", 11) == 0); + CHECK(compaction_files_add(current, COMPACTION_FILE_MODIFIED, + "promote.c", 9) == 0); + + CHECK(compaction_files_merge(current, previous) == 0); + CHECK(compaction_files_count(current, COMPACTION_FILE_READ) == 2); + CHECK(compaction_files_count(current, COMPACTION_FILE_MODIFIED) == 2); + /* Merge never mutates the prior fugu-generated details. */ + CHECK(compaction_files_count(previous, COMPACTION_FILE_READ) == 2); + CHECK(compaction_files_count(previous, COMPACTION_FILE_MODIFIED) == 1); + + buf_init(&out); + CHECK(compaction_files_append(&out, current, SIZE_MAX) == 0); + CHECK(buf_is(&out, expected, sizeof(expected) - 1)); + + buf_free(&out); + compaction_files_free(current); + compaction_files_free(previous); +} + +static void +test_file_safety_and_transactions(void) +{ + static const char expected[] = + "notes\n\n" + "\n" + "keep.c\n" + "\n\n" + "\n" + "a<&>.c\n" + ""; + static const char empty_expected[] = + "\n" + "\n\n" + "\n" + ""; + static const char malformed_utf8[] = { + '{', '"', 'p', 'a', 't', 'h', '"', ':', '"', 'x', + (char)0xc0, (char)0x80, '"', '}' + }; + static const char nul_path[] = { 'x', '\0', 'y' }; + static const char control_path[] = { 'x', 0x1f, 'y' }; + static const char bad_utf8_path[] = { + 'x', (char)0xc0, (char)0x80, 'y' + }; + struct compaction_files *empty, *files; + struct msglist messages; + struct msg *message; + struct buf out; + char *long_path; + + files = compaction_files_new(); + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, + "keep.c", 6) == 0); + + /* Malformed, nested, non-string, and inexact tool calls are ignored. */ + TAILQ_INIT(&messages); + message = msg_new(ROLE_ASSISTANT); + msg_add_tool_use(message, "1", "read", "{\"path\"}", + strlen("{\"path\"}")); + msg_add_tool_use(message, "2", "read", "{\"path\":7}", + strlen("{\"path\":7}")); + msg_add_tool_use(message, "3", "read", + "{\"nested\":{\"path\":\"nested.c\"}}", + strlen("{\"nested\":{\"path\":\"nested.c\"}}")); + msg_add_tool_use(message, "4", "reader", "{\"path\":\"reader.c\"}", + strlen("{\"path\":\"reader.c\"}")); + msg_add_tool_use(message, "5", "shell", "{\"path\":\"shell.c\"}", + strlen("{\"path\":\"shell.c\"}")); + msg_add_tool_use(message, "6", "read", malformed_utf8, + sizeof(malformed_utf8)); + append_message(&messages, message); + CHECK(compaction_files_collect(files, &messages) == 0); + CHECK(compaction_files_count(files, COMPACTION_FILE_READ) == 1); + CHECK(compaction_files_count(files, COMPACTION_FILE_MODIFIED) == 0); + msglist_free(&messages); + + /* One rejected path rolls back earlier valid calls in the same span. */ + TAILQ_INIT(&messages); + message = msg_new(ROLE_ASSISTANT); + msg_add_tool_use(message, "7", "read", "{\"path\":\"candidate.c\"}", + strlen("{\"path\":\"candidate.c\"}")); + msg_add_tool_use(message, "8", "edit", + "{\"path\":\"bad\\n\"}", + strlen("{\"path\":\"bad\\n\"}")); + append_message(&messages, message); + CHECK(compaction_files_collect(files, &messages) == -1); + CHECK(compaction_files_count(files, COMPACTION_FILE_READ) == 1); + CHECK(compaction_files_count(files, COMPACTION_FILE_MODIFIED) == 0); + msglist_free(&messages); + + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, nul_path, + sizeof(nul_path)) == -1); + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, "", 0) == -1); + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, control_path, + sizeof(control_path)) == -1); + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, bad_utf8_path, + sizeof(bad_utf8_path)) == -1); + long_path = malloc(COMPACTION_FILE_PATH_MAX + 1); + CHECK(long_path != NULL); + if (long_path != NULL) { + memset(long_path, 'x', COMPACTION_FILE_PATH_MAX + 1); + CHECK(compaction_files_add(files, COMPACTION_FILE_READ, long_path, + COMPACTION_FILE_PATH_MAX + 1) == -1); + free(long_path); + } + CHECK(compaction_files_count(files, COMPACTION_FILE_READ) == 1); + CHECK(compaction_files_add(files, COMPACTION_FILE_MODIFIED, + "a<&>.c", 6) == 0); + + /* A tight output bound cannot partially append generated structure. */ + buf_init(&out); + buf_addstr(&out, "notes"); + CHECK(compaction_files_append(&out, files, sizeof(expected) - 2) == -1); + CHECK(buf_is(&out, "notes", 5)); + CHECK(compaction_files_append(&out, files, sizeof(expected) - 1) == 0); + CHECK(buf_is(&out, expected, sizeof(expected) - 1)); + buf_free(&out); + + /* Every generated summary gets both blocks, including empty sets. */ + empty = compaction_files_new(); + buf_init(&out); + CHECK(compaction_files_append(&out, empty, + sizeof(empty_expected) - 2) == -1); + CHECK(out.len == 0); + CHECK(compaction_files_append(&out, empty, + sizeof(empty_expected) - 1) == 0); + CHECK(buf_is(&out, empty_expected, sizeof(empty_expected) - 1)); + buf_free(&out); + compaction_files_free(empty); + compaction_files_free(files); +} + int main(void) { @@ -429,6 +649,9 @@ main(void) test_result_boundaries(); test_previous_summary(); test_document_bound(); + test_file_collection_and_rendering(); + test_file_cumulative_merge(); + test_file_safety_and_transactions(); REGRESS_END(); } blob - 055f699049af7bf78409f053534a3cf087ad2171 blob + ef442a546e46c57af45ba8512f9ea51db50de6a0 --- src/common/compaction.c +++ src/common/compaction.c @@ -26,6 +26,7 @@ #include "json.h" #include "msg.h" #include "utf8.h" +#include "xmalloc.h" #include "compaction.h" const char compaction_system_prompt[] = @@ -50,7 +51,377 @@ struct compact_writer { int records; }; +struct compaction_file { + u_char *path; + size_t len; +}; + +struct compaction_file_set { + struct compaction_file *files; + size_t len; + size_t cap; +}; + +struct compaction_files { + struct compaction_file_set read; + struct compaction_file_set modified; + size_t bytes; +}; + +static void +file_set_free(struct compaction_file_set *set) +{ + size_t i; + + for (i = 0; i < set->len; i++) + free(set->files[i].path); + free(set->files); + memset(set, 0, sizeof(*set)); +} + +struct compaction_files * +compaction_files_new(void) +{ + return (xcalloc(1, sizeof(struct compaction_files))); +} + +void +compaction_files_free(struct compaction_files *files) +{ + if (files == NULL) + return; + file_set_free(&files->read); + file_set_free(&files->modified); + free(files); +} + static int +path_cmp(const void *ap, size_t alen, const void *bp, size_t blen) +{ + size_t len; + int cmp; + + len = alen < blen ? alen : blen; + cmp = len == 0 ? 0 : memcmp(ap, bp, len); + if (cmp != 0) + return (cmp); + if (alen < blen) + return (-1); + if (alen > blen) + return (1); + return (0); +} + +static int +file_set_find(const struct compaction_file_set *set, const void *path, + size_t pathlen, size_t *at) +{ + size_t lo, hi, mid; + int cmp; + + lo = 0; + hi = set->len; + while (lo < hi) { + mid = lo + (hi - lo) / 2; + cmp = path_cmp(path, pathlen, set->files[mid].path, + set->files[mid].len); + if (cmp == 0) { + *at = mid; + return (1); + } + if (cmp < 0) + hi = mid; + else + lo = mid + 1; + } + *at = lo; + return (0); +} + +static void +file_set_insert(struct compaction_file_set *set, size_t at, + const void *path, size_t pathlen) +{ + u_char *copy; + size_t cap; + + copy = xmalloc(pathlen + 1); + if (pathlen > 0) + memcpy(copy, path, pathlen); + copy[pathlen] = '\0'; + if (set->len == set->cap) { + cap = set->cap == 0 ? 16 : set->cap * 2; + if (cap > COMPACTION_FILE_COUNT_MAX) + cap = COMPACTION_FILE_COUNT_MAX; + set->files = xreallocarray(set->files, cap, + sizeof(*set->files)); + set->cap = cap; + } + if (at < set->len) + memmove(&set->files[at + 1], &set->files[at], + (set->len - at) * sizeof(*set->files)); + set->files[at].path = copy; + set->files[at].len = pathlen; + set->len++; +} + +static void +file_set_remove(struct compaction_file_set *set, size_t at) +{ + free(set->files[at].path); + if (at + 1 < set->len) + memmove(&set->files[at], &set->files[at + 1], + (set->len - at - 1) * sizeof(*set->files)); + set->len--; +} + +static int +path_valid(const void *path, size_t pathlen) +{ + const u_char *p = path; + uint32_t cp; + size_t off, used; + + if (pathlen == 0 || pathlen > COMPACTION_FILE_PATH_MAX || path == NULL) + return (0); + for (off = 0; off < pathlen; off += used) { + used = utf8_decode(p + off, pathlen - off, &cp); + if ((cp == UTF8_REPLACEMENT && used == 1) || cp < 0x20 || + (cp >= 0x7f && cp <= 0x9f) || cp == 0x2028 || + cp == 0x2029) + return (0); + } + return (1); +} + +static size_t +files_count(const struct compaction_files *files) +{ + return (files->read.len + files->modified.len); +} + +static int +files_add(struct compaction_files *files, enum compaction_file_kind kind, + const void *path, size_t pathlen) +{ + size_t at, read_at; + + if (!path_valid(path, pathlen)) + return (-1); + if (file_set_find(&files->modified, path, pathlen, &at)) + return (0); + if (kind == COMPACTION_FILE_READ) { + if (file_set_find(&files->read, path, pathlen, &at)) + return (0); + if (files_count(files) == COMPACTION_FILE_COUNT_MAX || + pathlen > COMPACTION_FILE_BYTES_MAX - files->bytes) + return (-1); + file_set_insert(&files->read, at, path, pathlen); + files->bytes += pathlen; + return (0); + } + if (kind != COMPACTION_FILE_MODIFIED) + return (-1); + if (file_set_find(&files->read, path, pathlen, &read_at)) { + file_set_remove(&files->read, read_at); + file_set_find(&files->modified, path, pathlen, &at); + file_set_insert(&files->modified, at, path, pathlen); + return (0); + } + if (files_count(files) == COMPACTION_FILE_COUNT_MAX || + pathlen > COMPACTION_FILE_BYTES_MAX - files->bytes) + return (-1); + file_set_insert(&files->modified, at, path, pathlen); + files->bytes += pathlen; + return (0); +} + +static struct compaction_files * +files_clone(const struct compaction_files *src) +{ + struct compaction_files *copy; + size_t i; + + copy = compaction_files_new(); + for (i = 0; i < src->read.len; i++) + file_set_insert(©->read, copy->read.len, + src->read.files[i].path, src->read.files[i].len); + for (i = 0; i < src->modified.len; i++) + file_set_insert(©->modified, copy->modified.len, + src->modified.files[i].path, src->modified.files[i].len); + copy->bytes = src->bytes; + return (copy); +} + +static void +files_publish(struct compaction_files *dst, struct compaction_files *src) +{ + struct compaction_files old; + + old = *dst; + *dst = *src; + *src = old; + compaction_files_free(src); +} + +int +compaction_files_add(struct compaction_files *files, + enum compaction_file_kind kind, const void *path, size_t pathlen) +{ + struct compaction_files *candidate; + + if (files == NULL) + return (-1); + candidate = files_clone(files); + if (files_add(candidate, kind, path, pathlen) == -1) { + compaction_files_free(candidate); + return (-1); + } + files_publish(files, candidate); + return (0); +} + +static int +tool_file_kind(const char *name, enum compaction_file_kind *kind) +{ + if (name == NULL) + return (0); + if (strcmp(name, "read") == 0) { + *kind = COMPACTION_FILE_READ; + return (1); + } + if (strcmp(name, "write") == 0 || strcmp(name, "edit") == 0 || + strcmp(name, "multi_edit") == 0) { + *kind = COMPACTION_FILE_MODIFIED; + return (1); + } + return (0); +} + +static int +collect_block(struct compaction_files *files, const struct block *block) +{ + struct json json; + enum compaction_file_kind kind; + char *path; + size_t pathlen; + int path_token; + + if (block->type != BLOCK_TOOL_USE || + !tool_file_kind(block->tool_name, &kind)) + return (0); + if ((block->tool_input_len > 0 && block->tool_input == NULL) || + !json_valid_object(block->tool_input, block->tool_input_len) || + json_parse(&json, block->tool_input, block->tool_input_len, 0) == -1) + return (0); + path_token = json_obj_get(&json, json_root(&json), "path"); + if (path_token == -1 || !json_is_string(&json, path_token)) { + json_done(&json); + return (0); + } + path = json_get_str(&json, path_token, &pathlen); + json_done(&json); + if (path == NULL) + return (-1); + if (files_add(files, kind, path, pathlen) == -1) { + free(path); + return (-1); + } + free(path); + return (0); +} + +int +compaction_files_collect(struct compaction_files *files, + const struct msglist *messages) +{ + const struct msg *message; + const struct block *block; + struct compaction_files *candidate; + + if (files == NULL || messages == NULL) + return (-1); + candidate = files_clone(files); + TAILQ_FOREACH(message, messages, entry) { + if (message->role != ROLE_ASSISTANT) + continue; + TAILQ_FOREACH(block, &message->blocks, entry) { + if (collect_block(candidate, block) == -1) { + compaction_files_free(candidate); + return (-1); + } + } + } + files_publish(files, candidate); + return (0); +} + +int +compaction_files_merge(struct compaction_files *files, + const struct compaction_files *previous) +{ + struct compaction_files *candidate; + size_t i; + + if (files == NULL || previous == NULL) + return (-1); + candidate = files_clone(files); + for (i = 0; i < previous->read.len; i++) { + if (files_add(candidate, COMPACTION_FILE_READ, + previous->read.files[i].path, + previous->read.files[i].len) == -1) + goto fail; + } + for (i = 0; i < previous->modified.len; i++) { + if (files_add(candidate, COMPACTION_FILE_MODIFIED, + previous->modified.files[i].path, + previous->modified.files[i].len) == -1) + goto fail; + } + files_publish(files, candidate); + return (0); + +fail: + compaction_files_free(candidate); + return (-1); +} + +size_t +compaction_files_count(const struct compaction_files *files, + enum compaction_file_kind kind) +{ + if (files == NULL) + return (0); + if (kind == COMPACTION_FILE_READ) + return (files->read.len); + if (kind == COMPACTION_FILE_MODIFIED) + return (files->modified.len); + return (0); +} + +int +compaction_files_get(const struct compaction_files *files, + enum compaction_file_kind kind, size_t index, const void **path, + size_t *pathlen) +{ + const struct compaction_file_set *set; + + if (files == NULL || path == NULL || pathlen == NULL) + return (-1); + if (kind == COMPACTION_FILE_READ) + set = &files->read; + else if (kind == COMPACTION_FILE_MODIFIED) + set = &files->modified; + else + return (-1); + if (index >= set->len) + return (-1); + *path = set->files[index].path; + *pathlen = set->files[index].len; + return (0); +} + +static int writer_add(struct compact_writer *w, const void *data, size_t len) { if (w->out->len > w->maxlen || len > w->maxlen - w->out->len) @@ -78,6 +449,88 @@ record_begin(struct compact_writer *w, const char *lab } static int +file_path_add(struct compact_writer *writer, + const struct compaction_file *file) +{ + size_t i, start; + + start = 0; + for (i = 0; i < file->len; i++) { + const char *escaped; + + switch (file->path[i]) { + case '&': + escaped = "&"; + break; + case '<': + escaped = "<"; + break; + case '>': + escaped = ">"; + break; + default: + continue; + } + if (writer_add(writer, file->path + start, i - start) == -1 || + writer_addstr(writer, escaped) == -1) + return (-1); + start = i + 1; + } + return (writer_add(writer, file->path + start, file->len - start)); +} + +static int +file_block_add(struct compact_writer *writer, const char *name, + const struct compaction_file_set *set) +{ + size_t i; + + if (writer_addstr(writer, "<") == -1 || + writer_addstr(writer, name) == -1 || + writer_addstr(writer, ">\n") == -1) + return (-1); + for (i = 0; i < set->len; i++) { + if (file_path_add(writer, &set->files[i]) == -1 || + writer_addstr(writer, "\n") == -1) + return (-1); + } + if (writer_addstr(writer, "") == -1) + return (-1); + return (0); +} + +int +compaction_files_append(struct buf *out, const struct compaction_files *files, + size_t maxlen) +{ + struct compact_writer writer; + struct buf rendered; + + if (out == NULL || files == NULL || out->len > maxlen) + return (-1); + buf_init(&rendered); + memset(&writer, 0, sizeof(writer)); + writer.out = &rendered; + writer.maxlen = SIZE_MAX; + if ((out->len > 0 && writer_addstr(&writer, "\n\n") == -1) || + file_block_add(&writer, "read-files", &files->read) == -1 || + writer_addstr(&writer, "\n\n") == -1 || + file_block_add(&writer, "modified-files", &files->modified) == -1) + goto fail; + if (rendered.len > maxlen - out->len) + goto fail; + buf_add(out, rendered.data, rendered.len); + buf_free(&rendered); + return (0); + +fail: + buf_free(&rendered); + return (-1); +} + +static int tool_value_add(struct compact_writer *w, const struct json *json, int value) { const u_char *raw; blob - b3ac9a218e357dcc1899f34c2a50993aadd2ee91 blob + 62c4147f7e26c32d7ff9b860a4bdabea7274fd7d --- src/common/compaction.h +++ src/common/compaction.h @@ -20,10 +20,19 @@ #include struct buf; +struct compaction_files; struct msglist; #define COMPACTION_TOOL_RESULT_MAX 2000 +#define COMPACTION_FILE_PATH_MAX 4096 +#define COMPACTION_FILE_COUNT_MAX 4096 +#define COMPACTION_FILE_BYTES_MAX (1024 * 1024) +enum compaction_file_kind { + COMPACTION_FILE_READ, + COMPACTION_FILE_MODIFIED +}; + extern const char compaction_system_prompt[]; /* @@ -36,4 +45,26 @@ extern const char compaction_system_prompt[]; int compaction_serialize(struct buf *, const struct msglist *, const void *, size_t, size_t); +/* + * Structured file details carried across Compactions. Paths are retained + * with explicit lengths, in bytewise order. All mutating operations are + * transactional: validation or bound failure leaves the state unchanged. + */ +struct compaction_files *compaction_files_new(void); +void compaction_files_free(struct compaction_files *); +int compaction_files_add(struct compaction_files *, + enum compaction_file_kind, const void *, size_t); +int compaction_files_collect(struct compaction_files *, + const struct msglist *); +int compaction_files_merge(struct compaction_files *, + const struct compaction_files *); +size_t compaction_files_count(const struct compaction_files *, + enum compaction_file_kind); +int compaction_files_get(const struct compaction_files *, + enum compaction_file_kind, size_t, const void **, size_t *); + +/* Append both generated XML-like blocks within maxlen. */ +int compaction_files_append(struct buf *, const struct compaction_files *, + size_t); + #endif /* COMPACTION_H */