<feed xmlns='http://www.w3.org/2005/Atom'>
<title>newos4/modules/test, 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-12T16:02:47+00:00</updated>
<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): second review round — identity for every stock node, load dedup, sweep race, a11y</title>
<updated>2026-11-07T18:03:38+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T18:03:38+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=05d72563637e88825ff852d803621c567de47e34'/>
<id>urn:sha1:05d72563637e88825ff852d803621c567de47e34</id>
<content type='text'>
Four models (kimi-k2.7-code, deepseek-v4-pro, minimax-m3, glm-5.3) reviewed the shipped change
set. Review log with every accepted and rejected finding:
.hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md

- identity guard now covers every snapshot node, not just #terminal-container: terminal.js also
  binds #output, #command-input, #input-area and #status-bar, so a cloned one leaves the terminal
  writing into a detached node while the page looks perfect
- _restoreStock anchors on the next stock node already home, then on the recorded trailing
  sibling; a raw child index was measured wrong (terminal,input,status-bar,quick-actions)
- _loading map: a double-clicked card or a repeated 'ui easy' loads the mode once
- _sweepPending returns while that id is in flight instead of declaring failure at 6 s and
  overwriting the stored preference
- _syncChrome() on every switch (quick-actions was left in the tab order behind the mode root)
- picker: role=dialog/aria-modal/labelledby/focus, shown after 600 ms instead of waiting for a
  registration that never happens with lazy modes, and suppressed when ?ui= already applied
- ?ui= must be one of the six; _loadMode only accepts packages['ui-&lt;id&gt;']; hasOwnProperty on modes
- VFS names are shell-quoted before reaching api.exec (cat/edit)
- refresh generation no longer bumps per tick (slow captures left later panes at '...')
- easy: padding shorthand reset the 34 px footer reserve (footer overlapped the composer)
- cockpit panes get tabIndex and focus; F1/F12 left to the browser; nothing swallowed in inputs
- api.el lost its unused innerHTML sink; the test lost its _parseArgs stub (it hid the regression)
- v4.6.155
</content>
</entry>
<entry>
<title>fix(newos4): four-model review — lazy-load race, restore order, a11y, key handling</title>
<updated>2026-11-07T17:56:04+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:56:04+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=fc24fdbe5d5bb504a967989122a66df9f9a7c6d7'/>
<id>urn:sha1:fc24fdbe5d5bb504a967989122a66df9f9a7c6d7</id>
<content type='text'>
Reviewed the shipped change set with kimi-k2.7-code, deepseek-v4-pro, glm-5.3 and minimax-m3.
Findings below are the ones that survived verification against file:line; the rest are in
.hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md

Accepted and fixed:
- apply() had no generation token: a lazy mode load that finished after the user switched to
  another mode force-applied the abandoned one (`apply(want,{force:true})` in the .then).
  Every apply() now bumps `_loadSeq` and the callback acts only while its token is newest.
  Same token also removes a double mount: registerMode() already applies the mode as it
  registers, so the callback was mounting every lazy mode twice (teardown+cleanup twice).
- _restoreStock() used the recorded `nextSibling` as insert anchor. Every real mode moves
  SEVERAL stock nodes (terminal, input, status bar), so the recorded sibling is inside the
  mode container and every insert fell through to append. Now anchors on the next stock node
  that is already back home. A raw child index also fails (elements removed shift the
  indices) — measured order terminal,input,status-bar,quick-actions.
- _abortToClassic() left the broken mode persisted and `_pending` set, so the next reload
  retried the same failure with no way out. It now clears both and stores 'classic'.
- ?ui= reached classList.add('wx11-ui-' + id) unvalidated: a space or colon throws a
  DOMException and boot dies. Validated against /^[a-z][a-z0-9-]{0,15}$/ and no longer
  persisted (the comment always claimed it was a request, not a preference).
- Modes hide .prompt-label, so #command-input had no accessible name at all. The shell sets
  aria-label once when a mode reparents it.
- cockpit: preventDefault on all F1-F8 killed help, the address bar and devtools. F1/F12 are
  left to the browser and nothing is swallowed when focus is in a text field.
- commander: Ctrl/Cmd+K opened the palette even while typing in the composer. Ignored from
  inside editable elements now.
- picker: role=dialog + aria-modal + aria-labelledby + focus on the first card.
- api.capture(): strip the quoting characters from argv (capturePipeOutput takes argv).
- stale `wx11-ui-*` body class removed on switch instead of accumulating.

Rejected with evidence:
- "loadModuleFromURL may not return a boolean" — modules/core/module-manager.js:56 returns
  true on success and :60 false in the catch. Verified by reading the function.
- "_abortToClassic does not clear _pending" — it did not; that one is accepted above. The
  sibling claim that it "leaves the broken mode persisted" was verified by reading the
  function: it never touched localStorage, so also accepted.
- "mm.LATEST_VER / webx11_cache_ver are never set, so the mode URL falls back to ?v=0" —
  modules/core/module-loader.js:12 sets window.webx11.moduleManager.LATEST_VER and :14-15
  writes webx11_cache_ver. Verified live: the mode fetch carries ?v=v4.6.152.
- "moduleManager.modules may be a Map" — it is a plain object (terminal.moduleManager.modules
  .CryptoGzipModule is the documented access path), so ui-commander's Object.keys works.

Verified: node modules/test/test-ui-shell.js -&gt; 25/25 (the test now moves TWO stock nodes; a
raw-index restore fails it with "terminal-container,input-area,status-bar,quick-actions");
probe-ui-modes.py --cycle against production -&gt; ALL PASS, 13 checks.
</content>
</entry>
<entry>
<title>feat(newos4): six interface modes (easy -&gt; cockpit) over one terminal</title>
<updated>2026-11-07T17:19:47+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:19:47+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=aac2b1c0689aefeb140e08c24278ec486611fe10'/>
<id>urn:sha1:aac2b1c0689aefeb140e08c24278ec486611fe10</id>
<content type='text'>
Symptom: newOS4 had exactly one layout. A first-time user had no way in that did
not require learning commands, and an operator had no surface showing live runtime
state; the only way to see heap/VFS/tool-call telemetry was to type cat /sys/...

Root cause: no UI layer existed. main.html hard-codes one flex column, and
terminal.js binds the stock nodes by id at construction, so an alternative layout
had to either duplicate the output stream or reparent the real nodes.

Fix:
- modules/core/module-uishell.js (new): mode registry, #ui-root, stock-node
  snapshot/restore, identity check that reverts to classic when a mode removes or
  clones #terminal-container, shell-docked #ui-foot (the AI stop button exists in
  every mode), ui command, gated first-run picker, ?ui= honoured only if the mode
  actually registers, lazy mode loading via the same loader ext modules use.
- modules/ext/ui-{easy,chat,studio,workspace,commander,cockpit}.js (new): six
  reparenting skins with scoped CSS. Workspace/commander/cockpit read real /sys/*
  mounts and captured command output; no value on screen is invented.
- modules/test/test-ui-shell.js (new): 25 assertions - teardown, clone rejection,
  failed-mount fallback, footer docking, destructive-mode rejection.
- terminal.js: register the shell; make one failing ext module stop aborting the
  rest of the serial load loop (it did, silently, which is why six ui-* files at
  the end of extKeys never loaded).
- module-loader.js: LATEST_VER v4.6.152. The six ui-* keys are deliberately NOT
  in extKeys - see the rate-limit note below.
- admin/scripts/probe-ui-modes.py (new): 6 modes x 2 viewports plus a --cycle pass
  that switches all seven states on ONE page and asserts the output node keeps its
  identity.

Also fixed, found while testing (pre-existing, production-affecting):
- nginx limit_req zone=webx11 burst 50 -&gt; 200. One newOS4 boot fans out ~40 assets;
  the bucket emptied mid-boot and every later module got 503. Measured in
  webx11.error.log: "limiting requests, excess: 50.696 by zone webx11" against
  modules/ext/ui-chat.js. A reload within ~50 s silently broke modules. Sustained
  rate unchanged at 60r/m.

Verified: node modules/test/test-ui-shell.js -&gt; 25/25; removing both _restoreStock()
calls measures 20/25 with five FAILs; probe-ui-modes.py --cycle against production -&gt;
ALL PASS, 13 checks; served bytes: v4.6.152 x3 in main.html, class UIShellModule x1
in core-bundle.js, six ui-* packages in packages.json.
</content>
</entry>
<entry>
<title>release(v4.6.150): commit the module work that has been live but uncommitted</title>
<updated>2026-11-06T10:45:21+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-06T10:45:21+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=03143a6e88832d06c7b08cdbf8de96ba79bda7d6'/>
<id>urn:sha1:03143a6e88832d06c7b08cdbf8de96ba79bda7d6</id>
<content type='text'>
Operator commit-approve, 2026-11-06. These changes have been served since
2026-11-02/03 while git HEAD still stamped v4.6.146, so the mirror published a
tree four builds behind the running instance and the release tag named the
wrong build.

module-orchestrator.js — orchestrate-parallel reported success it could not
prove. Results are now recorded by sub-task id through a resultSink handed to
delegateTask (harvest matched by currentTask/lastTask strings, which a later
delegation on the same agent overwrote, losing finished work as "agent not
found"); the tally counts summaries with status ok instead of
Promise.allSettled resolutions (a spawn refusal fulfils with undefined and used
to print green); and a busy agent is no longer reused at pool cap, where two
siblings erased each other's lease. Guard:
modules/test/test-orchestrator-parallel-harvest.js (16 cases, reproduces the
one-agent three-sub-task cap case that printed 3/3 succeeded).

module-billing.js — the balance said "not signed in — newOS4 sends no session
token", which was already false: the browser sends the HttpOnly cookie and
parse_bearer_token() falls back to it. `cost` is on by default (the flag was
opt-in and nobody knew the command existed; `cost off` opts out) and the
command now prints today's platform spend from GET /api/billing/usage?days=1
alongside the session estimate.

modules/ext/webdav.js — the inline WebView detector missed "Version/4.0"
Android WebViews, including the operator's own phone, so the login offered a
Telegram widget that cannot complete there. It now delegates to
modules/wx11-tg-webview.js when the page loaded it and keeps a matching
fallback (wv token, Version/4.0-Android, Telegram, FBAN/Instagram/Line).

modules/ext/media.js — text analysis model renamed deepseek-v4-flash -&gt;
deepseek-v4.1-flash in both the field and the printed label.

main.html + module-loader.js — v4.6.146 -&gt; v4.6.150 in LATEST_VER and the three
asset queries; core-bundle.js rebuilt (rebuild-bundles.py reports it already
in sync, 1,250,569 bytes).

Verified: node --check on all six scripts and the new test; the 16-case
orchestrator guard passes against the deployed bundle (ORCH_SRC=...); bundle
rebuild is a no-op; core-bundle.js?v=v4.6.150 answers 200.
</content>
</entry>
<entry>
<title>fix(newos4): read_file pages were silently truncated; delegation no longer invents "completed"</title>
<updated>2026-11-02T20:21:24+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-02T20:21:24+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=e2f36467748943639c960453549b6b91c8cc7ab2'/>
<id>urn:sha1:e2f36467748943639c960453549b6b91c8cc7ab2</id>
<content type='text'>
Two defects found by re-deriving a report that claimed both were still open.

read_file paging (module-aiassist.js)
- A page larger than the result clip was clipped AFTER read_file had computed
  next_offset. On the last page next_offset is null, so the model was told
  truncated:false while its tail had been removed: measured 11 890 of 19 999
  chars delivered, returned:19999, no pointer to the rest - 8 109 chars lost
  with no way to ask for them.
  * the clip now sets truncated=true and next_offset=offset+keep when it removes
    the tail of a page that had no continuation (and never splits a surrogate pair)
  * read_file caps a page at the result budget (min(pageCap, toolResultChars-200)),
    so a page is always delivered whole and next_offset is exactly contiguous
  Live v4.6.143: 19 999-char file -&gt; page 11 800 (next_offset 11 800), page 2
  8 199 (next_offset null): 11 800 + 8 199 = 19 999, nothing withheld. A model
  asking for limit:32000 is clamped to 11 800 and told truncated:true.
  The ~500-char symptom was the 500-char floor: a caller passing limit:200 gets
  500, which is why targeted sed -n reads looked better than the tool.

Delegation reporting (module-orchestrator.js, module-aiassist.js)
- chat() returning {content} was already fixed in 8a9333b, but three other places
  still substituted the word "completed" for an absent answer, so the symptom
  survived: delegateTask's outbox wrote status:'completed' even when the worker
  returned nothing; the delegate_task tool returned {success:true, result:"completed"};
  orchestrate-parallel summarised an unmatched agent as {status:'ok',
  result:'completed (agent recycled)'} and a missing outbox as "completed (no outbox)".
- Now: an empty/undefined answer is status 'failed' with error 'agent produced no
  answer' / 'no result in outbox' / 'agent not found — result unverifiable', and
  the tool returns success:false with result:null.

Tests: test-delegation-reporting.js 6 (RED 3/6 against HEAD: D3/D4 success:true,
D5 the literal "completed"), test-tool-context-bound 26 with E6-E8 (RED 24/2:
truncated:false, next_offset null). maintain 27/27.
</content>
</entry>
<entry>
<title>fix(newos4): memory-graph loads stop rewriting user data; unknown sizes say unknown</title>
<updated>2026-11-02T20:15:28+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-02T20:15:28+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=3b894c31c456592a21b36a7696423ad0936edab7'/>
<id>urn:sha1:3b894c31c456592a21b36a7696423ad0936edab7</id>
<content type='text'>
Three follow-ups from the merge review, each with the assertion that pins it.

module-memory-graph.js
- _normalizeEdges no longer mutates stored weights. It capped legacy 'failed' edges
  from 0.8 to 0.2 in memory, and the next debounced save made that permanent - an
  irreversible migration of the user's graph with no schema version. The cap moved to
  _effectiveWeight(), which ranking (showStats "Strongest relationships"), the session
  view and prune() now use; the stored record is left exactly as written.
- config.json is stamped version: 2 so a future format change can migrate deliberately.

ext/webdav.js
- the two fallback parsers emit size: null instead of 0. They cannot see the size
  column, and 0 rendered as "0 B" - the same "every file looks empty" symptom as the
  parseInt bug fixed in e639c2d, one regex miss away.
- _formatSize(null) renders '—' (unknown) rather than '0 B'; 0 still renders '0 B'.

module-aiassist.js
- the lock timeout now says what to do: "AI is still busy with another turn (waited Ns).
  Wait for it to finish, or run 'ai-stop' and retry - the running turn's lock is left
  alone." It used to be a dead-end string.

Tests: test-memory-graph 39 (T6.4 rewritten to the read-time contract: stored weight
stays 0.8, effective 0.2, success unaffected), test-webdav-sizes 28 (A14d null -&gt; '—').
maintain 26/26, live v4.6.141: _effectiveWeight 0.2 with the edge object unchanged,
_formatSize(null) '—', '0 B' still '0 B'.
</content>
</entry>
</feed>
