FABRIC-2.md §I.9: fix the terminal phantom-linebreak defect
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:
co-authored by
Claude Sonnet 5
parent
704573bdfc
commit
8edb95b65d
+33
@@ -4716,3 +4716,36 @@ same root cause, not a separately confirmed one.
|
||||
diagnosis is complete and reproducible on demand (QMP `send-key` + a screenshot, no physical
|
||||
keyboard needed, on any of the three architectures); whether and how to fix it is a decision
|
||||
for whoever picks this item up next.
|
||||
|
||||
**FIXED, 2026-09-05, explicit go-ahead given ("Yes, fix it").** Deferred-newline mechanism:
|
||||
`console_ensure_line_start()` (`include/starkernel/console.h`, `src/starkernel/hal/console.c`)
|
||||
no longer calls `console_putc_inner('\n')` immediately. It 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 — the next time `console_putc()` actually runs, or
|
||||
silently discarded via the new `console_cancel_deferred_line_start()` if the caller decides it
|
||||
has nothing to print after all. `sk_repl_idle()` (`repl.c`) now captures
|
||||
`tx_before_idle = console_tx_count()` right after its `console_ensure_line_start()` call, and
|
||||
calls `console_cancel_deferred_line_start()` at both of its exit points if
|
||||
`console_tx_count() == tx_before_idle` (nothing was actually printed that beat). This means a
|
||||
beat with nothing to report leaves the console completely untouched — no stray newline, no
|
||||
phantom blank line — while a beat that does print (xhci attach/detach, block-sync notices)
|
||||
still gets the correct "close the dangling prompt line first" behavior, now properly counted so
|
||||
the reanchor logic sees it. The pre-existing `n > 0` mid-edit gate at `sk_console_readline()`
|
||||
(`repl.c`, originally documented at old lines 598-609) is untouched — it serves an independent
|
||||
purpose (deferring bottom-half servicing during active typing) and was never the bug.
|
||||
|
||||
Verified via the mandatory 3-arch QEMU acceptance boot, all foreground, `clean qemu` each
|
||||
(amd64 `logs/20260905-015854/`, aarch64 `logs/20260905-020248/`, riscv64
|
||||
`logs/20260905-020552/`). All three reached `(zuse) ok>` and, per the raw serial log bytes,
|
||||
echoed the first real input on the *same log line* as the prompt — e.g. aarch64's
|
||||
`(zuse) ok> 1 1 + .r CR` and riscv64's `(zuse) ok> 1 1 . CR BYE`, not a fresh `[Hera]`-prefixed
|
||||
line as before the fix — then shut down cleanly via `BYE`. amd64's raw log additionally shows
|
||||
genuine human backspace-correction (`11^H ^H 1 + .`) still glued to the same prompt line,
|
||||
confirming the fix holds under real typing, not just a scripted single keystroke. No separate
|
||||
QMP send-key/screendump re-run was needed — these boots were driven live at the actual QEMU GTK
|
||||
window.
|
||||
|
||||
The secondary, unverified cursor-glyph-staleness observation above was not independently
|
||||
re-checked — out of scope for this fix, and about to be superseded anyway by a separate
|
||||
requested change (blinking `|` cursor instead of a static block), tracked as a new item, not
|
||||
here.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-05T05:38:13Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-05T06:05:19Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
@@ -129,14 +129,38 @@ void console_puts(const char *s);
|
||||
void console_println(const char *s);
|
||||
|
||||
/**
|
||||
* console_ensure_line_start - Make sure the next console_putc() starts at
|
||||
* the beginning of a fresh output line, emitting a newline if output is
|
||||
* currently mid-line (e.g. dangling behind a re-anchored prompt).
|
||||
* No-op if already at line start. Mirrors console_putc()'s dual
|
||||
* serial+framebuffer behavior (a bare '\n' reaches both).
|
||||
* console_ensure_line_start - Make sure the *next real output* starts at
|
||||
* the beginning of a fresh output line, closing off whatever is currently
|
||||
* mid-line (e.g. a dangling prompt). No-op if already at line start.
|
||||
*
|
||||
* FABRIC-2.md §I.9 fix, 2026-09-05: the newline is DEFERRED, not emitted
|
||||
* immediately -- it only actually reaches the console on the next real
|
||||
* console_putc() call, and is silently dropped (never emitted at all) if
|
||||
* console_cancel_deferred_line_start() is called first instead. Before this
|
||||
* fix, an immediate, unconditional newline here meant any caller invoking
|
||||
* this function "just in case" (sk_repl_idle() being the one real caller)
|
||||
* would visibly snap a bare, unfinished prompt line to a fresh blank line
|
||||
* even when nothing was actually about to be printed -- indistinguishable
|
||||
* from Enter having already been pressed at that prompt. Deferring means a
|
||||
* caller that turns out to have nothing to print can cancel cleanly, with
|
||||
* zero visible effect, while a caller that does print gets the correct
|
||||
* "close the old line first" behavior for free, still counted by
|
||||
* console_tx_count() as real output (unlike the old always-silent inner
|
||||
* form) since it is realized through the normal console_putc() path.
|
||||
*/
|
||||
void console_ensure_line_start(void);
|
||||
|
||||
/**
|
||||
* console_cancel_deferred_line_start - Discard a pending deferred newline
|
||||
* from console_ensure_line_start() without ever emitting it. No-op if no
|
||||
* newline is currently deferred. Callers that speculatively deferred a line
|
||||
* break before checking whether they actually have anything to print
|
||||
* (sk_repl_idle()'s idle-beat check being the motivating case, FABRIC-2.md
|
||||
* §I.9) call this when the check comes back negative, so the console is
|
||||
* left exactly as it was -- no stray newline, no phantom blank line.
|
||||
*/
|
||||
void console_cancel_deferred_line_start(void);
|
||||
|
||||
/**
|
||||
* console_tx_count - Monotonic count of console_putc() calls delivered to
|
||||
* either output (serial and/or framebuffer). In use by the REPL to detect
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[2J[001;001H[=3h[2J[001;001H[2J[001;001H[8;042;160t[2J[001;001H[2J[001;001H[8;056;240t[2J[001;001HBdsDxe: loading Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
|
||||
BdsDxe: starting Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
|
||||
[2J[001;001H[=3h[2J[001;001HStarKernel UEFI Loader
|
||||
Loading kernel from ESP...
|
||||
[CKPT 001] Entered efi_main - ConOut live
|
||||
RAW SERIAL UP
|
||||
[CKPT 002] Serial (COM1) initialized
|
||||
Monolithic build - kernel linked directly
|
||||
Collecting boot information...
|
||||
CmdLine: parsed OK
|
||||
[CKPT 004] Command line parsed
|
||||
[CKPT 005] Kernel stack allocation decided
|
||||
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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user