commit - c5f3a1cbb149227b342688a63cb3375c4be6c547
commit + 36f2bf37d293ba47b0c2c529451cfae948bb8cf0
blob - 1a4b63676dc080f78a50128e54b7bce0dca9a89e
blob + c878964fdd6faef25d24fdb12904056668e8e318
--- regress/turn/run.sh
+++ regress/turn/run.sh
$([ $? -ne 0 ]; echo $?)
stop_stub
+# A failed Compaction leaves both the live Projection and its acknowledged
+# cursor unchanged. The synthetic instruction is Provider-only: it is not a
+# pending message, and failure must neither record a compact event nor make the
+# following Turn claim a different cache boundary.
+rm -rf "$HOME/.fugu"
+rm -f "$dir/reqout"
+start_stub "OK ERROR OK"
+write_conf "$stubport"
+printf 'first question\n/compact\nsecond question\n/quit\n' > "$dir/lin"
+run_fugu_in "$dir/lin"
+ok "failed compact: line session continues" \
+ $([ "$rc" -eq 0 ]; echo $?)
+grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
+ok "failed compact: no compact event was recorded" \
+ $([ $? -ne 0 ]; echo $?)
+"$obj/reqcheck" "$dir/reqout"
+ok "failed compact: Projection and cursor survive unchanged" $?
+stop_stub
+
# --- 26: /context accounts for the compaction summary (behavior.md 3)
rm -rf "$HOME/.fugu"
start_stub "OK SUMMARY"
ok "picker: selected provider metadata drove the context window" $?
stop_stub
+ # Request a listing and immediately submit a slow-tool prompt; delayed
+ # selection must land between its two Generations. The selection cools
+ # the Projection cursor but must not terminate the Turn transaction.
+ rm -rf "$HOME/.fugu"
+ rm -f "$dir/reqout"
+ start_stub "SLEEPTOOL OK"
+ write_conf "$stubport"
+ printf '/model\rhello\r\000\000model-b\r\000\000\000\000/quit\r' | \
+ TERM=vt100 HOME=$HOME \
+ FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
+ FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=50 \
+ "$obj/ptydrive" "$fugu" >"$dir/pickerout" 2>&1
+ rc=$?
+ ok "late picker: active Turn survives model selection" \
+ $([ "$rc" -eq 0 ]; echo $?)
+ sed -n '/^--- SLEEPTOOL ---$/,/^--- OK ---$/p' "$dir/reqlog" | \
+ grep -q '"model":"claude-sonnet-4-6"'
+ ok "late picker: first Generation started on the old model" $?
+ sed -n '/^--- OK ---$/,$p' "$dir/reqlog" | \
+ grep -q '"model":"stub-model-b"'
+ ok "late picker: selection drives the next Generation" $?
+ n=$(grep -h '"outcome":"ok"' "$sessions"/*.ndjson 2>/dev/null | wc -l)
+ ok "late picker: exactly one active Turn commits" \
+ $([ "$n" -eq 1 ]; echo $?)
+ stop_stub
+
# An explicit context_limit remains the first-priority denominator.
rm -rf "$HOME/.fugu"
start_stub "OK"
blob - 7f06ece20e98b13ffd03d0d95533dac60072f8fc
blob + d3a5b25b1e3d7c524fd577fc257e6efab6dba794
--- regress/turn_txn/turn_txn_test.c
+++ regress/turn_txn/turn_txn_test.c
}
static void
+test_active_cache_reset_cools_cursor_without_ending_turn(void)
+{
+ struct fixture f;
+ struct msglist seed;
+ struct turn_txn_projection before_reset, current;
+
+ TAILQ_INIT(&seed);
+ fixture_new(&f, &seed);
+ turn_txn_begin(f.txn);
+ turn_txn_accept(f.txn, text_message(ROLE_USER, "one"));
+ projection(&f, ¤t);
+ turn_txn_projection_sent(f.txn, ¤t);
+ turn_txn_accept(f.txn, text_message(ROLE_ASSISTANT, "two"));
+ projection(&f, &before_reset);
+ CHECK(before_reset.message_count == 2 &&
+ before_reset.cache_tail_msg == 0);
+
+ turn_txn_cache_reset(f.txn);
+ projection(&f, ¤t);
+ CHECK(current.message_count == 2 && current.cache_tail_msg == -1 &&
+ current.token != before_reset.token);
+ CHECK(fatal_projection_sent(f.txn, &before_reset));
+ turn_txn_projection_sent(f.txn, ¤t);
+ turn_txn_accept(f.txn, text_message(ROLE_USER, "tool result"));
+ turn_txn_commit(f.txn, TURN_TXN_OK);
+ projection(&f, ¤t);
+ CHECK(current.message_count == 3 && current.cache_tail_msg == 1);
+
+ turn_txn_begin(f.txn);
+ turn_txn_accept(f.txn, text_message(ROLE_USER, "drop"));
+ turn_txn_cache_reset(f.txn);
+ turn_txn_abandon(f.txn);
+ projection(&f, ¤t);
+ CHECK(current.message_count == 3 && current.cache_tail_msg == -1);
+ turn_txn_free(f.txn);
+}
+
+static void
test_foreign_snapshot_is_rejected(void)
{
struct fixture one, two;
FATAL_COMMIT_IDLE,
FATAL_COMMIT_OUTCOME,
FATAL_ABANDON_IDLE,
- FATAL_RESET_ACTIVE,
+ FATAL_RESET_NULL,
FATAL_CLEAR_ACTIVE,
FATAL_CLEAR_CONTEXT,
FATAL_COMPACT_ACTIVE,
case FATAL_ABANDON_IDLE:
turn_txn_abandon(f.txn);
break;
- case FATAL_RESET_ACTIVE:
- turn_txn_begin(f.txn);
- turn_txn_cache_reset(f.txn);
+ case FATAL_RESET_NULL:
+ turn_txn_cache_reset(NULL);
break;
case FATAL_CLEAR_ACTIVE:
turn_txn_begin(f.txn);
test_success_is_durable_before_job_commit();
test_abandon_aborts_jobs_then_rolls_back_pending();
test_sent_snapshots_advance_and_stale_tokens_fail();
+ test_active_cache_reset_cools_cursor_without_ending_turn();
test_foreign_snapshot_is_rejected();
test_cap_does_not_claim_unsent_messages();
test_resume_clear_compact_and_cache_reset_ownership();
blob - 942a23cf5d400d1e79a25735bb7088635fd8235f
blob + ed92b0e4be6d6b4f5aa68f39e28f6d85e962b64b
--- src/fugu/coord.c
+++ src/fugu/coord.c
#include "skills.h"
#include "tooldefs.h"
#include "agentcfg.h"
+#include "turn_txn.h"
+#include "turn_txn_private.h"
#define TURN_ITER_MAX 50 /* behavior.md section 5 */
#define MAX_TOOL_CALLS 64 /* tool_use blocks per assistant msg */
int have_sigint_ev;
int cancelled;
- struct msglist conv; /* the canonical conversation */
+ struct turn_txn *turn; /* Lead Projection transaction */
struct journal *journal;
struct skillset *skills;
- int turn_n;
- int prev_msgs; /* count in the last request */
/* curses front end (behavior.md 2.1): output goes to fugu-tty */
int ui; /* terminal mode: render via tty */
}
static void
+turn_jobs_begin(void *arg)
+{
+ struct coord *c = arg;
+
+ tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_BEGIN);
+}
+
+static void
+turn_jobs_stop_executor(void *arg)
+{
+ struct coord *c = arg;
+
+ if (!c->tool_cancel_needed)
+ return;
+ (void)kill(c->kids[ROLE_TOOL].pid, SIGTERM);
+ c->tool_cancel_needed = 0;
+}
+
+static void
+turn_jobs_commit(void *arg)
+{
+ struct coord *c = arg;
+
+ tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_COMMIT);
+}
+
+static void
+turn_jobs_abort(void *arg)
+{
+ struct coord *c = arg;
+
+ tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_ABORT);
+}
+
+static void
cancel_signal(int sig, short event, void *arg)
{
struct coord *c = arg;
* pending set, so a pre-commit Ctrl-C always abandons and a post-boundary
* Ctrl-C cannot create a half-committed outcome. */
static int
-commit_turn(struct coord *c, enum turn_outcome outcome)
+commit_turn(struct coord *c, enum turn_txn_commit outcome)
{
sigset_t block, oldmask, pending;
int interrupted = 0;
if (!c->have_sigint_ev) {
- journal_turn_end(c->journal, c->turn_n, outcome);
- tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_COMMIT);
+ turn_txn_commit(c->turn, outcome);
return (0);
}
(void)event_loop(EVLOOP_NONBLOCK);
if (c->cancelled || sigismember(&pending, SIGINT)) {
c->cancelled = 1;
interrupted = 1;
- } else {
- journal_turn_end(c->journal, c->turn_n, outcome);
- tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_COMMIT);
- }
+ } else
+ turn_txn_commit(c->turn, outcome);
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1)
fatal("sigprocmask restore SIGINT");
return (interrupted ? -1 : 0);
}
static int
-msg_count(const struct msglist *l)
-{
- struct msg *m;
- int n = 0;
-
- TAILQ_FOREACH(m, l, entry)
- n++;
- return (n);
-}
-
-static int
protect_has(const struct buf *b, const char *path)
{
size_t off, len;
c->provider_window = sel.context_window;
free(c->conf->model);
c->conf->model = xstrdup(id);
- c->prev_msgs = 0; /* the cache starts cold (8) */
+ turn_txn_cache_reset(c->turn); /* the cache starts cold (8) */
send_ui_config(c); /* the status bar shows it */
emit_note(c, "model: %s", id);
break;
rebuild_system(c);
}
-/* Translate the canonical conversation into the active wire dialect. */
+/* Translate a borrowed Projection into the active wire dialect. */
static void
build_provider_request(struct coord *c, struct buf *body,
+ const struct msglist *messages, const struct msg *suffix,
const struct tool_def *tools, int ntools, int cache_tail_msg)
{
if (c->conf->provider_type == PROVIDER_OPENAI) {
req.systemlen = c->sysfull.len;
req.tools = tools;
req.ntools = ntools;
- req.conv = &c->conv;
+ req.conv = messages;
+ req.suffix = suffix;
openai_build_request(body, &req);
} else {
struct anthropic_req req;
req.systemlen = c->sysfull.len;
req.tools = tools;
req.ntools = ntools;
- req.conv = &c->conv;
+ req.conv = messages;
+ req.suffix = suffix;
req.cache = 1;
req.cache_tail_msg = cache_tail_msg;
anthropic_build_request(body, &req);
send_turn_request(struct coord *c)
{
const struct tool_def *base, *agent;
+ struct turn_txn_projection projection;
struct tool_def tools[20];
struct buf body;
int nbase, ntools;
tools[ntools++] = *agent;
}
buf_init(&body);
- build_provider_request(c, &body, tools, ntools,
- c->prev_msgs > 0 ? c->prev_msgs - 1 : -1);
+ turn_txn_projection(c->turn, &projection);
+ build_provider_request(c, &body, projection.messages, NULL, tools,
+ ntools, projection.cache_tail_msg);
if (body.len > FUGU_REQUEST_MAX) {
/*
* JSON escaping can expand piped context or a large tool
return (-1);
}
send_request(c, &body, c->lead_api_id, c->active_provider_slot);
+ turn_txn_projection_sent(c->turn, &projection);
buf_free(&body);
- c->prev_msgs = msg_count(&c->conv); /* messages just sent */
return (0);
}
return (um);
}
-/*
- * Discard every canonical message appended by an unsuccessful turn.
- * The Journal already treats TURN_ABANDONED transactionally; the live
- * Projection must have the identical last-successful state (I14).
- */
static int
-abandon_turn(struct coord *c, struct msg *first, int status)
+abandon_turn(struct coord *c, int status)
{
- struct msg *m, *next;
-
- /* A signal or drawer switch can break the Coordinator's wait while
- * fugu-tool is still inside a synchronous executor. Its SIGTERM hook
- * kills the published child group and lets dispatch resume to consume
- * the ordered ABORT. */
- if (c->tool_cancel_needed) {
- (void)kill(c->kids[ROLE_TOOL].pid, SIGTERM);
- c->tool_cancel_needed = 0;
- }
- tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_ABORT);
- journal_turn_end(c->journal, c->turn_n, TURN_ABANDONED);
- for (m = first; m != NULL; m = next) {
- next = TAILQ_NEXT(m, entry);
- TAILQ_REMOVE(&c->conv, m, entry);
- msg_free(m);
- }
+ turn_txn_abandon(c->turn);
reset_tcalls(c);
- c->prev_msgs = 0; /* failed requests restart caching cold */
buf_reset(&c->steer);
buf_reset(&c->steer_pend);
return (status);
}
static int
-cancel_turn(struct coord *c, struct msg *first)
+cancel_turn(struct coord *c)
{
int status;
- status = abandon_turn(c, first, 3);
+ status = abandon_turn(c, 3);
machine_error(c, "turn cancelled");
machine_finish(c, 3);
return (status);
}
static int
-fail_turn(struct coord *c, struct msg *first, int status,
- const char *message)
+fail_turn(struct coord *c, int status, const char *message)
{
int result;
- result = abandon_turn(c, first, status);
+ result = abandon_turn(c, status);
machine_error(c, message);
machine_finish(c, status);
return (result);
static int
run_turn(struct coord *c, const char *prompt, size_t promptlen)
{
- struct msg *um, *am, *first;
+ struct msg *um, *am;
int iter;
if (promptlen > FUGU_TURN_DATA_MAX) {
return (1);
}
um = msg_new(ROLE_USER);
- first = um;
msg_add_text(um, prompt, promptlen);
- TAILQ_INSERT_TAIL(&c->conv, um, entry);
- c->turn_n++;
memset(&c->turn_usage, 0, sizeof(c->turn_usage));
c->turn_tool_calls = 0;
c->turn_data = promptlen;
c->turn_data_exceeded = 0;
- journal_turn_begin(c->journal, c->turn_n);
- tool_turn_ctl(c, FUGU_IMSG_TOOL_TURN_BEGIN);
- journal_message(c->journal, um);
+ turn_txn_begin(c->turn);
+ turn_txn_accept(c->turn, um);
if (cancel_checkpoint(c))
- return (cancel_turn(c, first));
+ return (cancel_turn(c));
if (c->want_switch) {
- int status = abandon_turn(c, first, 1);
+ int status = abandon_turn(c, 1);
machine_error(c, "turn abandoned by session switch");
machine_finish(c, 1);
for (iter = 1; ; iter++) {
if (send_turn_request(c) == -1) {
- return (fail_turn(c, first, 1,
+ return (fail_turn(c, 1,
"request serialization failed"));
}
pump_stream(c);
post_stream_test_delay();
if (cancel_checkpoint(c))
- return (cancel_turn(c, first));
+ return (cancel_turn(c));
if (c->turn_data_exceeded) {
- int status = abandon_turn(c, first, 1);
+ int status = abandon_turn(c, 1);
emit_error(c, "fugu: turn data exceeds 4 MiB");
machine_error(c, "turn data exceeds 4 MiB");
/* a drawer switch abandons the in-flight turn cleanly
* (behavior.md 2.1, section 7); its steer text dies with it */
if (c->want_switch) {
- return (fail_turn(c, first, 1,
+ return (fail_turn(c, 1,
"turn abandoned by session switch"));
}
if (c->failed) {
const char *why = c->err[0] != '\0' ? c->err :
"turn failed";
- int status = abandon_turn(c, first, 1);
+ int status = abandon_turn(c, 1);
/* a failed turn contributes nothing (I14) */
emit_error(c, "fugu: %s", why);
/* neither text nor a tool call is empty content the
* provider would reject on the next request (I14) */
if (c->text.len == 0 && c->ntcalls == 0) {
- int status = abandon_turn(c, first, 1);
+ int status = abandon_turn(c, 1);
emit_error(c, "fugu: provider sent an empty reply");
machine_error(c, "provider sent an empty reply");
}
}
if (bad) {
- int status = abandon_turn(c, first, 1);
+ int status = abandon_turn(c, 1);
emit_error(c,
"fugu: provider sent malformed tool arguments");
}
}
if (cancel_checkpoint(c))
- return (cancel_turn(c, first));
+ return (cancel_turn(c));
am = build_assistant_msg(c);
- TAILQ_INSERT_TAIL(&c->conv, am, entry);
- journal_message(c->journal, am);
+ turn_txn_accept(c->turn, am);
machine_assistant(c);
if (cancel_checkpoint(c))
- return (cancel_turn(c, first));
+ return (cancel_turn(c));
/*
* Any tool_use blocks MUST be answered with tool_result on
*/
if (c->ntcalls == 0) {
/* the model is done: commit and show its text */
- if (commit_turn(c, TURN_OK) == -1)
- return (cancel_turn(c, first));
+ if (commit_turn(c, TURN_TXN_OK) == -1)
+ return (cancel_turn(c));
emit_answer(c);
machine_finish(c, 0);
return (0);
c->turn_data_exceeded ?
"turn data or worker result exceeded its bound" :
"turn abandoned by session switch";
- return (fail_turn(c, first, status, why));
+ return (fail_turn(c, status, why));
}
- TAILQ_INSERT_TAIL(&c->conv, um, entry);
- journal_message(c->journal, um);
+ turn_txn_accept(c->turn, um);
reset_tcalls(c);
if (cancel_checkpoint(c))
- return (cancel_turn(c, first));
+ return (cancel_turn(c));
if (iter >= TURN_ITER_MAX) {
- if (commit_turn(c, TURN_CAP) == -1)
- return (cancel_turn(c, first));
+ if (commit_turn(c, TURN_TXN_CAP) == -1)
+ return (cancel_turn(c));
emit_error(c, "fugu: reached the %d-step tool limit "
"for one turn", TURN_ITER_MAX);
machine_error(c, "tool-loop iteration limit reached");
static void
cmd_clear(struct coord *c)
{
- journal_clear(c->journal); /* resume also starts fresh */
- msglist_free(&c->conv);
- TAILQ_INIT(&c->conv);
- c->prev_msgs = 0; /* cold cache after a reset */
- buf_reset(&c->compaction); /* a reset voids the summary */
+ struct turn_txn_context context;
+ struct turn_txn_context *contextp = NULL;
/* the in-tree file is re-read after a clear (behavior.md 4) */
if (c->conf->project_context) {
ctx_read_project(c, f);
if (c->ctx_project.len == 0 && strcmp(f, "AGENTS.md") != 0)
ctx_read_project(c, "AGENTS.md");
- journal_context(c->journal,
- (const char *)c->ctx_personal.data, c->ctx_personal.len,
- (const char *)c->ctx_project.data, c->ctx_project.len);
+ context.personal = (const char *)c->ctx_personal.data;
+ context.personal_len = c->ctx_personal.len;
+ context.project = (const char *)c->ctx_project.data;
+ context.project_len = c->ctx_project.len;
+ contextp = &context;
}
- journal_sync(c->journal);
+ turn_txn_clear(c->turn, contextp);
+ buf_reset(&c->compaction); /* a reset voids the summary */
rebuild_system(c); /* drop the summary either way */
if (c->ui)
ui_send(c, FUGU_IMSG_UI_CLEARED, NULL, 0);
static int
cmd_compact(struct coord *c)
{
+ struct turn_txn_projection projection;
struct msg *instr;
struct buf body;
- if (TAILQ_EMPTY(&c->conv)) {
+ turn_txn_projection(c->turn, &projection);
+ if (projection.message_count == 0) {
emit_error(c, "fugu: nothing to compact");
return (1);
}
send_status(c, UI_SUMMARIZE); /* distinct busy state (2.1) */
- /* a synthetic user turn carrying the summarization instruction */
+ /* A Provider-only suffix carrying the summarization instruction. */
instr = msg_new(ROLE_USER);
msg_add_text(instr, COMPACT_INSTR, strlen(COMPACT_INSTR));
- TAILQ_INSERT_TAIL(&c->conv, instr, entry);
buf_init(&body);
- build_provider_request(c, &body, NULL, 0, -1); /* no tools (S3) */
+ build_provider_request(c, &body, projection.messages, instr, NULL, 0,
+ -1); /* no tools (S3) */
+ msg_free(instr);
if (body.len > FUGU_REQUEST_MAX) {
buf_free(&body);
- TAILQ_REMOVE(&c->conv, instr, entry);
- msg_free(instr);
emit_error(c, "fugu: too large to compact");
return (1);
}
c->turn_data = 0;
c->turn_data_exceeded = 0;
pump_stream(c);
- TAILQ_REMOVE(&c->conv, instr, entry);
- msg_free(instr);
if (cancel_checkpoint(c))
return (3);
if (cancel_checkpoint(c))
return (3);
- /* the summary replaces the conversation and becomes context */
+ /* The durable summary replaces the Projection and becomes context. */
+ turn_txn_compact(c->turn, c->text.data, c->text.len);
buf_reset(&c->compaction);
buf_add(&c->compaction, c->text.data, c->text.len);
- msglist_free(&c->conv);
- TAILQ_INIT(&c->conv);
- c->prev_msgs = 0;
- journal_compact(c->journal, (const char *)c->compaction.data,
- c->compaction.len);
- journal_sync(c->journal);
rebuild_system(c);
if (c->ui)
ui_send(c, FUGU_IMSG_UI_CLEARED, NULL, 0);
{
const char *sys = c->conf->system != NULL ? c->conf->system :
fugu_system_prompt;
+ struct turn_txn_projection projection;
struct msg *m;
size_t sysb = strlen(sys), prose = 0, tools = 0, largest = 0;
size_t ctxb = c->ctx_personal.len + c->ctx_project.len;
size_t compb = c->compaction.len;
int64_t win, total;
- int nmsg = 0;
+ int nmsg;
- TAILQ_FOREACH(m, &c->conv, entry) {
+ turn_txn_projection(c->turn, &projection);
+ nmsg = (int)projection.message_count;
+ TAILQ_FOREACH(m, projection.messages, entry) {
struct block *b;
size_t mb = 0;
}
if (mb > largest)
largest = mb;
- nmsg++;
}
total = est_tokens(sysb) + est_tokens(ctxb) + est_tokens(compb) +
est_tokens(prose) + est_tokens(tools);
enum output_mode output_mode, int debug, int verbose)
{
static struct coord c;
+ struct turn_txn_jobs turn_jobs;
+ struct msglist seed;
const char *libexec;
char sessdir[1024], id[SESSION_ID_MAX];
unsigned int i;
buf_init(&c.ctx_data);
buf_init(&c.steer);
buf_init(&c.steer_pend);
- TAILQ_INIT(&c.conv);
+ TAILQ_INIT(&seed);
libexec = libexec_dir();
c.libexec = libexec;
do {
c.nkids++;
}
- /* Open the journal now (still unrestricted). Resume replays
- * prior turns; the cache starts cold on resume (behavior.md 8),
- * so prev_msgs stays 0. */
+ /* Open the Journal now (still unrestricted). Resume replays into the
+ * seed; the Turn transaction constructor starts its cursor cold. */
if (journal_dir(sessdir, sizeof(sessdir)) == -1)
fatalx("cannot use ~/.fugu/sessions");
if (resume_id != NULL) {
- if (journal_replay(sessdir, resume_id, &c.conv) == -1)
+ if (journal_replay(sessdir, resume_id, &seed) == -1)
fatalx("unknown session: %s", resume_id);
if ((c.journal = journal_open(sessdir, resume_id)) == NULL)
fatalx("cannot reopen session %s", resume_id);
/* the in-tree context file is read via fugu-tool (no rpath here) */
context_post_bringup(&c, resume_id != NULL);
+ memset(&turn_jobs, 0, sizeof(turn_jobs));
+ turn_jobs.arg = &c;
+ turn_jobs.begin = turn_jobs_begin;
+ turn_jobs.stop_executor_if_live = turn_jobs_stop_executor;
+ turn_jobs.commit = turn_jobs_commit;
+ turn_jobs.abort = turn_jobs_abort;
+ c.turn = turn_txn_new(c.journal, &seed, &turn_jobs);
if (prompt != NULL) {
signal_set(&c.sigint_ev, SIGINT, cancel_signal, &c);
if (c.have_sigint_ev)
signal_del(&c.sigint_ev);
coord_teardown(&c);
+ turn_txn_free(c.turn);
+ c.turn = NULL;
journal_close(c.journal);
close(c.devnull_fd);
c.devnull_fd = -1;
skills_free(c.skills);
agentcfg_free(c.agentcfg);
- msglist_free(&c.conv);
reset_tcalls(&c);
free(c.sessions);
free(c.listed_models);
blob - cf405af16f58485c538794076e0a4edde1b491cb
blob + 78d9dfe99abc788cbd205f9c9dd5c32be8b083de
--- src/fugu/turn_txn.c
+++ src/fugu/turn_txn.c
void
turn_txn_cache_reset(struct turn_txn *txn)
{
- require_idle(txn);
+ require_txn(txn);
txn->sent_prefix_len = 0;
snapshot_changed(txn);
}
blob - 0d511e884776f1230014b121031ad3012c6b5294
blob + bb2d9bf523aa772d6df503f3c7dfa1ba0ae55fc7
--- src/fugu/turn_txn.h
+++ src/fugu/turn_txn.h
const struct turn_txn_projection *);
void turn_txn_commit(struct turn_txn *, enum turn_txn_commit);
void turn_txn_abandon(struct turn_txn *);
+/*
+ * Make the Projection cursor cold without changing transaction state.
+ * Valid while idle or active; invalidates every outstanding receipt.
+ */
void turn_txn_cache_reset(struct turn_txn *);
void turn_txn_clear(struct turn_txn *, const struct turn_txn_context *);
void turn_txn_compact(struct turn_txn *, const void *, size_t);