feat(log): LOG-FLUSH -- a VM's log records reach Artemis's log ring; and the ring keeps its order when it grows

FABRIC-3 SXXXII.4's deferred LOG-FLUSH, built to the 2026-10-14 rulings
(SXXXVIII): everything a VM logs, at or above the log level, goes into its
own buffer; it goes to Artemis on LOG-FLUSH, when full, and at once on an
ERROR, as one kernel-Hermes LOG_BATCH message she writes on her own turn,
the source being the sender the kernel stamped.  Records that cannot go yet
are counted and reported with the next flush.  LOG-SHOW (Hera only) reads
the ring back -- it had no reader.

Found by the first test that filled it: ring growth overwrote the oldest
record and put blank slots inside the live range.  Fixed test first.

Three ISAs: POST 1052 passed; new parity baseline word_count=526
hash=0x82f4ae7cb2e2d424 (deliberate).  CLAUDE.md: Phase 8 PKI closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 12:10:01 -04:00
co-authored by Claude Opus 5.5
parent 5e26fad30d
commit 4da370f492
35 changed files with 91102 additions and 15 deletions
+10
View File
@@ -63,6 +63,8 @@
* message line length (renamed LOG_MSG_LINE_MAX, 256) no longer share a
* name, so include order is irrelevant here. */
#include "starkernel/repl.h"
#include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */
#include "starkernel/vm_log_buffer.h" /* vm_log_buffer_add() */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "log.h"
#include "vm_host.h"
@@ -335,6 +337,14 @@ void log_message(LogLevel level, const char *fmt, ...) {
console_puts(reset);
console_puts(prefix);
console_println(buf);
/* And into the log buffer of the VM it belongs to, for Artemis's log
* ring (2026-10-14, FABRIC-3.md SXXXVIII). A record made outside any
* VM's text -- the kernel's own -- belongs to none and is not kept. */
{
struct VM *owner = vm_log_attributed_vm();
if (owner) vm_log_buffer_add(owner, (uint8_t)idx, rel_tsc, buf, (uint32_t)strlen(buf));
}
}
/**
+11
View File
@@ -55,6 +55,7 @@
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_current_vm() */
#include "starkernel/log_attrib.h" /* one path: vm_log_attributed_vm() */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: sk_vm_switch_signal_mark_work() */
#include "starkernel/vm_log_buffer.h" /* vm_log_batch_deliver() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
@@ -656,6 +657,16 @@ int sk_hermes_drain_checkpoint(VM *vm) {
console_println(line);
}
/* A log batch is records, not FORTH text: written to Artemis's log ring
* here, in her own context (2026-10-14, FABRIC-3.md SXXXVIII). */
if (msg->type == SK_HERMES_MSG_TYPE_LOG_BATCH) {
sk_hermes_pending_pop(self);
(void)vm_log_batch_deliver(vm, msg->from, (const uint8_t *)msg->payload_addr,
msg->payload_len);
sk_hermes_release(msg);
return 1;
}
/* Off the queue BEFORE it runs (2026-10-14): running it can reach a
* colon-word checkpoint that drains this same VM again, and that
* drain must find the next message, not this one -- popped only after,
+143
View File
@@ -0,0 +1,143 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2026 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* vm_log_buffer.c - a VM's log records, on their way to Artemis's log ring.
* See the header for the contract (FABRIC-3.md §XXXVIII, 2026-10-14).
*/
#ifdef __STARKERNEL__
#include "starkernel/vm_log_buffer.h"
#include "vm.h"
#include "log.h" /* LOG_ERROR, LOG_WARN */
#include "starkernel/vm/kernel_hermes.h" /* sk_hermes_send_one(), SK_HERMES_MSG_TYPE_LOG_BATCH */
#include "starkernel/capsule_birth.h" /* capsule_vm_find_by_name_nocase(), capsule_vm_registry_get() */
#include "starkernel/log_region.h" /* log_region_append() */
#include "starkernel/console.h" /* console_println() -- never log_message() here */
#include "freestanding/stdio.h" /* snprintf() */
#include <string.h>
#define REC_HEAD 11u /* level 1 + timestamp 8 + length 2 */
typedef char vm_log_buf_fits[(VM_LOG_BUF_SIZE + 64 <= SK_HERMES_CHUNK_MAX_PAYLOAD) ? 1 : -1];
/* Set while a record is being added or a buffer sent: a log line made from
* inside that work (a kernel-Hermes refusal, say) must not re-enter it. */
static int g_busy;
static uint32_t put_record(uint8_t *at, uint8_t level, uint64_t ts,
const char *msg, uint16_t len) {
at[0] = level;
memcpy(at + 1, &ts, 8);
memcpy(at + 9, &len, 2);
memcpy(at + REC_HEAD, msg, len);
return REC_HEAD + len;
}
static uint32_t count_records(const uint8_t *buf, uint32_t len) {
uint32_t at = 0, n = 0;
while (at + REC_HEAD <= len) {
uint16_t l;
memcpy(&l, buf + at + 9, 2);
at += REC_HEAD + l;
n++;
}
return n;
}
static int send_buffer(VM *vm) {
uint8_t payload[SK_HERMES_CHUNK_MAX_PAYLOAD];
uint32_t len = 0, records = count_records(vm->log_buf, vm->log_buf_len);
VMRegistryEntry artemis;
if (vm->log_buf_len == 0 && vm->log_lost == 0) return 0;
if (vm->log_lost > 0) {
char note[64];
int n = snprintf(note, sizeof(note), "%u log records not sent: Artemis not ready",
(unsigned)vm->log_lost);
if (n > 0) len += put_record(payload, LOG_WARN, 0, note, (uint16_t)n);
}
memcpy(payload + len, vm->log_buf, vm->log_buf_len);
len += vm->log_buf_len;
if (capsule_vm_find_by_name_nocase("Artemis", &artemis) != 0 || !artemis.vm_ptr ||
(artemis.state != VM_STATE_LIVE && artemis.state != VM_STATE_SWITCHED_OUT) ||
sk_hermes_send_one(vm->stadium_vm_id, artemis.vm_id, SK_HERMES_MSG_TYPE_LOG_BATCH,
0, payload, len) != 0) {
vm->log_lost += records;
vm->log_buf_len = 0;
return -1;
}
vm->log_lost = 0;
vm->log_buf_len = 0;
return (int)records;
}
void vm_log_buffer_add(VM *vm, uint8_t level, uint64_t timestamp,
const char *msg, uint32_t len) {
if (!vm || !msg) return;
if (g_busy) { vm->log_lost++; return; }
g_busy = 1;
if (len > VM_LOG_BUF_SIZE - REC_HEAD) len = VM_LOG_BUF_SIZE - REC_HEAD;
if (vm->log_buf_len + REC_HEAD + len > VM_LOG_BUF_SIZE) (void)send_buffer(vm);
vm->log_buf_len = (uint16_t)(vm->log_buf_len +
put_record(vm->log_buf + vm->log_buf_len, level, timestamp, msg, (uint16_t)len));
if (level == LOG_ERROR) (void)send_buffer(vm);
g_busy = 0;
}
int vm_log_buffer_flush(VM *vm) {
int rc;
if (!vm || g_busy) return -1;
g_busy = 1;
rc = send_buffer(vm);
g_busy = 0;
return rc;
}
int vm_log_batch_deliver(VM *vm, VMUuid from, const uint8_t *payload, uint32_t payload_len) {
VMRegistryEntry artemis, sender;
const char *source = "?";
uint32_t at = 0;
int written = 0, failed = 0;
if (capsule_vm_find_by_name_nocase("Artemis", &artemis) != 0 || artemis.vm_ptr != (void *)vm) {
return -1;
}
/* The source is the sender the kernel stamped on the message: a VM
* cannot write a record under another's name. */
if (capsule_vm_registry_get(from, &sender) == 0) source = sender.name;
while (at + REC_HEAD <= payload_len) {
uint8_t level = payload[at];
uint64_t ts;
uint16_t len;
memcpy(&ts, payload + at + 1, 8);
memcpy(&len, payload + at + 9, 2);
if (at + REC_HEAD + len > payload_len) break; /* malformed tail: stop */
if (log_region_append(level, ts, source, (uint32_t)strlen(source),
(const char *)payload + at + REC_HEAD, len) < 0) {
failed++;
} else {
written++;
}
at += REC_HEAD + len;
}
if (failed) console_println("Artemis: log ring write failed -- records not kept");
return written;
}
#endif /* __STARKERNEL__ */