commit 83f2c983cb279c2381c039c3947997ebde1ce2e1 from: Isaac Meerleo date: Fri Jul 17 21:24:23 2026 UTC Add immutable System epoch state commit - 323a47d9cab66665f0a5e7b0432a69d1c942642f commit + 83f2c983cb279c2381c039c3947997ebde1ce2e1 blob - f278d6717aee499f26d1b62c8196bc9d57ed0acd blob + d583c71ff2f09d11db233da953a5df273a6cee84 --- regress/Makefile +++ regress/Makefile @@ -1,7 +1,7 @@ # Long integration suites lead so scripts/check can overlap their tails with # shorter proofs; this order is intentionally not alphabetical. SUBDIR= journal turn print agent agentcfg anthropic buf check claude_subscription compaction conf deploy diff editor generation http imsgev json \ - local_port log markdown model_window openai output sandbox skills sse term tool_display tools \ + local_port log markdown model_window openai output sandbox skills sse system_epoch term tool_display tools \ turn_mechanics turn_txn ui_config ui_layout web welcome xmalloc .include blob - /dev/null blob + 9cd0d4146353034b117cb93bcd35bcfd27028f78 (mode 644) --- /dev/null +++ regress/system_epoch/Makefile @@ -0,0 +1,8 @@ +PROG= system_epoch_test +SRCS= system_epoch_test.c system_epoch.c buf.c log.c xmalloc.c +NOMAN= yes + +.PATH: ${.CURDIR}/../../src/fugu +CFLAGS+= -I${.CURDIR}/../../src/fugu + +.include blob - /dev/null blob + 03f2f68bd17979f0b2124a0f8c79a36b18a06687 (mode 644) --- /dev/null +++ regress/system_epoch/system_epoch_test.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include "system_epoch.h" + +#include "regress.h" + +static int +buf_is(const struct buf *b, const void *want, size_t len) +{ + return (b->len == len && (len == 0 || memcmp(b->data, want, len) == 0)); +} + +static void +test_exact_variants(void) +{ + static const char full[] = + "BASE\n\nPERSONAL\n\nPROJECT\n\n" + "Summary of the conversation so far (your own continuation notes):" + "\n\nSUMMARY"; + static const char restricted[] = + "BASE\n\nSummary of the conversation so far " + "(your own continuation notes):\n\nSUMMARY"; + struct system_epoch_input input; + struct system_epoch epoch; + + system_epoch_init(&epoch); + memset(&input, 0, sizeof(input)); + input.reason = SYSTEM_EPOCH_START; + input.base.data = "BASE"; + input.base.len = 4; + input.personal.data = "PERSONAL"; + input.personal.len = 8; + input.project.data = "PROJECT"; + input.project.len = 7; + input.summary.data = "SUMMARY"; + input.summary.len = 7; + CHECK(system_epoch_build(&epoch, &input) == 0); + CHECK(epoch.version == SYSTEM_EPOCH_VERSION); + CHECK(epoch.reason == SYSTEM_EPOCH_START); + CHECK(buf_is(&epoch.base, "BASE", 4)); + CHECK(buf_is(&epoch.personal, "PERSONAL", 8)); + CHECK(buf_is(&epoch.project, "PROJECT", 7)); + CHECK(buf_is(&epoch.summary, "SUMMARY", 7)); + CHECK(buf_is(&epoch.full, full, sizeof(full) - 1)); + CHECK(buf_is(&epoch.restricted, restricted, + sizeof(restricted) - 1)); + CHECK(system_epoch_select(&epoch, 1) == &epoch.full); + CHECK(system_epoch_select(&epoch, 0) == &epoch.restricted); + system_epoch_free(&epoch); +} + +static void +test_owned_state(void) +{ + char base[] = "owned-base"; + char personal[] = "owned-personal"; + char project[] = "owned-project"; + char summary[] = "owned-summary"; + struct system_epoch_input input; + struct system_epoch epoch, copy; + + system_epoch_init(&epoch); + system_epoch_init(©); + memset(&input, 0, sizeof(input)); + input.reason = SYSTEM_EPOCH_CLEAR; + input.base.data = base; + input.base.len = strlen(base); + input.personal.data = personal; + input.personal.len = strlen(personal); + input.project.data = project; + input.project.len = strlen(project); + input.summary.data = summary; + input.summary.len = strlen(summary); + CHECK(system_epoch_build(&epoch, &input) == 0); + base[0] = personal[0] = project[0] = summary[0] = 'X'; + CHECK(buf_is(&epoch.base, "owned-base", 10)); + CHECK(buf_is(&epoch.personal, "owned-personal", 14)); + CHECK(buf_is(&epoch.project, "owned-project", 13)); + CHECK(buf_is(&epoch.summary, "owned-summary", 13)); + CHECK(system_epoch_select(&epoch, 0) == &epoch.restricted); + CHECK(buf_is(&epoch.personal, "owned-personal", 14)); + CHECK(buf_is(&epoch.project, "owned-project", 13)); + CHECK(system_epoch_copy(©, &epoch) == 0); + epoch.base.data[0] = epoch.personal.data[0] = 'Y'; + CHECK(buf_is(©.base, "owned-base", 10)); + CHECK(buf_is(©.personal, "owned-personal", 14)); + CHECK(copy.reason == SYSTEM_EPOCH_CLEAR); + system_epoch_free(©); + system_epoch_free(&epoch); +} + +static void +test_invalid_input_is_transactional(void) +{ + static const char base_nul[] = { 'b', '\0', 'x' }; + static const char personal_nul[] = { 'p', '\0', 'x' }; + static const char project_nul[] = { 'r', '\0', 'x' }; + struct system_epoch_input input; + struct system_epoch epoch; + + system_epoch_init(&epoch); + memset(&input, 0, sizeof(input)); + input.reason = SYSTEM_EPOCH_MIGRATION; + input.base.data = "KEEP"; + input.base.len = 4; + input.personal.data = "P"; + input.personal.len = 1; + CHECK(system_epoch_build(&epoch, &input) == 0); + + input.reason = (enum system_epoch_reason)99; + CHECK(system_epoch_build(&epoch, &input) == -1); + input.reason = SYSTEM_EPOCH_CLEAR; + input.base.data = NULL; + CHECK(system_epoch_build(&epoch, &input) == -1); + input.base.data = base_nul; + input.base.len = sizeof(base_nul); + CHECK(system_epoch_build(&epoch, &input) == -1); + input.base.data = "B"; + input.base.len = 1; + input.personal.data = personal_nul; + input.personal.len = sizeof(personal_nul); + CHECK(system_epoch_build(&epoch, &input) == -1); + input.personal.data = "P"; + input.personal.len = 1; + input.project.data = project_nul; + input.project.len = sizeof(project_nul); + CHECK(system_epoch_build(&epoch, &input) == -1); + + input.project.data = NULL; + input.project.len = 0; + input.base.data = "x"; + input.base.len = SYSTEM_EPOCH_BASE_MAX + 1; + CHECK(system_epoch_build(&epoch, &input) == -1); + input.base.len = 1; + input.personal.data = "x"; + input.personal.len = SYSTEM_EPOCH_EXTERNAL_MAX + 1; + CHECK(system_epoch_build(&epoch, &input) == -1); + input.personal.data = NULL; + input.personal.len = 0; + input.summary.data = "x"; + input.summary.len = SYSTEM_EPOCH_SUMMARY_MAX + 1; + CHECK(system_epoch_build(&epoch, &input) == -1); + input.summary.data = NULL; + input.summary.len = 0; + input.base.data = "x"; + input.base.len = SYSTEM_EPOCH_VARIANT_MAX; + input.personal.data = "y"; + input.personal.len = 1; + CHECK(system_epoch_build(&epoch, &input) == -1); + + CHECK(epoch.version == SYSTEM_EPOCH_VERSION); + CHECK(epoch.reason == SYSTEM_EPOCH_MIGRATION); + CHECK(buf_is(&epoch.base, "KEEP", 4)); + CHECK(buf_is(&epoch.personal, "P", 1)); + CHECK(buf_is(&epoch.full, "KEEP\n\nP", 7)); + CHECK(buf_is(&epoch.restricted, "KEEP", 4)); + system_epoch_free(&epoch); +} + +static void +test_all_reasons_and_binary_summary(void) +{ + static const u_char base[] = { 'B', 0xff }; + static const u_char personal[] = { 'P', 0xfe }; + static const u_char project[] = { 'R', 0xfd }; + static const u_char summary[] = { 'S', '\0', 0xfc }; + static const char marker[] = + "\n\nSummary of the conversation so far " + "(your own continuation notes):\n\n"; + static const enum system_epoch_reason reasons[] = { + SYSTEM_EPOCH_START, + SYSTEM_EPOCH_CLEAR, + SYSTEM_EPOCH_COMPACT, + SYSTEM_EPOCH_MIGRATION + }; + struct system_epoch_input input; + struct system_epoch epoch, copy; + size_t i, off; + + system_epoch_init(&epoch); + system_epoch_init(©); + memset(&input, 0, sizeof(input)); + input.base.data = base; + input.base.len = sizeof(base); + input.personal.data = personal; + input.personal.len = sizeof(personal); + input.project.data = project; + input.project.len = sizeof(project); + input.summary.data = summary; + input.summary.len = sizeof(summary); + for (i = 0; i < sizeof(reasons) / sizeof(reasons[0]); i++) { + input.reason = reasons[i]; + CHECK(system_epoch_build(&epoch, &input) == 0); + CHECK(epoch.reason == reasons[i]); + CHECK(epoch.version == SYSTEM_EPOCH_VERSION); + } + CHECK(buf_is(&epoch.base, base, sizeof(base))); + CHECK(buf_is(&epoch.personal, personal, sizeof(personal))); + CHECK(buf_is(&epoch.project, project, sizeof(project))); + CHECK(buf_is(&epoch.summary, summary, sizeof(summary))); + off = sizeof(base); + CHECK(epoch.full.len == sizeof(base) + 2 + sizeof(personal) + 2 + + sizeof(project) + sizeof(marker) - 1 + sizeof(summary)); + CHECK(memcmp(epoch.full.data, base, sizeof(base)) == 0); + CHECK(memcmp(epoch.full.data + off, "\n\n", 2) == 0); + off += 2 + sizeof(personal); + CHECK(memcmp(epoch.full.data + off, "\n\n", 2) == 0); + off += 2 + sizeof(project); + CHECK(memcmp(epoch.full.data + off, marker, sizeof(marker) - 1) == 0); + CHECK(memcmp(epoch.full.data + epoch.full.len - sizeof(summary), + summary, sizeof(summary)) == 0); + CHECK(epoch.restricted.len == sizeof(base) + sizeof(marker) - 1 + + sizeof(summary)); + CHECK(memcmp(epoch.restricted.data, base, sizeof(base)) == 0); + CHECK(memcmp(epoch.restricted.data + sizeof(base), marker, + sizeof(marker) - 1) == 0); + CHECK(memcmp(epoch.restricted.data + epoch.restricted.len - + sizeof(summary), summary, sizeof(summary)) == 0); + CHECK(system_epoch_copy(©, &epoch) == 0); + CHECK(buf_is(©.summary, summary, sizeof(summary))); + CHECK(buf_is(©.full, epoch.full.data, epoch.full.len)); + CHECK(buf_is(©.restricted, epoch.restricted.data, + epoch.restricted.len)); + system_epoch_free(©); + system_epoch_free(&epoch); +} + +static void +test_copy_failure_is_transactional(void) +{ + struct system_epoch_input input; + struct system_epoch dst, src; + + system_epoch_init(&dst); + system_epoch_init(&src); + memset(&input, 0, sizeof(input)); + input.reason = SYSTEM_EPOCH_START; + input.base.data = "DESTINATION"; + input.base.len = 11; + CHECK(system_epoch_build(&dst, &input) == 0); + input.reason = SYSTEM_EPOCH_COMPACT; + input.base.data = "SOURCE"; + input.base.len = 6; + CHECK(system_epoch_build(&src, &input) == 0); + src.full.data[0] = 'X'; + CHECK(system_epoch_copy(&dst, &src) == -1); + CHECK(dst.reason == SYSTEM_EPOCH_START); + CHECK(buf_is(&dst.base, "DESTINATION", 11)); + CHECK(buf_is(&dst.full, "DESTINATION", 11)); + src.version = SYSTEM_EPOCH_VERSION + 1; + CHECK(system_epoch_copy(&dst, &src) == -1); + CHECK(buf_is(&dst.full, "DESTINATION", 11)); + system_epoch_free(&src); + system_epoch_free(&dst); +} + +int +main(void) +{ + test_exact_variants(); + test_owned_state(); + test_invalid_input_is_transactional(); + test_all_reasons_and_binary_summary(); + test_copy_failure_is_transactional(); + REGRESS_END(); +} blob - /dev/null blob + 14e9c5e26ee7067784bd3d946062195c7e02ef71 (mode 644) --- /dev/null +++ src/fugu/system_epoch.c @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include "buf.h" +#include "system_epoch.h" + +static const char summary_prefix[] = + "\n\nSummary of the conversation so far " + "(your own continuation notes):\n\n"; + +static int +reason_valid(enum system_epoch_reason reason) +{ + return (reason == SYSTEM_EPOCH_START || reason == SYSTEM_EPOCH_CLEAR || + reason == SYSTEM_EPOCH_COMPACT || + reason == SYSTEM_EPOCH_MIGRATION); +} + +static int +span_valid(const struct system_epoch_span *span) +{ + return (span->data != NULL || span->len == 0); +} + +static int +size_add(size_t *total, size_t add) +{ + if (SIZE_MAX - *total < add) + return (-1); + *total += add; + return (0); +} + +static int +input_valid(const struct system_epoch_input *input) +{ + size_t full, restricted, summary = 0; + + if (input == NULL || !reason_valid(input->reason) || + !span_valid(&input->base) || !span_valid(&input->personal) || + !span_valid(&input->project) || !span_valid(&input->summary) || + input->base.len > SYSTEM_EPOCH_BASE_MAX || + input->personal.len > SYSTEM_EPOCH_EXTERNAL_MAX || + input->project.len > SYSTEM_EPOCH_EXTERNAL_MAX || + input->summary.len > SYSTEM_EPOCH_SUMMARY_MAX) + return (0); + full = restricted = input->base.len; + if (input->personal.len > 0 && + (size_add(&full, 2) == -1 || + size_add(&full, input->personal.len) == -1)) + return (0); + if (input->project.len > 0 && + (size_add(&full, 2) == -1 || + size_add(&full, input->project.len) == -1)) + return (0); + if (input->summary.len > 0) { + summary = sizeof(summary_prefix) - 1; + if (size_add(&summary, input->summary.len) == -1 || + size_add(&full, summary) == -1 || + size_add(&restricted, summary) == -1) + return (0); + } + if (full > SYSTEM_EPOCH_VARIANT_MAX || + restricted > SYSTEM_EPOCH_VARIANT_MAX) + return (0); + if ((input->base.len > 0 && memchr(input->base.data, '\0', + input->base.len) != NULL) || + (input->personal.len > 0 && memchr(input->personal.data, '\0', + input->personal.len) != NULL) || + (input->project.len > 0 && memchr(input->project.data, '\0', + input->project.len) != NULL)) + return (0); + return (1); +} + +static void +add_span(struct buf *out, const struct system_epoch_span *span) +{ + if (span->len > 0) + buf_add(out, span->data, span->len); +} + +static void +add_external(struct buf *out, const struct system_epoch_span *span) +{ + if (span->len == 0) + return; + buf_addstr(out, "\n\n"); + add_span(out, span); +} + +static void +add_summary(struct buf *out, const struct system_epoch_span *summary) +{ + if (summary->len == 0) + return; + buf_add(out, summary_prefix, sizeof(summary_prefix) - 1); + add_span(out, summary); +} + +static int +buf_equal(const struct buf *a, const struct buf *b) +{ + return (a->len == b->len && + (a->len == 0 || memcmp(a->data, b->data, a->len) == 0)); +} + +void +system_epoch_init(struct system_epoch *epoch) +{ + memset(epoch, 0, sizeof(*epoch)); + buf_init(&epoch->base); + buf_init(&epoch->personal); + buf_init(&epoch->project); + buf_init(&epoch->summary); + buf_init(&epoch->full); + buf_init(&epoch->restricted); +} + +void +system_epoch_free(struct system_epoch *epoch) +{ + buf_free(&epoch->base); + buf_free(&epoch->personal); + buf_free(&epoch->project); + buf_free(&epoch->summary); + buf_free(&epoch->full); + buf_free(&epoch->restricted); + memset(epoch, 0, sizeof(*epoch)); +} + +int +system_epoch_build(struct system_epoch *epoch, + const struct system_epoch_input *input) +{ + struct system_epoch next; + + if (epoch == NULL || !input_valid(input)) + return (-1); + + system_epoch_init(&next); + next.version = SYSTEM_EPOCH_VERSION; + next.reason = input->reason; + add_span(&next.base, &input->base); + add_span(&next.personal, &input->personal); + add_span(&next.project, &input->project); + add_span(&next.summary, &input->summary); + + add_span(&next.full, &input->base); + add_external(&next.full, &input->personal); + add_external(&next.full, &input->project); + add_summary(&next.full, &input->summary); + add_span(&next.restricted, &input->base); + add_summary(&next.restricted, &input->summary); + + system_epoch_free(epoch); + *epoch = next; + return (0); +} + +int +system_epoch_copy(struct system_epoch *dst, const struct system_epoch *src) +{ + struct system_epoch_input input; + struct system_epoch next; + + if (dst == NULL || src == NULL || src->version != SYSTEM_EPOCH_VERSION) + return (-1); + if (dst == src) + return (0); + memset(&input, 0, sizeof(input)); + input.reason = src->reason; + input.base.data = src->base.data; + input.base.len = src->base.len; + input.personal.data = src->personal.data; + input.personal.len = src->personal.len; + input.project.data = src->project.data; + input.project.len = src->project.len; + input.summary.data = src->summary.data; + input.summary.len = src->summary.len; + system_epoch_init(&next); + if (system_epoch_build(&next, &input) == -1 || + !buf_equal(&next.full, &src->full) || + !buf_equal(&next.restricted, &src->restricted)) { + system_epoch_free(&next); + return (-1); + } + system_epoch_free(dst); + *dst = next; + return (0); +} + +const struct buf * +system_epoch_select(const struct system_epoch *epoch, int project_context) +{ + if (epoch == NULL) + return (NULL); + return (project_context ? &epoch->full : &epoch->restricted); +} blob - /dev/null blob + cfe01a1a5d4bb697e413f1e2ed212a8a04a603fa (mode 644) --- /dev/null +++ src/fugu/system_epoch.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SYSTEM_EPOCH_H +#define SYSTEM_EPOCH_H + +#include + +#include + +#include "buf.h" +#include "proto.h" + +#define SYSTEM_EPOCH_VERSION 1U +#define SYSTEM_EPOCH_BASE_MAX FUGU_REQUEST_MAX +#define SYSTEM_EPOCH_EXTERNAL_MAX FUGU_CTX_MAX +#define SYSTEM_EPOCH_SUMMARY_MAX FUGU_TURN_DATA_MAX +#define SYSTEM_EPOCH_VARIANT_MAX FUGU_REQUEST_MAX + +enum system_epoch_reason { + SYSTEM_EPOCH_START, + SYSTEM_EPOCH_CLEAR, + SYSTEM_EPOCH_COMPACT, + SYSTEM_EPOCH_MIGRATION +}; + +struct system_epoch_span { + const void *data; + size_t len; +}; + +struct system_epoch_input { + enum system_epoch_reason reason; + struct system_epoch_span base; + struct system_epoch_span personal; + struct system_epoch_span project; + struct system_epoch_span summary; +}; + +/* + * One owned immutable System baseline epoch. base, personal, and project + * are NUL-free; summary and the derived variants may contain NUL. All spans + * are otherwise opaque bytes: this Module deliberately does not validate or + * normalize UTF-8. Each external span is capped at FUGU_CTX_MAX, summary at + * FUGU_TURN_DATA_MAX, and base and either complete variant at + * FUGU_REQUEST_MAX. A Provider codec still checks final JSON expansion. + */ +struct system_epoch { + uint32_t version; + enum system_epoch_reason reason; + struct buf base; + struct buf personal; + struct buf project; + struct buf summary; + struct buf full; + struct buf restricted; +}; + +void system_epoch_init(struct system_epoch *); +void system_epoch_free(struct system_epoch *); +/* On failure, a previously built destination remains byte-identical. */ +int system_epoch_build(struct system_epoch *, + const struct system_epoch_input *); +int system_epoch_copy(struct system_epoch *, const struct system_epoch *); +/* Borrowed until the epoch is rebuilt or freed. */ +const struct buf *system_epoch_select(const struct system_epoch *, int); + +#endif /* SYSTEM_EPOCH_H */