<feed xmlns='http://www.w3.org/2005/Atom'>
<title>newos4/main.html, branch v4.6.168</title>
<subtitle>newOS4 - the browser terminal that runs WebX11 user spaces</subtitle>
<id>https://newos.webx11.duckdns.org/newos4/atom?h=v4.6.168</id>
<link rel='self' href='https://newos.webx11.duckdns.org/newos4/atom?h=v4.6.168'/>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/'/>
<updated>2026-11-12T17:29:53+00:00</updated>
<entry>
<title>fix(newos4): `ui` now works in classic — it was being answered by the AI (v4.6.168)</title>
<updated>2026-11-12T17:29:53+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T17:29:53+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=0acf9b61c3d3c750e4af39900c43fc1cdd221578'/>
<id>urn:sha1:0acf9b61c3d3c750e4af39900c43fc1cdd221578</id>
<content type='text'>
Operator report: "Ui command don't work". It was worse than a broken command: typing `ui` in the
classic terminal printed

    Security: Unknown command "ui" — routing to AI (did you mean "ai"?)...
    → Routing to AI assistant...

and an AI turn answered it. Three layers each looked healthy on its own:

1. `ui` is registered by UIShellModule, which since the v4.6.162 gate only loads when the visitor has
   already opted in — so the command that switches the interface did not exist in the state you need
   it from.
2. `_classify()` decides "known" from the command registry (module commands + a short builtin list),
   not from the safety whitelist, so `ui` was `unknown: true`. At the default security level 1 an
   unknown command that is not whitelisted is offered as a typo and then routed to the AI. `ui` was
   not whitelisted.
3. Nothing printed an error anyone would recognise as failure — it looked like the terminal was
   answering you.

Fix:
  * `'ui'` joins `_baseWhitelist` (114 -&gt; 115). It is a navigation command: safe, non-destructive, and
    it must never depend on the interface it navigates to.
  * `dispatchCommand` gets an `if (cmd === 'ui')` fallback that mounts the shell on demand via a new
    `_mountUIShell()` (register + init, the same two steps the boot loop performs) and then opens the
    picker. It also writes `webx11_ui_experimental=1`, so the opt-in survives the reload.
  * The mount is deliberately NOT `_safeRegister('UIShellModule', …)`: that bare call is the boot gate
    and `verify-newos4-ui-gate.py` fails on any occurrence of it. A guard that grants one exception
    has to grant whatever is written next to it, so the on-demand path is separate by name.

Guard: new check `ui_command_in_classic` — executes `ui` on the served page with no flag and waits for
`webx11.uiShell` + `#wx11-ui-picker`. Deliberately behavioural: `_classify('ui').known` reads FALSE even
when the command works (registry vs whitelist), so a text or classifier assertion would have been a lie
in both directions. The RED case is on record: the AI-routing line above, captured in the live
scrollback before the fix.

Verified on v4.6.168 (served): `ui` -&gt; picker with 7 options; `ui easy` -&gt; easy; `ui cockpit` -&gt;
cockpit; `ui classic` -&gt; classic; `ui nonsense` -&gt; "Unknown mode 'nonsense'. Available: classic, easy,
cockpit" and stays put. Guard 8/8 rc=0. maintain OK.
</content>
</entry>
<entry>
<title>fix(newos4): the three glyph-only switch buttons now say what they do (v4.6.165)</title>
<updated>2026-11-12T17:21:09+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T17:21:09+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=a14d8ab15130779e4d612f690cd3c806800875c6'/>
<id>urn:sha1:a14d8ab15130779e4d612f690cd3c806800875c6</id>
<content type='text'>
Asked "how do I switch?", I checked all six modes and found the controls were already uniform in
function — every mode calls `api.exec('ui picker')` — but not in labelling:

  easy       ⇄ Change view
  chat       ⇄ Style
  workspace  ⇄ Style
  studio     ⇄          &lt;- glyph only
  commander  ⇄          &lt;- glyph only
  cockpit    ⇄          &lt;- glyph only

An unlabelled ⇄ on a phone is a guess. The three bare ones now carry
title="Change view — pick another interface" and aria-label="Change view", which api.el() forwards
via setAttribute. Verified on the served build: the snapshot reads `button "Change view"` in studio,
and clicking it opens the picker with all seven options (six modes + "Keep the classic terminal").

No behaviour change — same handler, same picker; the control is now discoverable and announced by
screen readers.
</content>
</entry>
<entry>
<title>fix(newos4): a mode the user picks now survives a reload (v4.6.164)</title>
<updated>2026-11-12T16:56:17+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T16:56:17+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=924779e5fd49daabc2056f17df91fcafbe6e956d'/>
<id>urn:sha1:924779e5fd49daabc2056f17df91fcafbe6e956d</id>
<content type='text'>
Found by the operator actually running /ui-selftest.html on his phone. The report said 6/6 PASS,
and the interesting part was what happened next: pick "easy" from the picker, reload, and you are
back in classic. Two defects stacked on top of each other, neither of which reported anything:

1. `apply()` on an unregistered mode starts a lazy load and returns false. When the mode registers,
   `registerMode()` re-applied it with a hardcoded `persist:false` — so the picker's choice was
   never written to `webx11_ui_mode` at all. The intent ("the user asked for this") was dropped the
   moment the mode file had to be fetched, which is the normal case for all six modes.
2. Even when the mode did persist, `webx11_ui_experimental` was never set — and that key is what the
   in-bundle gate reads before UIShellModule is registered. A stored mode behind a closed gate is a
   preference the product silently ignores.

Fix:
  * `_pendingPersist` carries the caller's intent through the lazy path; `registerMode()` honours it
    instead of hardcoding false, and the load-completion apply already forwarded `o.persist`;
  * `_persistMode(want)` writes the mode AND, for a real (non-classic) mode, sets
    `webx11_ui_experimental='1'`, so the gate opens on the next boot.

Both directions verified live on v4.6.164:
  * user pick (lazy path)  -&gt; webx11_ui_mode=easy + webx11_ui_experimental=1, then
    main.html with NO query boots easy (#easy-actions present, body wx11-ui-easy, picker suppressed);
  * shared link ?ui=chat    -&gt; mode applied for the session, _fromQuery=true, localStorage STILL null
    (a link must not become the recipient's preference — that is the shell's own rule and it survives).

Also: /ui-selftest.html no longer prints a meaningless `heapMB: 9.5`. Chromium pins
performance.memory.usedJSHeapSize at 10,000,000 without --enable-precise-memory-info, which a page
cannot request, so the report now says the heap is not measurable in-page instead of inventing a
number — the same fake-green that was fixed in probe-ui-crash.py.

guard 7/7 rc=0, maintain OK, served core-bundle byte-identical to disk.
</content>
</entry>
<entry>
<title>fix(newos4): ?ui=&lt;mode&gt; is an opt-in too — the gate had killed every shared mode link (v4.6.163)</title>
<updated>2026-11-12T16:40:27+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T16:40:27+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=facd53ed5fc932c53aa7c3a5f4100da234906f9a'/>
<id>urn:sha1:facd53ed5fc932c53aa7c3a5f4100da234906f9a</id>
<content type='text'>
v4.6.162 shipped the opt-in gate as `get('ui') === '1'`. But the UI shell has always treated
`?ui=&lt;mode&gt;` as a REQUEST — `module-uishell.js` reads `q = get('ui')` and honours it when the value
is one of `lazyIds()` (easy|chat|studio|workspace|commander|cockpit), with `_fromQuery` set so the
request never becomes a stored preference. So every shared link of the form `main.html?ui=chat`
stopped mounting the shell entirely and booted classic, silently, with no error: the gate required a
literal '1' that the shell's own convention never sends.

Found by an independent verification pass that tried `probe-ui-modes.py --cycle` against prod and got
`FAIL easy@390x844: wait_for_selector timeout` — a false red on a working build, which traced back to
this. The probe's own breakage was mine (I had just changed its default base to `?ui=1`, and it
appends `?ui=&lt;mode&gt;`, producing `?ui=1?ui=easy`); that is fixed in the next commit.

Gate now: opt in on ANY `ui` value except an explicit off ('0'/'false'), plus the localStorage key.
  q = get('ui'); if (q !== null &amp;&amp; q !== '' &amp;&amp; q !== '0' &amp;&amp; q !== 'false') return true;
  return localStorage.getItem('webx11_ui_experimental') === '1';

Verified on the served page (v4.6.163):
  * `?ui=chat`  -&gt; uiShell object, data-ui-mode=chat, body wx11-ui-chat, #chat-stream present,
                   _fromQuery=true, picker NOT shown, and localStorage UNCHANGED
                   (webx11_ui_experimental=null, webx11_ui_mode=null) — a link is not a preference.
  * no flag     -&gt; uiShell undefined, 0 #ui-root, no picker (unchanged from v4.6.162).
  * `?ui=1`     -&gt; shell + picker (guard's browser assertion still passes).
Served core-bundle.js byte-identical to disk; guard green (7/7 checks, rc=0); probe-ui-modes.py
--cycle ALL PASS 13 checks; maintain.py --fix PASS.
</content>
</entry>
<entry>
<title>feat(newos4): re-enable the six UI modes behind an opt-in gate (v4.6.162)</title>
<updated>2026-11-12T16:11:57+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T16:11:57+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=503afccbe728448f124db3663515728fa7a7505c'/>
<id>urn:sha1:503afccbe728448f124db3663515728fa7a7505c</id>
<content type='text'>
The six interface modes (v4.6.152–156) were removed from the startup path outright in
v4.6.157 after crash reports. The crash was never reproduced: F2 in the rollback plan
(2026-11-09_224409) drove all six modes on one page in headless Chromium at 390×844 and
recorded crashes: [], pageerrors: [], heap flat at 10 MB over a 25 s idle. The two
defects the investigation DID find were fixed instead — F3 (nginx 413 on a &gt;1 MB agent
context) and F4 (36 tool handlers where a model-supplied non-string reached
.trim/.split/.replace). v4.6.159–161 also removed the last navigator.onLine verdicts,
which is what made "it died mid-session" look like a UI crash.

So the modes come back, but only for a visitor who asks for them.

1. The gate (modules/core/terminal.js, plan Task 9)
   - Registration is now conditional:
         const _uiExp = ... URLSearchParams(location.search).get('ui') === '1'
                        || localStorage.getItem('webx11_ui_experimental') === '1';
         if (_uiExp) this._safeRegister('UIShellModule', 'UI shell');
   - Without the flag nothing changes: the shell is not registered, no mode can mount (the
     ui-*.js files all begin `if (!terminal.uiShell) return;`), no first-run picker
     appears, `data-ui-mode` is absent and `webx11.uiShell` is undefined.
   - The gate is in the bundle, so it cannot be bypassed by a stale cached ext file.

2. What was verified on the served page (v4.6.162, live Chromium)
   - Classic, no flag: NEWOS4_VERSION v4.6.162, 48 modules, uiShell undefined, 0 #ui-root,
     no #wx11-ui-picker, no data-ui-mode, terminal visible, `help` renders 18,124 chars,
     0 console warnings, 0 JS errors, 55 console messages.
   - Opt-in `?ui=1`: uiShell object, #ui-root + #ui-foot, data-ui-mode="classic",
     49 modules, picker present (opt-in only), 0 warnings, 0 JS errors.
   - All six modes driven through uiShell.apply() with a 1.2 s settle each, and back to
     classic: chat (body wx11-ui-chat, 6 mode nodes), cockpit (3), easy (4), commander (4),
     studio (3), workspace (5), classic (0). #terminal-container survived every switch,
     zero throws, heap 6.3 MB against 3.3 MB classic.
   - Served bytes identical to disk for core-bundle.js; `_uiExp` appears twice in the
     served bundle and the registration line is the guarded one (core-bundle.js:4111).

3. Not done, deliberately (plan Task 9 step 4)
   - `webx11_ui_experimental` is NOT defaulted on for new visitors. The plan asks for one
     week of clean use first, with the crash matrix re-run on the then-current bundle.

Guard for this gate: admin/scripts/verify-newos4-ui-gate.py, wired above `exit $FAIL` in
smoke-auth.sh — a bare (unconditional) registration is the failure it exists to catch.
</content>
</entry>
<entry>
<title>fix(newos4): stop reporting network and VFS size as facts we do not have (v4.6.161)</title>
<updated>2026-11-12T16:02:47+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T16:02:47+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=e452ce7785e5e47ec01a409d6da1f59564f8f365'/>
<id>urn:sha1:e452ce7785e5e47ec01a409d6da1f59564f8f365</id>
<content type='text'>
1. The last two `navigator.onLine`-as-fact payloads
   - module-aiassist.js: the debug/health payload carried
     `online: navigator.onLine`, consumed by downloads/dashboard.html, which rendered
     "🔴 Offline" — on an Android WebView (reports false while fully connected) that
     told working users they were offline. The key is gone; the payload carries
     `network:'unknown'`, `networkKnown:false` and `networkHint`.
   - module-world-model.js: the snapshot had the same field, and it is also handed to the
     LLM through `_modelDigest()`/`ask()`, so the model was told the user was offline.
     Now `network:'unknown'` + `networkKnown:false`.
   - `_loadFromVFS()` scrubs `system.online` from a model persisted by an older build —
     otherwise the stale boolean would survive inside the digest for the life of the VFS.
   - downloads/dashboard.html: `networkText()` renders reachable/unreachable/http-* only
     when doctor produced them ("(doctor)"), `unprobed` as "— not probed", and everything
     else as "— unknown (run doctor for a live probe)". A legacy payload's `online:false`
     lands in "unknown", never "Offline". The row label is "Network", not "Online".
   - Neither payload invents a probe: SystemModule stores no last-doctor verdict, so
     'unknown' is the honest value until one exists.

2. `vfsSize` in both payloads was a different unit again
   - aiassist: `JSON.stringify(fs.root).length / 1024`; world-model:
     `JSON.stringify(fs.root).length`. Same lie v4.6.160 fixed in `vfs-size` — UTF-16 code
     units plus serialisation structure. Both now sum UTF-8 bytes of file content
     (`_utf8Len`/`_measureVfs`, Blob → TextEncoder → code-point fallback, depth/node
     capped) and carry `sizeBytes` beside the human string so the rows reconcile.
   - world-model's `_gatherFileInfo().size` used a THIRD unit on an adjacent line
     (`JSON.stringify(fs.root).length/1024`); now the same measurement, plus `sizeBytes`.
   - `showModel()` renders exact bytes and tolerates a pre-fix persisted string.

3. Predictor made a guaranteed-401 request on every boot (UI-plan finding F6)
   - `_wdConfig()` returns `user:''`/`pass:''` when the saved WebDAV config has a URL but
     no credentials, and `_pullFromServer`/`_pushToServer` fetched the auth-gated
     `/spaces/&lt;user&gt;/_data/predictor.json` anyway — 107 × 401 in one day, logged by the
     browser as console errors. Both now refuse without credentials
     (`cache-only (no credentials)`).

Verified: 42/42 node tests, 2 new files (38 payload/honesty assertions, 8 predictor-store
assertions); the predictor suite is 4/8 against the pre-fix source, and the payload suite
fails 14 assertions against HEAD (`online:false` present, vfsSize === JSON.stringify
length). Live Chromium on v4.6.161, on the served dashboard: a legacy payload with
`online:false` renders "Network — unknown (run doctor for a live probe)", the current
payload renders "85.8 KB (87880 B)" and the same unknown, and a doctor-produced
`unreachable` renders "⚠️ unreachable (doctor)". Served bytes identical for
core-bundle.js, main.html and downloads/dashboard.html. `maintain.py --fix` all PASS.
</content>
</entry>
<entry>
<title>fix(newos4): honest size/network measurements + checkpoint and phantom-delete fixes (v4.6.160)</title>
<updated>2026-11-12T15:39:12+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T15:39:12+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=228ad621e2631487e74acdf9bf58e797f60ab745'/>
<id>urn:sha1:228ad621e2631487e74acdf9bf58e797f60ab745</id>
<content type='text'>
Applies the three 2026-11-09 patches after review, and fixes two more
navigator.onLine lies found while verifying them. Each patch was checked
against the live code first; one of its claims did not survive.

1. virtual-fs — phantom deletes and checkpoint bloat
   - deleteCheckpoint('nope') returned true and printed "Deleted checkpoint"
     for a name that did not exist: _resolvePath() validates intermediate
     segments only, so the leaf was never checked. New _requireLeaf() refuses
     a resolved path whose leaf is absent, and deleteCheckpoint uses it.
   - CHECKPOINT SIZE: checkpoint() cloned the whole root minus
     _snapshots/_sessions/.trash, so _mods (module originals) and
     .semantic_index were duplicated into EVERY checkpoint — measured
     2,101,686 bytes for a 2 MiB _mods. Both are now excluded on write and
     preserved across rollback (without the keep-list change a rollback would
     delete the live module originals). Measured after: 185 bytes.
   - Claim corrected: the patch also claimed deleteFile() reported success on
     a phantom path. It did not — live code already required
     `node &amp;&amp; node.type === 'file'` and returned false. The guard is kept as
     defence-in-depth, and the test asserts the real behaviour rather than the
     claimed one.

2. module-fs — vfs-size reported ~2x the real size
   - It measured JSON.stringify(root).length: UTF-16 code units PLUS every
     structural character of the serialisation. A 1046-byte tree printed as
     1961; anything under ~5 KB printed as "0.00 MB".
   - Every number is now stored bytes (UTF-8 of file content): one measure()
     walk for the total, the per-file table, the top-directory table and the
     system dirs, so the rows add up to the total. The patch's arithmetic was
     still off in seven places, all fixed here: a UTF-16 fallback that
     reintroduced the bug when Blob was missing, a top-5 table that could
     never sum to the total (now top 10 + an aggregated remainder row), the
     top-dirs table dropping files stored at root (now a "/(root files)" row),
     a 2-decimal MB total that printed 0.00 for small VFS, three duplicate
     hand-rolled walks, readDir() used for dir sizes, and "1 files".

3. module-system — status/doctor no longer trust navigator.onLine
   - status printed "Network: Offline" from navigator.onLine, which an
     Android WebView reports false while fully connected.
   - doctor now awaits a real HEAD probe (AbortController, 4 s): only
     'reachable' is healthy, a throw is HIGH "Network: unreachable", a non-ok
     answer is MEDIUM and names the status (405 to HEAD is not "offline").
   - THE PATCH'S THRESHOLDS COULD NEVER FIRE: it compared localStorage bytes
     against navigator.storage.estimate().quota, which is the whole-origin
     quota (GBs, mostly Cache Storage), while localStorage has its own ~5 MB
     cap — a 4.5 MB localStorage reported "All systems healthy" against a
     2 GB quota. Now: localStorage measured as (key+value)*2 UTF-16 bytes with
     HIGH &gt; 4 MiB / MEDIUM &gt; 3 MiB, VFS measured in UTF-8 bytes with
     HIGH &gt; 3 MiB and &gt; 80% of the measured localStorage bytes. The origin
     quota appears only in one informational line and decides no severity.
   - Also removed navigator.onLine from version, bug-report and bug (a grep
     for it is now 0 in the module), and added a "Measured:" line so a clean
     report still shows what was measured.

4. terminal.js / module-servermon.js — the same lie, where the user looks
   - The status bar printed "❌ Offline" in red from navigator.onLine; on a
     WebView that is false while online. It now shows "📶 Network?" with a
     title pointing at `doctor` unless the browser claims online.
   - `net` test line now reads "Browser reports: offline (unreliable)" as a
     warning instead of a red failure — the real verdict comes from the
     probes that follow.

Verified: 40/40 node tests (3 new files: 36 + 53 + 34 assertions, each proven
red against the pre-patch code; the doctor test runs against the PATCH's
threshold logic and shows the 4.5 MB case staying silent). An independent
orchestrator probe re-derived all of it: phantom delete false, 2 MB _mods
absent from the checkpoint and preserved through rollback, vfs-size exact
bytes for ASCII/é/☃ nested trees, 4.5 MB localStorage HIGH despite a 2 GB
quota, failing probe unreachable, status never claiming Online/Offline.
Served bytes identical for core-bundle.js / main.html / module-system.js /
virtual-fs.js. Live Chromium on v4.6.160: vfs-size "85.8 KB (87880 bytes)
across 40 files", doctor "Measured: VFS 0.08 MB UTF-8 · localStorage 0.07 MB
UTF-16 in 14 keys", a stubbed failing probe reports HIGH unreachable, the
status bar reads "📶 Network?" when the browser lies, deleteCheckpoint on a
missing name returns false, and the console has 0 warnings and 0 JS errors.
</content>
</entry>
<entry>
<title>fix(newos4): one arg-coercion boundary + binary-safe gunzip + clean boot log (v4.6.159)</title>
<updated>2026-11-12T15:24:15+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-12T15:24:15+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=c08a795ae822e905639caf563e36cf7953d530f1'/>
<id>urn:sha1:c08a795ae822e905639caf563e36cf7953d530f1</id>
<content type='text'>
1. Tool arguments are coerced once, at the dispatch boundary (module-aiassist.js)
   - _coerceToolArgs(name, a) at :1584 reads the tool's own JSON schema and, for every
     declared string prop, converts number/boolean with String() and object/array with
     JSON.stringify (never "[object Object]"); declared integer/number props accept
     unambiguous numeric strings. null/undefined stay absent so the handlers' defaults
     and _pathArg still own "no value given".
   - Called at both dispatch boundaries: runTool (:2650) and runModuleTool (:3565 —
     reachable from view_module and from ship_module -&gt; test_module/health_check, which
     have their own parse+switch).
   - A JSON body of "null" or "[1,2]" parses to a non-object; both sites normalise to {}
     before any handler reads a.field.
   - The 26 handlers keep their `a.x || ''` reads: `|| ''` catches only FALSY, which is
     how a truthy number or object reached .trim()/.split()/.replace() and filed one
     auto-report per tool (user_get "key.split is not a function", create_skill,
     patch_skill, delegate_task "task.substring is not a function", create_mindmap,
     web_fetch, list_skills, ship_module).

2. gunzip is binary-safe (cryptogzip.js — plan 2026-11-03 T3, the last open task)
   - _cmdGunzip used _vfsRead + _gunzipString (base64-only), so a real .gz fetched with
     wget reported "not valid base64 input (illegal character \u001f)" and could not be
     decompressed at all. It now reads bytes (_vfsReadBytes handles both the module's
     base64 output and raw Latin-1 bytes), checks the 1f 8b magic, and reports
     "not a gzip file" for non-gzip input without writing or deleting anything.
     The module's own gzip output still round-trips.

3. Boot log is clean (terminal.js)
   - Seven _safeRegister() calls for Editor/Shell/Alias/Finder/FileManager/Diff/BusyBox
     were guaranteed no-ops: those classes are declared in critical-ext-bundle.js, a
     &lt;script&gt; after module-loader.js, so they do not exist yet when new WebX11Terminal()
     runs; adding them to the registry map would still resolve to undefined. Removed with
     a comment — the ext bundle registers each one on load.
   - SimpleGUIModule IS a core class but was missing from _safeRegister's registry map,
     so the fallback could never restore it. Added.

4. New guard: admin/scripts/verify-plan-age.py + profile wrapper plan-age-guard.sh
   (cron newos4-plan-age-guard, no_agent, daily 07:10). Silent when healthy; reports
   plans marked &lt;!-- autonomy: review --&gt; with no execution stamp older than 72 h —
   the process gap that let four fix plans sit for six days before v4.6.158. Read-only
   by construction (148 real plans byte+mtime identical before/after a run), rc=2 when
   the directory cannot be scanned, --strict opt-in so a report is never recorded as a
   crashed guard.

Verified: 37/37 node tests, 3 new test files and 631 new assertions (613 arg-coercion,
18 gunzip); test-cryptogzip-guard's C8 updated because it pinned the old misleading
"not valid base64" message. Independent orchestrator checks: all 66 declared string
props across 58 tool defs coerced under 4 hostile shapes; positive cases untouched
(string content not double-encoded, numbers still numbers, null still absent, unknown
tool identity preserved). Served bytes byte-identical for core-bundle.js, main.html and
cryptogzip.js. Live Chromium on v4.6.159: write_file with object content wrote 27 chars
of readable JSON (not "[object Object]"), read_file with offset:"0"/limit:"200" coerced
to numbers, user_set with key 12345 -&gt; "12345" and object value -&gt; JSON, and the boot
log now carries 0 SafeRegister warnings (52 messages, 0 JS errors) with SimpleGUIModule
registered.
</content>
</entry>
<entry>
<title>fix(newos4): six crash classes from the feedback queue (v4.6.158)</title>
<updated>2026-11-09T22:58:35+00:00</updated>
<author>
<name>Hermes SysOp</name>
<email>sysop@webx11.duckdns.org</email>
</author>
<published>2026-11-09T22:58:35+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8'/>
<id>urn:sha1:9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8</id>
<content type='text'>
Each item is symptom -&gt; cause -&gt; fix, re-verified against the served bytes.

1. debug-ai panel threw "Cannot read properties of undefined (reading 'substring')"
   (RPT-20261108-231622). _updateDebugPanel and two sibling previews called
   JSON.stringify(entry.args).substring(); JSON.stringify(undefined) returns
   undefined, and _toolCallLog entries carry args: undefined for any tool call
   with no arguments. All three previews now go through _formatToolArgs(), which
   renders "null" and never throws.

2. test_module threw "Cannot convert undefined or null to object"
   (RPT-20261108-232127). Object.keys(instance.commands) assumed every module
   initialises a command map. The throw landed after loadModuleFromCode had
   already succeeded, so the rolledBack:true branches were skipped and an
   unverified module stayed loaded. Both sites now require instance.commands.

3. list_files threw "originalPath.replace is not a function" (RPT-20261109-172742,
   3 occurrences in errors.json). A model can send a non-string path despite the
   schema. The path family (list_files/read_file/write_file/make_directory/
   delete_file) now coerces through _pathArg() and answers the tool's own honest
   error instead of a TypeError.

4. wpm search failed for EVERY keyword (RPT-20261109-123907): packages.json entry
   `kanban` used `description` while the other 31 use `desc`, and search() called
   pkg.desc.toLowerCase() mid-loop. Catalog reads now go through _pkg() /
   _packageMap() (desc || description, values coerced to strings), and the kanban
   entry carries name/version/desc/commands. Re-running the original loop for
   pdf/git/zzz is clean.

5. wx/webxai/webx turned a chat-lock timeout into an unhandled rejection
   (RPT-20261107-150726): forwardToAI() called chat(args) in a synchronous
   function without returning or catching the promise, so the 10 s busy throw
   never reached dispatchCommand's catch. It now returns the promise with a
   .catch that prints "AI error: ..." through terminal.print — console.error is
   hooked by the debug supervisor and would re-file the same report.

6. CryptoGzip._base64ToBytes threw a raw DOMException on non-base64 input
   (RPT-20261102-203616). It now validates first and throws
   Error("not valid base64 input (&lt;reason&gt;)"). The duplicate shadowing
   definitions at the bottom of the class were deleted — the later copy is the
   one that carried the unguarded atob().

Also regenerated modules/core/core-bundle.js (generated, never hand-edited) and
bumped the build v4.6.157 -&gt; v4.6.158 (main.html x3 + module-loader.js).

Verified: 35/35 node tests, including 6 new regression tests each proven red
against the pre-fix source; an independent orchestrator harness 36/36; and
newos4-maintain.py --fix all PASS after regenerating the stale changelog page.
Served bytes are byte-identical for the bundle, main.html and both ext modules,
and a live headless-Chromium run against v4.6.158 shows WpmModule.search('pdf')
returning 1 result, the debug panel rendering a no-arg entry as "null",
_base64ToBytes('grüße') raising a plain Error that names base64, and 0 JS errors
on load.

m.pull (RPT-20261102-211438) is closed as a usage error: no module in the tree
defines pull(); the real interfaces are the webdav-pull and model-pull commands.
</content>
</entry>
<entry>
<title>fix(newos4): take the UI shell off the startup path until the crash reports are explained</title>
<updated>2026-11-09T21:45:10+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-09T21:45:10+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=bb210547ea45aabd7b68563921844ba162db67cb'/>
<id>urn:sha1:bb210547ea45aabd7b68563921844ba162db67cb</id>
<content type='text'>
Symptom: operator reports newOS4 "started to crash since the last ui update"; a
user (Max) reports his browser crashed. Order: roll back, do not re-enable until
it is verified in a dev environment.

Root cause: not yet established, and the log work rules out the obvious suspect.
- 0 requests for modules/ext/ui-*.js in the entire access log: no interface mode
  has ever mounted in production, so a mode's own chrome cannot be the cause.
- all six modes driven on one page in headless Chromium at 390x844: no crash, no
  page errors, one #terminal-container, heap flat at 10 MB over a 25 s idle.
- the six errors.json entries that read like a crash report (SPA is not defined,
  DOM harness FAILED: N, "...reading 'makeInstance'") exist in no shipped file -
  grep on the served 1.29 MB bundle returns 0 - so they came from a harness run
  inside that one browser session.

Two real defects were found instead, both planned not bundled here:
- /ollama/api/chat inherits nginx's 1 MB default client_max_body_size and
  refused a 1,063,595-byte agent context with 413 (only 413 in 7 days).
- module-aiassist.js:2576 const originalPath = a.path || '' then .replace on a
  model-supplied non-string: TypeError, 3 events on tool:list_files, 32 sites
  with the same shape.

Fix: the UIShellModule registration in terminal.js is commented out with a dated
note and the revert recipe; bundles rebuilt from source; LATEST_VER v4.6.156 -&gt;
v4.6.157; main.html's three ?v= refs bumped. The six modes are lazy ext files
whose first line is `if (!terminal.uiShell) return;`, so an accidental load is a
no-op and no first-run picker appears.

Verified: served core-bundle.js?v=v4.6.157 is cmp-identical to disk (1,298,695 B)
and the registration exists only as a comment (core-bundle.js:4082). Playwright
390x844: shellPresent false, uiShellModuleRegistered false, uiRoot false,
pickerNode false, moduleCount 47, help renders 21,802 chars, pageerrors [].

Workaround: none needed - users get classic only. Re-enable = restore the line,
rebuild, bump LATEST_VER + main.html, re-run admin/scripts/probe-ui-modes.py.
Plan: .hermes/plans/2026-11-09_224409-newos4-ui-rollback-dev-verify.md
</content>
</entry>
</feed>
