/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file frame_capture_fixtures.h * @brief Synthetic tool-call argument values reproducing the FRAME CAPTURE * failure shapes: what lands in a parameter VALUE when a turn is * force-closed mid-thought. * * The content (project "lumen", its paths and its analysis text) is * invented. The shapes are not: each fixture keeps the structure, * approximate length and escape pattern of a failure observed in real * serving -- the leading "", the prose lead-in, the call * dialect, over-closed braces, shell metacharacters, code fences and a * truncated SEARCH/REPLACE block. * * WHAT THEY SHOW. The forced tool grammar compels the XML frame; when a * turn is force-closed mid-thought (the serve INJECTS "" once the * per-model reasoning_budget is spent -- batch_sched.c driver_reason_step, * serve_generation.c sg_run_completion), the grammar cannot tell that * close from the model's own, sets reasoning_ended, and compels the * opener from the next char. Whatever the model was still going to say * then lands in the free-text parameter VALUE. FX_NATIVE_XML_CAPTURE is * the control: the captured text there is a NATIVE Qwen-XML call, the * very dialect the grammar armed, so the capture is not dialect drift. * * @status None * @issues None * @todo None */ #ifndef PN_TEST_FRAME_CAPTURE_FIXTURES_H #define PN_TEST_FRAME_CAPTURE_FIXTURES_H /* A `grep` frame's `pattern` argument, ~60 chars. * THE FALSIFYING CASE. The model's own followed by a truncated * NATIVE Qwen-XML `read` call -- the exact dialect the grammar armed. No * dialect drift happened here, so no dialect theory explains the capture; the * turn was force-closed mid-thought. RECOVERABLE. */ static const char FX_NATIVE_XML_CAPTURE[] = "\n\n\n\nlumen/serve.log"; /* A `read` frame's `path` argument, ~140 chars. * The model's own , then a prose lead-in, then a complete call in the * Hermes `call_function` shape. The leading is the tell: the model * had NOT finished reasoning when the frame was compelled, so it wrote the * close it was always going to write INTO the value. RECOVERABLE. */ static const char FX_HERMES_LEADIN_CAPTURE[] = "\n\nLet me look at the Makefile to understand the build structur" "e:\n\ncall_function{\"func\":\"read\",\"args\":{\"path\":\"lu" "men/Makefile\"}}"; /* A `read` frame's `path` argument, ~100 chars. * Bare `call_function` call, over-closed ("}}}" where two braces close the * object). RECOVERABLE. */ static const char FX_OVERCLOSED_CAPTURE[] = "call_function{\"func\":\"read\",\"args\":{\"path\":\"lumen/in" "clude/lumen/settings_cache.h\"}}}"; /* A `read` frame's `path` argument, ~150 chars. * Prose lead-in then a complete `call_function` call, absolute path. * RECOVERABLE. */ static const char FX_ABSOLUTE_PATH_CAPTURE[] = "Let me find the correct path for settings_cache.h:\n\ncall_fu" "nction{\"func\":\"read\",\"args\":{\"path\":\"/home/user/src/lumen/inclu" "de/settings_cache.h\"}}}"; /* A `glob` frame's `pattern` argument, ~190 chars. * A shell pipeline plus a dangling markdown fence in a `glob` pattern. Not a * complete call in any shape: NOTHING TO RECOVER, and NO marker to key on. * The trailing token is cut mid-word, as a truncated emission is. */ static const char FX_SHELL_IN_GLOB[] = "lumen/include/**/*.h* | head -20; echo \"---\"; find lumen -name \"setti" "ngs_cache.h\" 2>/dev/null; echo \"---\"; ls lumen/src/settings/*.h 2>/de" "v/null; echo \"---\"; ls lume-4471093821\n```"; /* A `glob` frame's `pattern` argument, over 5,000 chars. * Mostly analysis prose, with C source (braces!) and a TRUNCATED SEARCH/REPLACE * edit block at the end. Not a complete call, and the braces must not fool a * JSON scanner into manufacturing one. NOTHING TO RECOVER, NO marker. */ static const char FX_ANALYSIS_DUMP[] = "lumen/include/lumen/settings_cache.h*lumen/src/settings/settings_cache.c" ":212` is the `log_warn(\"settings_cach**e: load failed...` line. The pro" "blem is that the loader reads a `version` field that older settings file" "s never wrote, and then bails out because it cannot find the key.\n\nLet" " me walk through what happens step by step:\n1. `settings_cache_open` re" "ads the whole file and hands it to `parse_settings`, which expects `vers" "ion` at the top level\n2. When a file written by an older build is opene" "d, the parser returns no error for the missing key, but the later lookup" " of `version` returns NULL and the caller treats that as corruption\n\nT" "he fix should be:\n1. Treat a missing `version` as version 1 instead of " "as a hard failure\n2. Run the upgrade steps in order, from whatever vers" "ion the file declares up to the current one\n\nLet me check how the othe" "r loaders deal with this - particularly anything named UPGRADE_STEPS or " "similar:\n* Around line 140-151 there is already an UPGRADE_STEPS tabl" "e of functions that rewrite older layouts. The trouble is that step 2 re" "names `cache_dir` to `cache_root` unconditionally, which corrupts a file" " that already uses the new name.\n\nWhat actually needs to change:\n* " "Check the declared version before running each step\n* Make each step " "idempotent rather than assuming it runs exactly once\n\nLet me see what " "happens when step 2 runs on a file that already has `cache_root`. The ol" "d key is absent, so the rename copies a NULL value over the good one. Th" "at is the \"empty cache_root\" report.\n\nReading the flow again more ca" "refully:\n1. Line 205 calls parse_settings, which builds the tree - this" " succeeds even when keys are missing\n2. Line 209-210 loops over UPGRADE" "_STEPS and applies every one of them\n\nThe issue is that on a second st" "art the steps run again, and nothing records that they already ran. The " "return value is ignored too - looking at line 209-210:\n\n```c\nfor (siz" "e_t i = 0; i < sizeof(UPGRADE_STEPS) / sizeof(*UPGRADE_STEPS); i++)\n " " UPGRADE_STEPS[i](cache->root);\n```\n\nNothing checks whether a step fa" "iled! So a failing rename is silently swallowed, and that is NOT the rea" "l issue on its own. The real issue shows up at line 205:\n\n```c\nif (pa" "rse_settings(cache->text, &cache->root, &err) != 0) {\n log_warn(\"se" "ttings_cache: load failed: %s\", err ? err : \"?\");\n free(err);\n``" "`\n\nHold on - parse_settings also validates that `cache_root` is a non-" "empty string. The validation runs BEFORE the upgrade steps, so on an old" " file the key is still named `cache_dir`, validation fails, and the upgr" "ade never gets a chance to run. Let me look at line 74:\n\n```c\n{ \"cac" "he_root\", KIND_STRING, REQUIRED },\n```\n\nThat entry will reject any f" "ile that predates the rename! And because validation and parsing happen " "in the same call, the whole load fails when that one entry fails.\n\nSo " "the fix is one of:\n1. Run validation separately, after the upgrade step" "s have rewritten the tree \n2. Or mark `cache_root` optional and check " "it after upgrading\n\nThe cleanest fix is to make sure every upgrade ste" "p runs BEFORE validation, and to make each step check its own preconditi" "on so that running it twice is harmless.\n\nActually, looking again at l" "ine 146:\n```c\n{ 2, rename_cache_dir },\n```\n\nThis IS in UPGRADE_STEP" "S, which runs AFTER parse_settings. But parse_settings already applied t" "he schema table from line 74, and that happens INSIDE the same call that" " builds the tree. \n\nWhen parsing and validation share one call, a sing" "le failed rule fails the whole call and returns no tree at all. So when " "the `cache_root` rule runs against a file that still says `cache_dir` (b" "ecause the rename has not happened yet), it FAILS and throws away the pa" "rsed tree, including the parts that were perfectly fine.\n\nThe fix is t" "o move validation OUT of parse_settings, or split it so it only runs onc" "e the upgrade steps have renamed the key. Better approach: have parse_se" "ttings only build the tree, then run the upgrade steps, then validate th" "e result with a clear message for each rule that fails.\n\nI also want t" "o double-check the writer side, because if `settings_cache_save` writes " "the file before validation there is a window where a half-upgraded tree " "reaches disk. Looking at line 260-268, save is only called from `setting" "s_cache_commit`, and commit is only reachable after a successful open, s" "o that window does not exist today. Good - nothing to change there.\n\nO" "ne more thing worth confirming is the version bump itself. After the las" "t step runs, the loader must write `version` back as the current number," " otherwise every start replays all of the steps from version 1. Line 214" " does set it, but only on the success path, which is exactly the path th" "at never runs for an old file. Once validation moves after the steps, th" "at line becomes reachable and the bump happens.\n\nLet me implement the " "fix:\n```c|CODE_EDIT|lumen/src/settings/settings_cache.c\n<<<<<<< SEARCH" "\n/* The full key table, in the order validate_settings walks it.\n * On" "e definition so a new key can never be validated in some loaders and not" "\n * others \xe2\x80\x94 that asymmetry is exactly how `cache_root` woul" "d have gone missing. */\n#define SETTINGS_KEYS " " \\"; /* A `read` frame's `path` argument, ~230 chars. * The ORIGINAL wedge shape: a bash pipeline captured into a `read` path. The * model wanted `bash`, but nothing in the value says so. NOTHING TO RECOVER, * and NO marker -- content sniffing cannot reach this family at all. */ static const char FX_SHELL_IN_READ_PATH[] = "/home/user/src/lumen/src/core/settings_cache.c.md 2>/dev/null; find /hom" "e/user/src -path \"*/src/settings_cache.c\" 2>/dev/null | head -5; find " "/home/user/sr -name \"settings_cache.c\" 2>/dev/null | grep lumen"; #endif /* PN_TEST_FRAME_CAPTURE_FIXTURES_H */