commit - dcf6fb79009f4db25858a81e627d201087852dfd
commit + 0716373ef1fefafca2d72d1ba8a4be8189504a0b
blob - 4cb427fd13a4a04ec24f3c601d4fa615d91a5669
blob + 558868ffb6292c86f70e4ad0fa0c6184be41d09c
--- handoff/behavior.md
+++ handoff/behavior.md
One ordinary entry completes its whole Turn, including every
Generation and Tool, before another `UI_END` can remove the next
-entry. Complete Steers received before the same safe Tool-result
-boundary are combined in Owner-selection order and appended as text
-to that boundary's one canonical User message before the next
-Provider dispatch. If a successful final Generation leaves no such
-boundary, a complete Steer already received by the coordinator at
-the commit gate starts the immediate next Turn before `UI_END` can
-dispatch ordinary Queue work. A failed or abandoned Turn discards
-both its delivered and buffered Steers.
+entry. Complete Steers accepted within the existing aggregate input
+bound before the same safe Tool-result boundary are combined in
+Owner-selection order and appended as text to that boundary's one
+canonical User message before the next Provider dispatch; an entry
+that would exceed the bound is dropped with a notice. If a successful
+final Generation leaves no such boundary, a complete Steer already
+received by the coordinator at the commit gate starts the immediate
+next Turn before `UI_END` can dispatch ordinary Queue work. A failed
+or abandoned Turn discards both its delivered and buffered Steers.
Ordinary Queue entries may remain after an ordinary Turn error while
the same front end survives. Queue state is not durable: quit, crash,
crash, and session-switch re-exec do not preserve it.
- Print-mode turns (`-p`) append under the same rules; `fugu -r id
-p "..."` continues a saved session headlessly.
-- A turn ended by the iteration cap (§5) is a *completed* turn: its
+- A turn ended by the Tool-batch cap (§5) is a *completed* turn: its
exchanges up to the cap are journaled context and survive resume;
the cap error ends the turn, it does not unwind it. Failure and
abandonment (which do unwind) are the only discarding ends.
blob - bf39c9d0a7be7fbc92d4ba48153547a3a3ad1e03
blob + 5f9df1209a5f60dd4c83b60661ca45f545ced8d7
--- handoff/verification.md
+++ handoff/verification.md
acceptance, Provider dispatch, and Turn commit or abandonment. It
asserts one ordered canonical User message for multiple Steers at a
Tool boundary and exact committed replay; one FIFO removal per
- `UI_END`, including a first queued Turn that continues through a
- Tool; interleaved Steer priority without FIFO reordering; and a
- final-response Steer running before ordinary Queue work. Provider
- failure, cancellation, session-switch re-exec, and resume fixtures
- assert that delivered or buffered Steers do not leak and that
- undispatched Queue text reaches neither a Provider request nor a
- committed Journal Projection. `regress/turn_mechanics` sets a
- two-batch limit, adds one and several Steers to the first result
- batch, and asserts `TURN_STATUS_CAP` after exactly two batches with
- no allowance reset.
+ `UI_END` when the Queue is nonempty, including a first queued Turn
+ that continues through a Tool; and interleaved Steer priority
+ without FIFO reordering. It also proves that a complete
+ final-response Steer received by the coordinator at the commit gate
+ runs before ordinary Queue work. Provider-failure, cancellation,
+ session-switch re-exec, and resume fixtures assert that Steers
+ associated with the abandoned Turn reach neither a later Projection
+ nor resume. Undispatched Queue text reaches no Provider request or
+ Journal record in those loss paths and is absent from the resumed
+ Projection. `regress/turn_mechanics` sets a two-batch limit, adds one
+ and several Steers to the first result batch, and asserts
+ `TURN_STATUS_CAP` after exactly two batches with no allowance reset.
- **Freshness guard:** modifying a file between `read` and
`edit`/`write`/`multi_edit` yields the re-read refusal; the guard
resets across resume.
blob - 7da7e7c0e2d7f00481bb03c2be583418686d49d7
blob + 262a0930c5ca8ea077a200efe4dcabb185a30b21
--- regress/turn/reqcheck.c
+++ regress/turn/reqcheck.c
}
static int
+sleep_tool_message_is(const struct json *j, int msg)
+{
+ int block, content, input;
+
+ if (!message_role_is(j, msg, "assistant") ||
+ (content = json_obj_get(j, msg, "content")) == -1 ||
+ !json_is_array(j, content) || json_arr_len(j, content) != 1 ||
+ (block = json_arr_get(j, content, 0)) == -1 ||
+ !json_is_object(j, block) ||
+ !string_is(j, json_obj_get(j, block, "type"), "tool_use") ||
+ !string_is(j, json_obj_get(j, block, "id"), "toolu_zz") ||
+ !string_is(j, json_obj_get(j, block, "name"), "shell") ||
+ (input = json_obj_get(j, block, "input")) == -1 ||
+ !json_is_object(j, input) ||
+ !string_is(j, json_obj_get(j, input, "command"),
+ "sleep 2; echo slept-ok"))
+ return (0);
+ return (1);
+}
+
+static int
tool_text_message_is(const struct json *j, int msg, const char *tool_id,
const char *text)
{
!json_is_object(j, block) ||
!string_is(j, json_obj_get(j, block, "type"), "tool_result") ||
!string_is(j, json_obj_get(j, block, "tool_use_id"), tool_id) ||
+ !string_is(j, json_obj_get(j, block, "content"), "slept-ok\n") ||
(block = json_arr_get(j, content, 1)) == -1 ||
!json_is_object(j, block) ||
!string_is(j, json_obj_get(j, block, "type"), "text") ||
n = json_arr_len(j, messages);
if (n < 3 || !message_is(j, json_arr_get(j, messages, n - 3),
"user", prompt) ||
- !message_role_is(j, json_arr_get(j, messages, n - 2), "assistant"))
+ !sleep_tool_message_is(j, json_arr_get(j, messages, n - 2)))
return (0);
last = json_arr_get(j, messages, n - 1);
if (!message_role_is(j, last, "user") ||
(block = json_arr_get(j, content, 0)) == -1 ||
!json_is_object(j, block) ||
!string_is(j, json_obj_get(j, block, "type"), "tool_result") ||
- !string_is(j, json_obj_get(j, block, "tool_use_id"), "toolu_zz"))
+ !string_is(j, json_obj_get(j, block, "tool_use_id"), "toolu_zz") ||
+ !string_is(j, json_obj_get(j, block, "content"), "slept-ok\n"))
return (0);
return (1);
}
if (json_arr_len(&j, messages) != 3 ||
!message_is(&j, json_arr_get(&j, messages, 0), "user",
"ACTIVE-TURN") ||
- !message_role_is(&j, json_arr_get(&j, messages, 1),
- "assistant") ||
+ !sleep_tool_message_is(&j,
+ json_arr_get(&j, messages, 1)) ||
!steer_batch_message_is(&j,
json_arr_get(&j, messages, 2)) ||
steer_batch_count(&j, messages) != 1)
}
if (steer_replay) {
if (json_arr_len(&j, messages) != 11 ||
+ !sleep_tool_message_is(&j,
+ json_arr_get(&j, messages, 1)) ||
!steer_batch_message_is(&j,
json_arr_get(&j, messages, 2)) ||
steer_batch_count(&j, messages) != 1 ||
blob - e61c8ce18bbf46cb70e92ec7ef44b4bc5bb7d0fc
blob + 3aafeed64c4a0d64a3cfe44ea0bac160be1d9bed
--- regress/turn_mechanics/turn_mechanics_test.c
+++ regress/turn_mechanics/turn_mechanics_test.c
static const struct {
const char *text;
size_t len;
+ size_t round;
} cases[] = {
- { "steer-a", sizeof("steer-a") - 1 },
- { "steer-a\n\nsteer-b", sizeof("steer-a\n\nsteer-b") - 1 }
+ { "steer-a", sizeof("steer-a") - 1, 0 },
+ { "steer-a\n\nsteer-b", sizeof("steer-a\n\nsteer-b") - 1, 0 },
+ { "steer-b", sizeof("steer-b") - 1, 1 }
};
struct fixture f;
struct turn_adapter adapter;
struct turn_result result;
struct msg *message;
struct block *block;
- size_t i;
+ const char *other_id, *text_id;
+ size_t i, other_message_index, text_message_index;
int rc;
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
fixture_init(&f, &adapter, "q");
script_tool_round(&f, "round-a");
- f.tools[0].text = cases[i].text;
- f.tools[0].text_len = cases[i].len;
script_tool_round(&f, "round-b");
+ f.tools[cases[i].round].text = cases[i].text;
+ f.tools[cases[i].round].text_len = cases[i].len;
options_init(&options, TURN_POLICY_LEAD, 1, 2);
rc = turn_run(&adapter, &options, &result);
f.sent_callbacks == 2 && message_count(&f.accepted) == 5 &&
f.projection_count[0] == 1 && f.projection_count[1] == 3 &&
f.add_text_rc == 0);
- message = message_at(&f.accepted, 2);
+ text_message_index = cases[i].round == 0 ? 2 : 4;
+ other_message_index = cases[i].round == 0 ? 4 : 2;
+ text_id = cases[i].round == 0 ? "round-a" : "round-b";
+ other_id = cases[i].round == 0 ? "round-b" : "round-a";
+ message = message_at(&f.accepted, text_message_index);
block = message == NULL ? NULL : TAILQ_FIRST(&message->blocks);
CHECK(message != NULL && message->role == ROLE_USER &&
block != NULL && block->type == BLOCK_TOOL_RESULT &&
- strcmp(block->tool_use_id, "round-a") == 0 &&
+ strcmp(block->tool_use_id, text_id) == 0 &&
block->result_len == 2 && memcmp(block->result, "ok", 2) == 0);
block = block == NULL ? NULL : TAILQ_NEXT(block, entry);
CHECK(block != NULL && block->type == BLOCK_TEXT &&
block->textlen == cases[i].len &&
memcmp(block->text, cases[i].text, cases[i].len) == 0 &&
TAILQ_NEXT(block, entry) == NULL);
- message = message_at(&f.accepted, 4);
+ message = message_at(&f.accepted, other_message_index);
block = message == NULL ? NULL : TAILQ_FIRST(&message->blocks);
CHECK(message != NULL && message->role == ROLE_USER &&
block != NULL && block->type == BLOCK_TOOL_RESULT &&
- strcmp(block->tool_use_id, "round-b") == 0 &&
+ strcmp(block->tool_use_id, other_id) == 0 &&
block->result_len == 2 && memcmp(block->result, "ok", 2) == 0 &&
TAILQ_NEXT(block, entry) == NULL);
msglist_free(&f.accepted);