FABRIC-2.md §I.9: fix the terminal phantom-linebreak defect
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

console_ensure_line_start() (hal/console.c) used to emit its newline
immediately via a path that deliberately skipped the tx-byte counter, so
that sk_repl_idle()'s unconditional per-beat call to it (repl.c, ~1s idle
heartbeat) could force a real newline the REPL's own prompt-reanchor logic
never noticed -- the prompt was never reprinted, and the next real
keystroke echoed onto the now-blank line, indistinguishable from Enter
having already been pressed at a bare prompt. Root-caused in the previous
commit (704573b); this commit applies the fix per explicit go-ahead.

Fix: defer the newline instead of emitting it eagerly.
console_ensure_line_start() now only sets a flag (g_pending_line_close);
the newline is realized -- for real, and counted by g_console_tx_count
like any other output -- on the next actual console_putc() call, or
silently discarded via the new console_cancel_deferred_line_start() if the
caller decides nothing was actually printed. sk_repl_idle() captures
tx_before_idle right after its console_ensure_line_start() call and cancels
the deferred newline at both of its exit points when console_tx_count()
hasn't moved. A beat with nothing to report now leaves the console
untouched; a beat that does print still closes the dangling prompt line
first, properly counted this time. The pre-existing n > 0 mid-edit gate in
sk_console_readline() is untouched -- independent purpose, not the bug.

Verified via the mandatory foreground 3-arch QEMU acceptance boot
(clean qemu, amd64 -> aarch64 -> riscv64, one at a time): all three reached
(zuse) ok>, all echoed the first real input on the same log line as the
prompt rather than a fresh line, all shut down cleanly via BYE. amd64's log
additionally shows live human backspace-correction still glued to the same
prompt line. Logs: logs/20260905-015854 (amd64), logs/20260905-020248
(aarch64), logs/20260905-020552 (riscv64); logs/20260905-015813 is a
foreground-rule-violation retry killed and redone correctly, kept per this
project's "never delete logs/" convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
This commit is contained in:
Robert Allan James
2026-09-05 02:08:06 -04:00
co-authored by Claude Sonnet 5
parent 704573bdfc
commit 8edb95b65d
13 changed files with 27108 additions and 26 deletions
+29 -7
View File
@@ -260,16 +260,33 @@ static void emit_prefix(void) {
* Also mirrors output to the framebuffer VT100 terminal when available.
* Serial output is ALWAYS active regardless of framebuffer state.
*
* console_putc_inner(): the same body minus the tx-counter increment.
* console_ensure_line_start() uses the inner form so its format-only
* newline does not read as "real output" to the REPL's prompt re-anchor
* (repl.c) -- a silent idle beat that merely closes a dangling prompt line
* must not be mistaken for chatter and trigger a prompt repaint. The
* counter counts characters the caller actually intended to emit.
* console_putc_inner(): the same body minus the tx-counter increment --
* used internally for the newline console_putc() itself realizes out of
* g_pending_line_close below (see console_ensure_line_start()), so that
* one real character write emits at most one counted "start" (the '\n'
* plus the character together read as a single unit of real output, not
* two, to console_tx_count()'s callers).
*/
static void console_putc_inner(char c);
/* FABRIC-2.md §I.9 fix, 2026-09-05: set by console_ensure_line_start(),
* cleared either by console_cancel_deferred_line_start() (no output
* followed -- drop the newline silently) or here in console_putc() (real
* output followed -- realize it for real, counted, immediately before the
* character that triggered it). See console.h's doc comments on both
* functions for the full rationale; the short version is that the old
* unconditional-and-immediate version of this newline fired every idle
* heartbeat regardless of whether anything was actually about to be
* printed, visibly snapping a bare REPL prompt to a fresh blank line on
* its own. */
static int g_pending_line_close = 0;
void console_putc(char c) {
if (g_pending_line_close) {
g_pending_line_close = 0;
g_console_tx_count++;
console_putc_inner('\n');
}
g_console_tx_count++;
console_putc_inner(c);
}
@@ -313,10 +330,15 @@ void console_ensure_line_start(void)
{
if (!g_line_start)
{
console_putc_inner('\n');
g_pending_line_close = 1;
}
}
void console_cancel_deferred_line_start(void)
{
g_pending_line_close = 0;
}
/**
* Write a string with newline to serial console
*/
+38 -13
View File
@@ -174,8 +174,18 @@ static void sk_repl_idle(VM *active_vm)
* the prompt and inherit no {VMName} prefix (g_line_start is 0). A
* fresh line first keeps every idle line prefix-tagged and readable,
* matching what an interactive typing session expects. No-op when the
* console is already at a line boundary. */
* console is already at a line boundary.
*
* FABRIC-2.md §I.9 fix, 2026-09-05: this newline is now deferred (see
* console_ensure_line_start()'s own doc comment) -- it only actually
* reaches the console if something below really prints. tx_before_idle
* lets this function tell "nothing happened" apart from "something did"
* the same way the reanchor check further up this file already does,
* so a beat with nothing to report can cancel the deferred newline
* before returning, leaving the bare prompt line completely untouched
* instead of visibly snapping it to a fresh blank line every ~1s. */
console_ensure_line_start();
uint64_t tx_before_idle = console_tx_count();
/* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the
* "interrupt-driven, coarse cadence, cheap early-exit" trigger Section
@@ -357,8 +367,12 @@ static void sk_repl_idle(VM *active_vm)
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
if (g_mama_interpreting || g_idle_pump_active)
if (g_mama_interpreting || g_idle_pump_active) {
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
}
return;
}
g_idle_pump_active = 1;
{
@@ -386,6 +400,10 @@ static void sk_repl_idle(VM *active_vm)
}
}
g_idle_pump_active = 0;
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
}
}
/*===========================================================================
@@ -596,17 +614,24 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
uint64_t now = heartbeat_ticks();
/* n == 0 gate: sk_repl_idle() opens with console_ensure_line_start(),
* which unconditionally forces a newline whenever the console isn't
* already at a line boundary -- including mid-edit, with characters
* already typed and echoed (n > 0) but Enter not yet pressed. Without
* this gate, that forced break fires on every elapsed
* SK_IDLE_BEAT_INTERVAL regardless of whether sk_repl_idle() actually
* has anything to print, visually snapping the in-progress input line
* to a fresh (empty) line -- indistinguishable from Enter having been
* pressed. Deferring the whole idle beat while a line is being edited
* (same n > 0 guard the prompt reanchor below already uses) delays
* xhci/block-sync servicing by at most one more interval, which its
* own "coarse cadence, cheap early-exit" design already tolerates. */
* closing off a dangling prompt line before any chatter it might
* print (xhci attach/detach, block-sync, MSG-TICK pump). Before the
* FABRIC-2.md §I.9 fix (2026-09-05), that newline was unconditional
* and immediate, so it fired on every elapsed SK_IDLE_BEAT_INTERVAL
* regardless of whether sk_repl_idle() actually had anything to
* print -- including mid-edit (n > 0, characters typed but Enter
* not yet pressed), visually snapping the in-progress line to a
* fresh blank one, indistinguishable from Enter having been
* pressed. console_ensure_line_start()'s newline is now deferred
* and self-cancelling when nothing follows it (console.c), which
* fixes that regardless of n -- but this gate is kept for its own,
* independent reason: deferring the *whole* idle beat while a line
* is being edited (same n > 0 guard the prompt reanchor below
* already uses) means a genuine xhci/block-sync/MSG-TICK event
* cannot interrupt output mid-line while the user is actively
* typing, only delaying that servicing by at most one more
* interval, which its own "coarse cadence, cheap early-exit"
* design already tolerates. */
if (n == 0 && now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle(active_vm);