Stage B: logging policy documented, level-aware log-ring eviction built, LOG-FLUSH deferred again (FABRIC-3.md §XXXII.4)
Policy decided for the kernel-only audit scope: an interactive command's direct response stays on console_println/console_puts; everything else (state transitions, background diagnostics, audit trails) routes through log_message() at the appropriate level, matching capsule_mint.c's verify_mint() precedent. Documented, not code-swept here -- reclassifying individual sites is Stage D's job. LOG-FLUSH deferred again, explicitly: the per-VM log buffer its own doc comment presumes (vm_log_buffer.h) doesn't exist anywhere in the tree -- building it is real feature work needing its own scoped stage. Level-aware eviction built: log_region_append() now reads the oldest ring slot's own level before evicting it, protecting ERROR/WARN records from being pushed out by INFO/DEBUG churn -- drops the incoming low-priority record instead. Found and fixed an adjacent bug while making this change: the prior two-valued return contract would have made a benign "dropped by design" outcome indistinguishable from a genuine write failure to its one caller, which unconditionally set vm->error on any nonzero return. Changed to a three-valued contract (0 success, 1 dropped by design, -1 genuine failure). Three-arch clean qemu acceptance passed. Eviction path itself not live-exercised (needs 128+ LOG-APPEND calls to fill the ring) -- flagged, matching this project's own precedent for that kind of gap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5c5896fbc1
commit
c05b70c8d6
+43
@@ -4357,6 +4357,49 @@ they're in scope rather than silently colliding with them:**
|
|||||||
pass, or explicitly defer them again as separate follow-on work. Either is fine; leaving it
|
pass, or explicitly defer them again as separate follow-on work. Either is fine; leaving it
|
||||||
unstated is not, per this document's own "never silently drop a stale claim" rule.
|
unstated is not, per this document's own "never silently drop a stale claim" rule.
|
||||||
|
|
||||||
|
**Stage B -- DONE, 2026-09-15.**
|
||||||
|
|
||||||
|
**Policy, decided and now the standing rule for this scope (`mama_forth_words.c`, the 5
|
||||||
|
kernel-only `word_source/` files, `vm_core.c`, `repl.c` -- §XXXII.3's own scope):** an
|
||||||
|
interactive command's direct response stays on `console_println`/`console_puts` -- the human
|
||||||
|
typed something and is looking at the console for the answer, matching `USE`'s own fix and its
|
||||||
|
five sibling guards. Everything else -- internal state transitions, background/idle-tick
|
||||||
|
diagnostics, audit trails, anything not a direct reply to a line just typed -- routes through
|
||||||
|
`log_message()` at the appropriate level, matching `capsule_mint.c`'s `verify_mint()` (§IX.5)
|
||||||
|
precedent. Counted, not guessed: `mama_forth_words.c` has 103 `console_println`/`console_puts`
|
||||||
|
call sites (`repl.c` has 18); most are direct interactive replies already correctly placed under
|
||||||
|
this rule -- reclassifying the specific sites that aren't is Stage D's job (the same files Stage
|
||||||
|
C's triage already walks), not repeated here.
|
||||||
|
|
||||||
|
**Decision on the two §XXVII gaps: fix eviction now, defer `LOG-FLUSH` again.**
|
||||||
|
`LOG-FLUSH` needs real new machinery first -- the per-VM log buffer its own doc comment already
|
||||||
|
presumes (`vm_log_buffer.h`) does not exist anywhere in the tree, confirmed by grep; building it
|
||||||
|
means designing that buffer format and the flush word together, genuine feature work deserving
|
||||||
|
its own scoped stage, not a rider on this one. **Deferred again, explicitly, as its own future
|
||||||
|
initiative** -- not silently dropped.
|
||||||
|
|
||||||
|
Level-aware eviction, by contrast, was a contained fix to one existing function -- **built**.
|
||||||
|
`log_region.c`'s `log_region_append()` now reads the oldest slot's own level
|
||||||
|
(new `log_region_read_slot()`, mirroring the existing `log_region_write_slot()`) before evicting
|
||||||
|
it: if the oldest record is `LOG_ERROR`/`LOG_WARN` (`LOG_REGION_PROTECTED_MAX_LEVEL`,
|
||||||
|
`log_region.h`) and the incoming record is not, the incoming record is dropped instead --
|
||||||
|
protecting real error history from being pushed out by INFO/DEBUG churn. Any other combination
|
||||||
|
(oldest already low-priority, or the incoming record is itself ERROR/WARN) evicts exactly as
|
||||||
|
before. **A real, adjacent bug found and fixed while making this change**: the prior two-valued
|
||||||
|
contract (`log_region_append()` returns 0 or -1) would have made a benign "dropped by design"
|
||||||
|
outcome indistinguishable from a genuine disk-write failure to its one caller
|
||||||
|
(`log_word_append_raw()`, `log_words.c`), which unconditionally set `vm->error` on any nonzero
|
||||||
|
return -- meaning a protected-eviction drop would have spuriously triggered a fault report on
|
||||||
|
whatever VM's turn happened to trigger it. Changed to a three-valued contract (0 success, 1
|
||||||
|
dropped by design, -1 genuine failure) and updated the one caller to only fault on `-1`.
|
||||||
|
|
||||||
|
**Verified:** three-architecture `clean qemu` acceptance (amd64/aarch64/riscv64) passed, standard
|
||||||
|
regression unaffected. **Not live-exercised against real eviction** -- the ring only starts
|
||||||
|
evicting after 128 records (`LOG_REGION_MAX_DEVBLOCKS` x `LOG_SLOTS_PER_DEVBLOCK`), and no
|
||||||
|
existing boot path calls `LOG-APPEND` by default; filling the ring live would need 128+ manual
|
||||||
|
`LOG-APPEND` calls, out of proportion for this pass. Flagged, not chased, matching this project's
|
||||||
|
own precedent for a compiled-but-not-failure-path-verified change (§IX.5's `verify_mint()` note).
|
||||||
|
|
||||||
### XXXII.5 -- Proposed staging and sequencing
|
### XXXII.5 -- Proposed staging and sequencing
|
||||||
|
|
||||||
Matching §XXVIII's own gated-stage shape (each stage its own commit, its own full 3-architecture
|
Matching §XXVIII's own gated-stage shape (each stage its own commit, its own full 3-architecture
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Capsule Block Manifest — Auto-generated
|
# Capsule Block Manifest — Auto-generated
|
||||||
<!-- Generated by mkcapsule --manifest 2026-09-15T12:53:40Z -->
|
<!-- Generated by mkcapsule --manifest 2026-09-15T17:51:41Z -->
|
||||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||||
<!-- Hand-written justifications and immutability notes live -->
|
<!-- Hand-written justifications and immutability notes live -->
|
||||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||||
|
|||||||
Binary file not shown.
@@ -90,6 +90,15 @@ extern "C" {
|
|||||||
#define LOG_REGION_GROWTH_INCREMENT 4u
|
#define LOG_REGION_GROWTH_INCREMENT 4u
|
||||||
#define LOG_REGION_MAX_DEVBLOCKS 32u /* ceiling -- devblock_from_top stays within [66,96] */
|
#define LOG_REGION_MAX_DEVBLOCKS 32u /* ceiling -- devblock_from_top stays within [66,96] */
|
||||||
|
|
||||||
|
/* FABRIC-3.md §XXXII.4, 2026-09-15: level-aware eviction. Deliberately a
|
||||||
|
* raw numeric threshold, not log.h's LogLevel enum -- this file stays
|
||||||
|
* decoupled from log_message() entirely (see this header's own top-level
|
||||||
|
* doc comment on why). Matches LogLevel's own ordering (LOG_ERROR=0,
|
||||||
|
* LOG_WARN=1, LOG_INFO=2...): a slot at this level or lower is "protected"
|
||||||
|
* -- eviction refuses to discard it for an incoming record above this
|
||||||
|
* threshold, dropping the incoming record instead. */
|
||||||
|
#define LOG_REGION_PROTECTED_MAX_LEVEL 1u /* LOG_WARN and below (ERROR, WARN) */
|
||||||
|
|
||||||
#define LOG_SLOTS_PER_DEVBLOCK 4u /* 4 x 1 KiB forth-blocks per 4 KiB devblock */
|
#define LOG_SLOTS_PER_DEVBLOCK 4u /* 4 x 1 KiB forth-blocks per 4 KiB devblock */
|
||||||
#define LOG_SLOT_SIZE 1024u
|
#define LOG_SLOT_SIZE 1024u
|
||||||
|
|
||||||
@@ -162,9 +171,14 @@ typedef char log_slot_size_check[(sizeof(log_slot_t) == LOG_SLOT_SIZE) ? 1 : -1]
|
|||||||
* @param source_len Length of source (truncated to LOG_SLOT_SOURCE_MAX-1).
|
* @param source_len Length of source (truncated to LOG_SLOT_SOURCE_MAX-1).
|
||||||
* @param msg Message text (not NUL-terminated required).
|
* @param msg Message text (not NUL-terminated required).
|
||||||
* @param msg_len Length of msg (truncated to LOG_SLOT_MSG_MAX).
|
* @param msg_len Length of msg (truncated to LOG_SLOT_MSG_MAX).
|
||||||
* @return 0 on success, -1 on any read/write failure (ring left however
|
* @return 0 on success. 1 if this record was dropped BY DESIGN, not a
|
||||||
* the failed operation left it -- blk_meta_zone_write() itself
|
* failure -- writing it would have evicted a higher-priority
|
||||||
* never partially writes a devblock).
|
* record still in the ring (LOG_REGION_PROTECTED_MAX_LEVEL,
|
||||||
|
* §XXXII.4); the ring is left completely unchanged. -1 on any
|
||||||
|
* genuine read/write failure (ring left however the failed
|
||||||
|
* operation left it -- blk_meta_zone_write() itself never
|
||||||
|
* partially writes a devblock). Callers that treat any nonzero
|
||||||
|
* return as an error must not conflate these two cases.
|
||||||
*/
|
*/
|
||||||
int log_region_append(uint8_t level, uint64_t timestamp,
|
int log_region_append(uint8_t level, uint64_t timestamp,
|
||||||
const char *source, uint32_t source_len,
|
const char *source, uint32_t source_len,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,16 @@ static uint32_t log_region_slot_devblock_from_top(uint32_t slot_index) {
|
|||||||
return LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / LOG_SLOTS_PER_DEVBLOCK);
|
return LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / LOG_SLOTS_PER_DEVBLOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int log_region_read_slot(uint32_t slot_index, log_slot_t *out) {
|
||||||
|
uint8_t devblock_buf[4096];
|
||||||
|
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
|
||||||
|
uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK;
|
||||||
|
|
||||||
|
if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1;
|
||||||
|
memcpy(out, devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, sizeof(*out));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) {
|
static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) {
|
||||||
uint8_t devblock_buf[4096];
|
uint8_t devblock_buf[4096];
|
||||||
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
|
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
|
||||||
@@ -117,7 +127,29 @@ int log_region_append(uint8_t level, uint64_t timestamp,
|
|||||||
total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
|
total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
|
||||||
}
|
}
|
||||||
if (ctrl.record_count >= total_slots) {
|
if (ctrl.record_count >= total_slots) {
|
||||||
/* Still full (growth ceiling already reached) -- evict oldest. */
|
/* Still full (growth ceiling already reached) -- evict oldest,
|
||||||
|
* unless doing so would discard higher-priority history for a
|
||||||
|
* lower-priority newcomer (FABRIC-3.md §XXXII.4, 2026-09-15:
|
||||||
|
* plain FIFO eviction here was flagged as level-blind -- a
|
||||||
|
* flood of INFO/DEBUG noise could push out real ERROR/WARN
|
||||||
|
* records). Read the oldest slot's own level before evicting:
|
||||||
|
* if it's ERROR/WARN and the incoming record is not, protect
|
||||||
|
* it -- drop the incoming record instead of evicting real
|
||||||
|
* history. Any other combination (oldest is already low
|
||||||
|
* priority, or the incoming record is itself ERROR/WARN)
|
||||||
|
* evicts exactly as before. A read failure on the oldest slot
|
||||||
|
* (should not happen -- it was written by this same function)
|
||||||
|
* falls back to the prior unconditional-eviction behavior
|
||||||
|
* rather than wedging the ring. */
|
||||||
|
log_slot_t oldest;
|
||||||
|
int oldest_protected = 0;
|
||||||
|
if (log_region_read_slot(ctrl.head_slot, &oldest) == 0) {
|
||||||
|
oldest_protected = (oldest.level <= LOG_REGION_PROTECTED_MAX_LEVEL);
|
||||||
|
}
|
||||||
|
int incoming_protected = (level <= LOG_REGION_PROTECTED_MAX_LEVEL);
|
||||||
|
if (oldest_protected && !incoming_protected) {
|
||||||
|
return 1; /* dropped by design, not a failure -- see header */
|
||||||
|
}
|
||||||
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
|
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
|
||||||
ctrl.record_count--;
|
ctrl.record_count--;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,9 +239,12 @@ static void log_word_append_raw(VM *vm)
|
|||||||
source[i] = '\0';
|
source[i] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* log_region_append()'s return is three-valued (see log_region.h):
|
||||||
|
* 0 success, 1 dropped by design (protected a higher-priority record,
|
||||||
|
* not a fault -- no vm->error), -1 a genuine write failure. */
|
||||||
if (log_region_append((uint8_t)level, (uint64_t)timestamp,
|
if (log_region_append((uint8_t)level, (uint64_t)timestamp,
|
||||||
source, (uint32_t)strlen(source),
|
source, (uint32_t)strlen(source),
|
||||||
msg, (uint32_t)msg_u) != 0) {
|
msg, (uint32_t)msg_u) < 0) {
|
||||||
vm->error = 1;
|
vm->error = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user