| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| |
|
|
|
|
|
|
|
|
|
| |
MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and
`enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed
nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees
warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled.
Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9};
{threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false.
test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three of the four agent-cluster items, each with a test that is red on HEAD.
- MAINT-008 (module-agent-registry.js + module-orchestrator.js): registerAgent took args[0] as the
agent id and only args[1] as the capability list, while both orchestrator callers passed the
command word first — so the boot log read "Agent 'register' registered with 1 capabilities" and
four capabilities were stored as one. The command word is now stripped, args[1..] are joined and
split on commas (both "a,b" and "a b c" work), and the callers pass [id, ...caps].
- MAINT-007 (registry + aiassist): mkdir/rm used relative paths, so a registration made from /docs
created /docs/agents/<id> and an unregister from any other cwd removed nothing — the leaked
directories the report suspected. Every path in the registry (and the four /_sessions writes in
module-aiassist.js) is absolute now.
- MAINT-009 (listAgents): a directory alone counted as a registered agent, so a hand-made
/agents/foo appeared as "foo — unknown (0 capabilities)". Only a readable status.json with an id
counts now; the rest are reported as unregistered directories.
Verification: test-agent-registry.js (11 asserts). RED on HEAD: 9 of 11 fail, including the exact
`✅ Agent 'register' registered with 1 capabilities` line and `hand-made — unknown (0
capabilities)`. Suite 15/15 files green; maintain --fix "node tests 15/15 pass".
Live on v4.6.109: boot prints "✅ Agent 'orchestrator-0' registered with 4 capabilities";
`agent-list` warns "1 directory(ies) in /agents have no status.json (not registered): hand-made";
register from / with a comma list stores 3; unregister from /docs removes the directory and leaves
no /docs/agents.
MAINT-006 stays open with a narrower note: the hard stop was only reachable when every call in a
batch was blocked, so a mixed batch (one repeated + one novel call) never ended. The same ceiling
now applies on the mixed path, but that branch lives inside agenticLoop and is not covered by a unit
test — it needs a live agent run that repeats a call.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
leave /skills
Two board items plus the context-window close-out.
- MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command
that is registered but has no help line, which reads as if the command does not exist. It now
says "X — registered by <Module>, which publishes no help line for it. Try: help". A command
that genuinely does not exist still gets the old message.
- MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with
their own index entries, so `skill list` filled up with system noise. Lessons now live in
/lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries
in /skills for one release, and the one-time migration deletes them from the skills index.
Verification: test-help-and-lessons.js (10 asserts, includes a lint-style guard that no lesson is
written into /skills). Suite 14/14 files green; maintain --fix "node tests 14/14 pass",
"help coverage 362/362". Live on v4.6.108: `help agent-register` -> "registered by
AgentRegistryModule, which publishes no help line for it"; `help a2a-prune` unchanged;
`help nope-not-real` still "No help available".
MAINT-001 close-out (no code change here): history is capped at 20 messages with a "[context trim]"
marker (first trim at turn 11 in a 60-turn probe), token compression took 80 msgs/8080 tok to
17/2625, and since v4.6.107 one turn's tool bodies are clipped to 6 000 chars each and elided past
24 000 with the newest 4 kept verbatim. Residual, recorded on the board: maxContextTokens is never
sent to the server as num_ctx, so the real window is whatever the endpoint defaults to.
MAINT-018 close-out: not reproducible. Live probes on v4.6.107/108 — `sed -n '1,3p' /probe.txt`
prints lines 1-3, `sed -n "2,4p"` prints 2-4, `grep 'line4'` matches, and the same commands inside
`script /q.sh` print correctly. The "Command not found: for;" symptom belongs to the line-by-line
script runner, which is MAINT-021 and stays open.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
a corrupt memory graph
The three items the last review and probes left open, each with a test that is red on HEAD.
- MAINT-023 (module-aiassist.js): one agentic turn grew without limit — every role:'tool' body
was pushed verbatim and only this.context was trimmed BETWEEN turns (measured 6 -> 112 705
tokens across 8 model calls against maxContextTokens 12 000). Observations are now clipped to
maxToolResultChars (6 000) with a note saying how much was withheld and to re-read with
offset/limit, and _boundToolContext() elides the oldest tool bodies once the running total
passes toolContextBudgetChars (24 000), always keeping the newest 4 verbatim. The ceiling is
turn-independent: the same ~26 k chars at 8 turns and at 20.
- MAINT-024 (modules/ext/skill-pack-core.js): the command printed "✅ Skill created" with
fs.writeFile()'s boolean discarded (probe: fresh VFS, success printed, no file), stripped
invalid characters instead of rejecting the name (My-Skill -> y-kill, DevOps_Thing ->
evpshing), and treated an unreadable /path argument as the skill body. Now: kebab-case is
validated (uppercase normalises and the final name is reported; anything else is refused),
/skills is created on first use, the write and index results are checked, and an unreadable
body path is an error.
- MAINT-003 (module-memory-graph.js): a corrupt store looked exactly like a fresh install —
fsReadJSON returns null on a parse failure, so 85 nodes could vanish with no warning and no
copy. The probe confirmed the loss: seed 6 nodes, truncate nodes.json and export.json, reload
-> 1 node, no message, nothing in .trash. _quarantineCorrupt() now keeps the unreadable file
as <name>.corrupt, warns, and then loads from whatever remains.
Tests: test-tool-context-bound.js (11), test-skill-create.js (16), test-memory-graph-recovery.js
(8). RED on HEAD: tool-context crashes on _clipToolResult (exit 1), skill-create 16 assertions
fail on the missing file/index, memory-graph B1/B2/C1 fail (no warning, no .corrupt copy).
Suite now 13/13 files green; maintain --fix reports "node tests 13/13 pass".
Live on v4.6.107: skill-create My-Skill -> ✅ "my-skill"; DevOps_Thing -> Invalid skill name;
/does/not/exist.md -> Cannot read skill body; only my-skill in the index.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
review findings
Second pass over v4.6.105, driven by an independent review that reproduced each item.
- ollama-manager.js: chat() now reports the captured cause ("Ollama error: Failed to fetch",
"…: HTTP 500") instead of the generic "No provider responded"; _lastChatError is reset per
call so an old reason is never replayed. chatWithTools carries the reason in its error string.
- ollama-manager.js: _ensureConnected() bounds connect() with a deadline (connectTimeoutMs,
default 10 s). connect() uses a bare fetch(), so a blackholed endpoint previously left the
shared in-flight promise pending forever and hung every later message — the old code failed
fast. The attempt is now try/finally, so a throwing terminal.log() can no longer poison
_connecting and suppress the retry on the next message.
- ollama-manager.js: an automatic connect no longer silently rewrites the user's saved model;
the previous choice is restored in memory and in localStorage, and the user is told.
- ollama-manager.js: the DeepSeek empty-content branch returned top-level tool_calls with
content '', so the caller pushed an assistant message with no tool_calls and then a
role:'tool' reply — an orphan tool message, which is the HTTP 400 that branch exists to
prevent. tool_calls now live inside the message as well.
- module-aiassist.js: a no-argument list_files returned the ROOT instead of the cwd (readDir('/')
instead of ls()); it now means the current directory again. Error text keeps the path as the
caller wrote it, and write_file distinguishes a read-only virtual path (/sys/…) from a missing
parent instead of blaming the parent.
Tests: test-ollama-error-text.js (new, 14 asserts, RED 9/5 before the edit), test-ai-fs-tools.js
7 → 16 asserts (write onto a directory, /sys write, list a file, '/', no-arg cwd, relative path),
test-ollama-autoconnect.js 11 → 19 (blackholed endpoint settles + retries, saved model survives;
the old "connecting hint" assertion is now falsifiable — the reviewer showed it also matched
connect()'s pre-existing line). Suite: 10/10 files green. Live on v4.6.106: healthy auto-connect
42 ms with the hint; blackholed endpoint settles in 1242 ms with connectTimeoutMs=1200, prints
"timed out" and "unreachable", _connecting cleared.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sees /sys
Three defects, each re-probed first and covered by a test that is red on HEAD.
- ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(),
so a user's first message died with "Ollama not connected. Use ollamaconnect first." even
when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and
chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only
then the error — which now says the endpoint is unreachable instead of telling the user to
run a command they already expected to work.
- module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool
answered "File written: …" anyway, sending the model on to a read that found nothing
(MAINT-012). It now returns "Write FAILED: … — parent directory missing or target is a
directory; nothing was written".
- module-aiassist.js list_files tool: the hand-rolled walk over root.content never resolved
the virtual mounts, so /sys — a directory the UI and `ls` both show — answered
"Directory not found" (MAINT-011). It now uses fs.readDir(), which resolves mounts and
returns null for a genuinely missing path.
Tests: modules/test/test-ai-fs-tools.js (7 asserts) and
modules/test/test-ollama-autoconnect.js (11 asserts). RED on HEAD: ai tools 4 passed/3 FAILED
(/sys → "Directory not found", write_file → "File written: …"), ollama 2 passed/9 FAILED
("Use ollamaconnect first"). GREEN now 7/7 and 11/11; maintain --fix reports "node tests 9/9 pass".
Live on v4.6.105: connectedBefore=false, _ensureConnected()=true in 41 ms, 37 models.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed
before the change and the new test is red on HEAD (5 passed, 7 FAILED).
- virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination
type check, so `mv file /docs` replaced the /docs node and every child vanished while the
command printed "Moved: …". Refuse when the destination is a real directory node (the
virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning
and then deleting the same key removes it; that path is now an explicit no-op.
- module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent
the operator looking for a missing source. The destination is now inspected first and the
message says "is a directory — refusing to overwrite it".
- module-fs.js ls: the path was only read from argv[0], so `ls -l /work` listed the cwd.
The first non-flag argument is now the target; the path-first form is unchanged.
Tests: modules/test/test-fs-mv-cp-guard.js (12 asserts) — RED on HEAD (A1-A3, B1, B2, C2, E1),
GREEN now; maintain --fix reports "node tests 7/7 pass". Browser on v4.6.104:
"mv: /vtest: is a directory — refusing to overwrite it", child and source intact,
"ls -l /vtest" lists /vtest (keep.txt), not the cwd.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
gate the build
Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was
correct; these are the defects it introduced or left behind, each measured first.
- virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring
it verbatim deleted every checkpoint, every agent session and the trash, made rollback
irreversible and unrepeatable. Re-attach the live copies after the root swap.
- checkpoint(): names are millisecond-resolution, so tight calls overwrote each other
(measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/"
(written but unlistable), and prune by snapshot createdAt instead of filename sort, which
let a custom name ("aaa-oldest") evict newer auto-named checkpoints.
- module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run
re-entered similarity as evidence and floored every prediction at 1 s. Backfill `usable`
from the measurement on load, drop the ones it rejects, add a belt-and-braces guard in
_findSimilar and count only usable points as actual executions.
- newos4-maintain.py: run every node-runnable modules/test/test-*.js on each maintenance pass.
The v4.6.102 regression was written but no job ever executed it.
- newos4-steer.py: `done` now requires --commit <sha> that exists in the repo, and gains
`reopen`. A written plan was being closed as done at 06:17 while the predictor fix shipped
nowhere, so the implementer's pre-flight queue was empty.
- steering.json: predictor item reopened, checkpoint item stamped with f7249ff.
- maintenance board: MAINT-002/005/015 were fixed in v4.6.81-85 but still listed open/high;
closed with their commit ids, and MAINT-010/016 annotated with the probes that keep them open.
Tests: test-vfs-checkpoint 23/23 (RED 14/9 before), test-predictor-accuracy 38/38
(T1-T3 RED on HEAD), maintain --fix all PASS, browser round-trip
"Checkpoint created: <name>" -> "Rolled back to checkpoint: <name>" on v4.6.103.
Also carries the already-deployed 2026-10-26 syntax-gate hunk in newos4-maintain.py (syntax_failures + gated rebuild) that had never been committed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Task 3 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
New modules/test/test-vfs-checkpoint.js loads the real VirtualFileSystem and
SystemModule with guarded localStorage/indexedDB/window stubs and asserts:
A1-A4 checkpoint('') returns the resolved name; two calls differ; the file
exists under the returned name
B1-B2 checkpoint-list shows the returned name; rollback(<that name>) is true
C1-C4 the `checkpoint` command prints exactly the name it created (and an
explicit name verbatim)
RED before the fix (v4.6.101):
FAIL C2 printed name exists on disk — printed=2026-10-27T07:31:21.575Z
listed=["2026-10-27T07:31:21.574Z"]
test-vfs-checkpoint: 2 passed, 8 FAILED
GREEN after:
$ node modules/test/test-vfs-checkpoint.js
test-vfs-checkpoint: 10 passed, 0 failed
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Task 2 + ship of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
module-system.js `checkpoint` read the clock a second time for the printed
name, so the name in "✅ Checkpoint created: <name>" differed from the name on
disk by >=1ms whenever no explicit name was given. It now prints the value
returned by fs.checkpoint(). try/catch and error strings unchanged.
Bundles rebuilt (rebuild-bundles.py); LATEST_VER in module-loader.js and all
three ?v= tags in main.html (core-bundle, module-loader, critical-ext-bundle)
bumped v4.6.101 -> v4.6.102.
Verification:
$ node --check modules/core/module-system.js
SYNTAX OK
$ node --check modules/core/core-bundle.js
BUNDLE SYNTAX OK
$ node modules/test/test-vfs-checkpoint.js (x5)
test-vfs-checkpoint: 10 passed, 0 failed
$ curl -s https://webx11.duckdns.org/spaces/newos4/main.html | grep -o 'core-bundle.js?v=[^"]*'
core-bundle.js?v=v4.6.102
$ python3 scripts/newos4-maintain.py
PASS: version v4.6.102 x3 in main.html (expect 3) ... maintenance result: OK
$ python3 scripts/newos4-skill-audit.py
skill audit: 7/7 pass
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Task 1 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
virtual-fs.js checkpoint(name) generated an ISO name when none was given but
returned bare `true`, so callers could not know which name the snapshot was
written under. It now returns the resolved name (string). Prune (max 3) and
saveToStorage() behaviour unchanged.
Verification:
$ node --check modules/core/virtual-fs.js
SYNTAX OK
$ node modules/test/test-vfs-checkpoint.js
test-vfs-checkpoint: 9 passed, 1 FAILED (remaining FAIL was the
module-system display path, fixed in the next commit)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The two panes kept separate clipboards (filemgr._clipboard vs webdav._selected), so "cut here, paste
there" did not exist — the operator's last phone-test item.
filemgr.js _openOverlay() publishes a read-only view of its clipboard on the terminal object:
webx11.fileClipboard() / fileClipboardClear() / fileClipboardSet(paths, mode).
webdav.js the bulk bar gains "📥 Paste N" (hidden when the clipboard is empty). Handler: for each
path readFile -> _webdavRequest('PUT', dest + '/' + name, {body}); only 200/201/204 counts.
For mode "cut" the local file is deleted AFTER the server accepted the bytes, never
before. A failed PUT keeps the local copy, prints "📥 Paste failed: <name> — HTTP n",
and leaves that path in the clipboard so "📥 Paste 1" is a retry. Successes leave it.
Summary "📥 Pasted N item(s) to <path>" (+ "— M failed"), then the pane reopens there.
Semantics: cut moves (upload then delete local), copy leaves the local file and keeps the clipboard so
the same files can go into several folders.
Verified with the transport stubbed in the live page (this browser has no WebDAV credential):
button "📥 Paste 2" for a 2-path cut
PUT ok (201) -> local file null (moved)
PUT fail(500) -> local file "BBB" (kept)
clipboard after -> {mode:"cut", paths:["/fail-d.txt"]}, button "📥 Paste 1"
node --check both modules, no-undef clean (55 files), v4.6.101 x3 in main.html, syntax OK, help 362/362.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
share, dead-end fullscreen, silent WebDAV move
All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code.
A "No file selected to download": the preview pane had no close control; the only ✕ was the file
manager's own, and it calls _closeOverlay() which wipes _multiSelect/_selectedFile/_selectMode.
On a phone the preview covers the list, so closing it before pressing Download always emptied the
selection. The toolbar now starts with ✕ -> _clearPreview(), which keeps the selection and leaves
fullscreen first; Download with no targets names the fix instead of stating a dead end.
B share/copy: 🔗 existed only in the preview toolbar and took a single path, and after the Android
share sheet there was no copy affordance. Added _sharePaths(paths, mode) used by both the toolbar
(1 path, 📋 copy and 🔗 share) and a new action-bar 🔗 Share (whole selection), plus
_inlineLinkBox() which renders a selectable input/textarea with all URLs when neither the share
sheet nor the clipboard is available. No prompt() anywhere on this path.
C fullscreen: requestFullscreen targeted .fm-preview-body, which sits INSIDE the pane but excludes
the toolbar, so ⛶ and ✗ disappeared and only the OS back gesture got out. Now fullscreens
#filemgr-preview — toolbar, ✕ and ⛶ stay on screen and ⛶ toggles.
D webdav move: bulk Move asked for the destination with prompt(), which returns null in iOS
standalone PWAs and most Android WebViews, so it silently did nothing and looked unimplemented.
Destination is now an inline row in the bulk bar (44px input + "Move here"/"Cancel", prefilled).
Verified on the served code: ext bundle md5 5f440c77b6ab5529729aee8019a6c351 (served == local),
webdav.js carries wdav-move-dest x2, and a headless browser probe on the real page shows the toolbar
as ["✕","⛶","📋","🔗"], closing the preview prints "Preview closed — selection kept" with the
selection intact, selecting 3 files and copying yields a textarea with the 3 real WebDAV URLs, and
Download with nothing selected explains what to do. 6 PASS on the URL test, no-undef clean,
v4.6.99, help 362/362.
Not done, deliberately: cut/copy + paste across the VFS/WebDAV border. Recorded as Issue D2 in the
plan with the exact insertion points, the upload-then-delete ordering rule and the failure case.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
multi-select, Web Share, 44px targets
Executes .hermes/plans/2026-10-24_051600-newos4-mobile-download-share.md (approved via the feedback
dashboard; marker autonomy: safe). Front-end only, v4.6.94 -> v4.6.95.
T1 modules/ext/filemgr.js — URL.revokeObjectURL ran in the same tick as a.click(); iOS Safari and
Android Chrome hand the blob: URL to the download manager asynchronously, so the transfer was
aborted while the status line still said "Downloaded". Now revoked after 30 s.
T2 download honours the multi-selection via _getSelectedPaths() (falling back to _selectedFile),
reports "⬇ Downloaded N files" and "❌ Skipped <path>" instead of a silent partial success.
T3 share: navigator.share -> clipboard.writeText -> inline read-only <input> (focused, selected).
prompt() is suppressed in iOS standalone PWAs and most Android WebViews, which is why tapping 🔗
did nothing. Unresolvable space now reports "🔗 Share unavailable — no space resolved".
T4 both preview toolbar buttons: min-height/min-width 44px, touch-action manipulation, 1em, gap 10px
(was 24px/0.85em/6px — the smallest tap targets in the UI).
T5 _previewShareUrl falls back _resolveSpaceName -> _detectSpaceName (webdav.js:22) -> page path,
and returns null rather than emitting /spaces//<path>.
T6 the URL helpers are now module-scope pure functions exported for Node, so
modules/test/test-download-url.js tests the shipped code instead of a local copy (4 -> 6 cases).
Also fixes a latent bug that made require() impossible: `typeof window.webx11` throws under Node
because typeof does not protect a member access on an undefined global.
Verified on the served bundle (filemgr.js is concatenated into critical-ext-bundle.js, NOT
core-bundle.js — the plan's T7 grep pointed at the wrong file): HTTP 200, 129 867 B, 2x
navigator.share, 3x fmBuildShareUrl, deferred revoke present, 11x min-height:44px.
Live browser probe on /spaces/newos4/main.html: _previewShareUrl('docs/a.md') ->
https://webx11.duckdns.org/spaces/newos4/docs/a.md (no double slash); with navigator.share absent
and clipboard blocked, clicking 🔗 produced .fm-share-input containing that URL; button style ends
min-height:44px;min-width:44px;touch-action:manipulation; toolbar gap:10px.
node modules/test/test-download-url.js -> 6 PASS. lint-no-undef clean (55 files).
maintain: PASS version v4.6.95 x3, syntax OK, HTTP 200 /main.html.
Also adds admin/docs/session-overview-2026-10-26.md (glm-5.3-compressed session record).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
leave the rest
The operator asked: "when I don't do anything, an agent can do it for me and I can just follow up
or steer it." This is that agent, deterministic (no LLM): same input, same decision, and every
reason is a grep or a commit hash that can be checked.
spaces/newos4/scripts/newos4-feedback-autoreview.py — rules, first match wins:
R1 duplicate same fingerprint already decided -> resolve
R2 already-fixed identifiers absent from modules/**/*.js AND
`git log -S<token>` proves a commit removed them -> resolve
R3 noise fetch/abort/probe patterns -> reject
R4 still-present auto report + stack + identifiers still in code -> approve (max 2/day)
R5 plan / R6 user report -> never decided
Writes to the same append-only decisions.jsonl with actor=auto-reviewer, note + evidence; the
applier's last-wins rule means any operator click overrides an agent decision. Kill switch:
data/feedback/autonomy.json {"mode":"off"} (safe default, aggressive = cap 5).
Cron job e31ac58a559e, 07:40 and 19:40, no_agent, silent unless it decided something.
Fixture proof (6 cases, /tmp corpus, git repo with a real removal commit):
RPT-A duplicate -> resolve (duplicate of already-decided RPT-B)
RPT-C already-fixed -> resolve (goneFromCode removed from modules/ in fc4eaf3)
RPT-D noise -> reject
RPT-E still-present -> approve
RPT-F user report -> SKIP
RPT-G never existed -> SKIP <- caught the first draft, which called it "already fixed"
Live dry-run on the real corpus: "nothing provable (0 left for the operator, mode=safe)" and
`git log -S emptyTrash -- spaces/newos4/modules` resolves to 563a9be, i.e. the report I wrongly
approved by hand tonight would have been auto-resolved correctly.
Dashboard: agent-decided cards carry a 🤖 badge + the reason + "Any button below overrides it",
and the tab shows "🤖 feedback agent: N decided on its own in its last run (safe mode, M left for
you)".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The debug supervisor files an auto report for any failed fetch, so a flaky phone connection or a
retired endpoint became a review card. One cluster held 14 407 + 1 031 occurrences and produced
3 of the 19 cards in the queue; 10 of 21 items reviewed on 2026-10-26 were noise for this reason.
api/debug/report.php: an auto report whose *every* error is network-class (fetch failed /
failed to fetch / network error / load failed / aborted / signal is aborted / ERR_INTERNET_*)
now increments data/debug/network_failures.json (per day, per source path, top 40 kept) and
returns 202 {filtered:"network"}. No RPT file, no card.
Never filtered: any report with auto=false (user content), and any auto report carrying a
stack/TypeError, i.e. a real defect. Verified on a throwaway tree and live through nginx:
auto network poll.php -> 202 filtered, no file
auto network /ollama/ -> 202 filtered, no file
auto TypeError + stack -> 201 filed (RPT-20261026-134455-95f10612)
user report w/ "fetch" -> 201 filed (RPT-20261026-134455-ab0f2cd4)
Live: counter total=1 by_source {'/spaces/newos4/api/chat/poll.php': 1}, reports stayed at 19.
The dashboard now shows the counter instead of silence:
"🌐 1 network failure counted at ingest today, not filed as cards — /spaces/newos4/api/chat/poll.php ×1"
|
| |
|
|
| |
reports referenced a file that did not exist
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The cards only carried a title, a path and buttons — approving blind was the only option.
The page cannot read .hermes/plans/*.md itself (600 darkbot, php-fpm is www-data), so the
collector — already running as darkbot every 10 minutes — now embeds what a decision needs:
plans -> excerpt (first 60 lines, 2.4 KB cap), task headings, status/implementation lines
reports -> body, page url, newOS4 version, user agent, first 3 error strings
Modal: click a card title (or, on a report, read it in place) -> title, meta line with
path/size/modified/task count/decision, the text in a scrollable monospace block, and the
same Approve / Reject / Noise / Resolve actions. Esc, backdrop and the X close it; on a
phone it is full-screen. Cards and modal share one actButtons() helper instead of
duplicating markup.
Also fixes a bug I introduced earlier: recent_apply() referenced APPLIED, which was never
defined — the NameError was swallowed by the function's own except, so the "last decision
applied" line could never render. It now shows, e.g. "last decision applied 2026-10-26
12:47:03 UTC (plan:…mobile-download-share.md → approve)".
Verified: node --check on the inline script; browser harness against the real snapshot
(plan modal shows Goal + 9 tasks + status, report modal shows message/errors/page/ua,
Esc closes); verify-mobile 390/360 x light/dark ALL VIEWPORTS OK with the modal open;
collector py_compile + run (recent_apply 8 rows).
|
| |
|
|
| |
cleaner directives
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Dashboard (/admin/feedback.html, admin-gated)
- User-feedback tab over the live :8908 API: list, filter, search, publish reply,
archive/reopen. Reply is what the reporter sees in /stear -> Your reports & replies.
- Agent-items tab: 3 plans parked at autonomy:review + 19 defect reports, one click each.
- Snapshot age line + stale warning (>30 min), and the last apply outcome with failures.
- Existing /admin/ tab relabelled "Client error log" (it renders feedback_log.json, not
the feedback store) and now links to the review dashboard.
Decision path
- admin/api/feedback-decide.php: exact-match nginx location + auth_request, Origin check,
item_id allowlist, append-only decisions.jsonl with fwrite+fsync verification.
- scripts/newos4-feedback-apply.py (darkbot): last decision per item wins, superseded rows
recorded, reject reverts safe->review, per-item try/except, ledger saved per mutation,
realpath containment, exact standalone-line marker flip, file mode preserved, queue
rotation at 1 MB. Steering directives now carry the report title.
- scripts/newos4-feedback-collect.py: snapshot for the dashboard, 40-line plan head,
steering shape assert (-1 = unknown), recent_apply outcomes.
- newos4-feedback-sync.sh: flock -n so overlapping runs cannot double-apply.
Guide + terminal
- feedback-guide.html: which channel (terminal feedback / /stear / automatic reports),
expectations, exact payload per channel, how to write a report that gets fixed.
Linked from /stear, index.html footer and help.html.
- newOS4: feedback list|read|reply|archive|reopen|stats|dashboard|help (+ feedback-admin
alias), admin-token derived per call, argument-validated dispatch so a sentence like
"feedback list is broken on mobile" is still a message, "feedback -- <text>" forces it.
- help.html: 8 rows, coverage 362/362.
Review: glm-5.3 REWORK -> 15 findings fixed, 5 refuted with evidence, 2 doc-only.
Guide corrected (invented "60 MB per batch" replaced by the real 512 KB/5-per-10-min limits).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.91)
P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k
threshold in this tree — the budgets are maxContextTokens (12000) and maxContext*2 messages — so
rather than chase a number that does not exist, this applies the decision that is defensible
either way: warn before the cliff, never lose history without saying so.
- 80% of the token budget prints one warning per crossing (no per-turn nagging)
- the message-count trim unshifts a marker naming how many messages left the window and quoting
the earliest one, instead of splicing them away silently
- 'max-context-tokens (default: 2000)' now reports the real code default, 12000
Verified: 900/1000 tokens -> warns once, second call silent; 1500/1000 -> compresses to 17
messages with the summary head intact; 9-message window with maxContext=2 -> 4 messages, marker
'[context trim: 6 earlier message(s) ... earliest was: "turn 0 ..."]'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.88-90)
One phantom entry, three symptoms:
- ls() injected 'sys' whenever it was called without a path, so 'cd _agents; ls' advertised a
sys directory that does not exist, and list_files('_agents/sys') answered 'Directory not
found' — the reported false negative. Injection is now gated on dir === root.
- tree() pushed the same virtual name into the walk and dereferenced it, throwing mid-output on
every root listing. Guarded.
- _gatherFileInfo() walked into /sys and counted a synthetic subtree: 146 files / 114 dirs for a
44-file / 15-dir store. Virtual entries are skipped.
Verified: root still lists sys; _agents and skills do not; tree('') completes; world model total
(44) equals an independent walk of fs.root (44), agree: true.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.86-87)
The reference checker ran /skill-view \S+/ over every skill's prose, so 'skill-view <name>' and a
quoted example in skills-system-guide were counted as missing skills: 'Broken refs: 2' with no
names, both phantom. Placeholders and quoted names are now ignored (trailing punctuation stripped
first), and the survivors are listed in the audit line and in the suggestion.
Verified: clean tree -> Broken refs: 0. A probe skill referencing 'ghost-skill' -> Broken refs: 1
(probe-broken-ref → ghost-skill), while '<name>.' and '"module-management"' in the same file are
ignored.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
getStaleSkills() returned daysUnused=999 for a skill that had useCount 0 and no lastUsed — a
placeholder rendered by the audit as '999d unused', indistinguishable from a measurement. It told
the operator to archive procedural memory that had simply never been instrumented.
Now: daysUnused null + neverUsed true when there is no lastUsed; real day count otherwise; the
audit prints '(never used)'.
Verified: probe skill with no telemetry -> {daysUnused: null, neverUsed: true}; one last used
2026-08-01 -> 83; no 999 anywhere in getStaleSkills() or the audit output.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
_modelDigest() returned JSON.stringify(...) cut at 4000 chars plus '…(digest truncated)'. The
tool then did JSON.parse() on it, so the default detail threw 'Unexpected end of JSON input'
every single call; detail:'full' worked only because it returned the object untouched.
The digest now returns an OBJECT (_modelSnapshot) and shrinks by reducing list lengths, then by
dropping whole sections with an explicit 'omitted' list. The string form is generated at the
edge and is always valid JSON.
Verified: default call -> no error, 13-key snapshot, facts answered; caps 400/1200/4000 all under
cap and parseable; the residual floor at cap 200 is identity fields only.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Two defects in one parser, both silent:
- content = args.slice(2).join(' ') destroyed every newline, so markdown arrived as one line
- the last argument was treated as tags whenever it contained a comma, and stayed in the body:
a multi-line skill became a single line with ~13 bogus tags, reported as success
Bodies can now be read from a VFS path (the only way markdown survives an argv splitter),
tags are opt-in via --tags=, category via --category=. skill-patch takes a path too.
Verified: body from file -> 6 newlines preserved, tags [alpha,beta], category testing;
inline body containing four commas -> tags [], body intact.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.81)
delegateTask declared `const outboxResult` inside the AI branch and returned it outside, so a
delegation with the AI module missing threw 'outboxResult is not defined'. The catch turned that
into _handleAgentFailure: the worker stayed idle with no result while spawn_agent reported ok —
the reported symptom exactly. Now function-scoped, and the no-module path returns
'queued — AI module not loaded, nothing executed' rather than claiming 'completed'.
Also: nginx served _docs/*.md as application/octet-stream (no .md type) — now text/markdown +
nosniff; agent-replies.php returns 410 for GET as well as POST, so there is one reply path.
Verified: with the AI module deleted at runtime, delegateTask returns the queued string and
throws nothing.
|
| |
|
|
|
|
|
| |
Contract now documents truncated/cursor semantics, the full code table with retry rules,
503-from-nginx (HTML, retryable), the two-cursor rule, strictly-increasing epoch, and the
security posture in plain words: client-minted tokens mean public write, mitigated but not
closed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
double print (v4.6.78-80)
12 findings accepted, 3 refuted with evidence (see _docs/chat-review-glm-2026-10-23.md).
Data loss: a corrupt thread read as [] and the next append rewrote it with one message —
now 500 and the file is left alone. Cursor after a >50-message truncation pointed at the
thread tail, making the oldest permanently unfetchable — now the last message actually
sent, plus truncated:true. Two posts in the same millisecond shared a cursor — epoch is
now strictly increasing.
Security: chat.py show() had no id validation and joined argv onto CHAT_DIR ('../../x'
read any .json as darkbot) — THREAD_RE added. The auto-reply prompt now treats message
text as untrusted data and forbids acting on instructions found in it.
Ops: relay housekeeping swept neither .lock-* nor orphaned .tmp-*; a crashed listing
looked identical to an empty queue; grep AWAITING also matched message bodies; hermes-poll
printed every reply twice.
Verified: corrupt file -> 500 with the file untouched; traversal -> 'invalid thread id';
hermes-poll -> exactly one line per reply.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
1. chat.py renamed the temp file AFTER releasing the flock, so two writers could each
read the pre-replace snapshot and the later rename silently dropped the earlier
message. os.replace now happens inside the lock.
2. The agent cursor was global. After hermes-reset it pointed past the end of the new
empty thread, so every read returned nothing — the agent would think it had been
ignored. Key is now per thread and reset clears every chat cursor.
3. chat-relay.sh had no overlap guard: a slow agent run (up to 5 min) could collide with
the next tick and answer the same thread twice. flock -n on /tmp/chat-relay.lock.
Verified: stale cursor 'webx11_chat_agent_cursor_oldthread00000' set to 9999999999999,
hermes-reset -> zero cursor keys remain, read returns {messages:[], note:'no new messages...'}.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two readers shared one cursor. The 30 s UI poller printed each reply and advanced
webx11_chat_state.cursor, so the agent's chat_hermes read then asked for 'since=<that>'
and got an empty list — the answer appeared on the terminal and never reached the model.
That is the reported symptom exactly.
chat_hermes read now uses webx11_chat_agent_cursor, starts from 0 on first use so the
whole thread is visible, and never touches the display cursor. Empty reads say why
('still awaiting Hermes' vs 'no new messages since your last read').
Proven: UI poll consumed the reply (printed to terminal), then chat_hermes read still
returned both the agent's question and Hermes's answer.
|
| |
|
|
|
|
| |
The OS4 agent reported the skill cites /_docs/chat-contract.md, which does not exist in its
VFS — correct: the file lives on the host and is served over HTTP. Browser VFS and disk are
two stores, so a path-only pointer is a dead end. Now names the URL.
|
| |
|
|
|
|
|
|
|
|
| |
A command that exists but is undocumented might as well not exist: the agent had no way to
learn that `hermes` exists, that `chat` is taken, that replies are asynchronous, or that
the cursor is milliseconds. Skill covers the commands, the chat_hermes tool, the failure
table (401/413/429/503/507), the rules that keep it useful, and how to verify a send.
Seeded from skill-pack-core._buildSkills() so every fresh session gets it; mirrored to
skills/chatting-with-hermes.{md,json} + index.json (19 entries).
|
| |
|
|
|
|
|
| |
Seven behaviour changes it has to act on, not just seven code changes: the command is
`hermes` not `chat` (ChatStudio owns that name), replies arrive untyped within 30s,
the cursor is epoch milliseconds, only 429 is retryable, the old reply endpoint is 410,
multi-path grep/head/tail, and on-demand agents are not a fault.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase C as repo edits (browser edits in _mods/ do not survive a reload).
- `hermes <msg>`: sends through the chat endpoint and always prints an outcome —
'📨 Sending…' then either '✅ sent id=… epoch=…' or a coded error with a hint
(429 retry later, 503 host busy, 401 run hermes-reset). `feedback` stays as the alias.
- NOT named `chat`: ChatStudioModule already registers `chat` and dispatch walks modules
in order, so a second `chat` was silently unreachable. Found by test, not by reading.
- `hermes-poll` / `feedback-inbox`, `hermes-reset` (token rotation).
- Poller: 30 s, paused while the tab is hidden, polls on visibilitychange, first poll
adopts the cursor and prints nothing, cursor persisted in webx11_chat_state.
- `chat_hermes` tool: send + read from inside the agent loop.
- Contract/help updated for the rename.
Acceptance: 'hermes acceptance test' in the browser, reply written host-side with
scripts/chat.py, appeared in the terminal by itself 30 s later with no command typed
(cursor 1792776973800).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
retired (v4.6.71 phase B)
- api/chat/{lib,post,poll}.php: thread = sha256(token)[0:16]; flock read-modify-write;
tmp+rename; ms epoch cursor; per-thread + per-IP windows with retry_after_ms;
401/413/507/405 carry a machine code, only rate_limited is retryable
- _docs/chat-contract.md: frozen shapes, incl. why the cursor is ms and not ts
- scripts/chat.py: list/show/reply, UTC, locked, atomic replace
- api/debug/agent-replies.php POST -> 410 Gone with pointers (one write path)
Measured: 401 no-token, 201+id, poll 1 msg, poll>since 1 new, data/chat 403,
4001 chars 413 (codepoints), 429 with Retry-After 576, round trip via chat.py.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.70)
Two decisions recorded by the operator on 2026-10-23, implemented.
6 — spawn-on-demand is the design, so stop reporting it as a defect:
- OrchestratorModule counts delegations inside delegateTask()
- status prints "Mode: on-demand — N/M worker(s) available, X delegation(s) this
session" and drops the "spawning is not dispatching" warning
- per-agent lines read "available, no delegations yet (spawned ...)" / "N
delegation(s), last ..."
10 — per-user, server file canonical, browser VFS a TTL'd read-through cache:
- store = the user's own WebDAV space (<space>/_data/predictor.json), so identity
comes from credentials the app already has; no new endpoint, no unauthenticated
write surface
- 5 min TTL: cache wins while fresh, server wins when it differs; a banner naming
both counts prints only on divergence
- write-through on save (debounced 5 s) with a MKCOL retry when the collection does
not exist yet; store mode is printed by predictor-status
- the cache write is guarded and verified — a missing /system/metrics made writeFile
fail silently, which is how the first version lost a pull
Verified live on v4.6.70 with a stubbed transport: pull applies 5 pts/5 paired and
writes a stamped cache, the divergence banner fires once and not on the second
(equal) pull, write-through PUT is issued, status reads "Store: server". A real
WebDAV round-trip still needs the user's WebDAV config in-app; the agando space has
no _data collection yet, so GET returns 404 -> "server (empty)".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Packet 1's multi-path ask was only honoured by wc in v4.6.58 — grep read only
args[ai+1] and head/tail only positional[0], so `grep x a b` searched `a`, printed
one file's hits, and exited 0. Same silent drop, three more commands.
- grep: loops every path; multiple files print `file:line: text`, `-c` prints one
count per file, missing files are reported per file and excluded from the summary
count; single-file output unchanged
- head: every path, `==> file <==` header when more than one; `head f 40` still works
- tail: every path with headers; a numeric second positional is still the legacy
`tail <file> <count>`; `-f` warns and follows only the first file instead of
printing all of them and claiming otherwise
Verified live on v4.6.67:
grep "bug_id" a b -> a:2 + b:3 + "2 match(es) in 2 file(s)"
grep -c "alpha" a b -> one count per file
grep "alpha" a missing -> hits from a, per-file error, "in 1 file(s)"
head -n 1 a b -> header + first line for each
tail -n 1 a b -> header + last line for each
tail a 2 -> legacy count still honoured
tail -f a b -> warns, follows a only
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.65)
Packet 9. `terminal-commands-silently-failing` and `command-composition-rules` described
the same bug family from two angles, and since v4.6.58 both were describing behaviour
that is now refused outright.
- new seeded skill `command-composition-rules` in skill-pack-core's _buildSkills():
the rule, why the refusal exists (with the 42-42 / 34k-a measurement), what works
(one command per call, wc multi-path, interactive-only pipes, quoted operators),
how to verify a plausible-looking result, and the pitfalls
- its History section names both retired skills so a reader arriving from either
search finds the surviving one
- mirrored onto disk as skills/command-composition-rules.md + .json and added to
skills/index.json (18 entries) so the repo copy matches what sessions receive
Verified live on v4.6.65: seeding reported 15 skills (was 14), the file and index
entry exist in the fresh VFS, and the natural-language query "does the terminal have
a shell and what is refused" returns command-composition-rules first (13.78, 3/4).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.64)
Packet 8. `new RegExp(pattern)` sat unguarded in the terminal grep handler and in
VFS find()/grep(), so a bad pattern threw out of the command handler into the error
ledger -- the same ledger packet 2 had to stop trusting.
- module-fs.js grep: compile in try/catch, print
"grep: invalid pattern: <pattern> (<engine message>)" as a warning and return
- virtual-fs.js find()/grep(): same guard, return [] and stash the message on
_lastPatternError instead of throwing into the module error wrapper
Verified live on v4.6.64 with the packet's own repro: `grep "/\bug_id\" file` prints
the message, the error counter stays 0 (was: one ledger entry per call), a valid
pattern still matches, and fs.grep('[') returns [] instead of throwing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.63)
Packet 6 and 7.
6 — four agents showed `sleeping` and nothing distinguished "waiting for work" from
"never handed a task". Added tasksAssigned/lastTaskAt to the agent record (counted
where a task is actually assigned), and the status line now reads
idle — NEVER SCHEDULED (0 tasks since 2026-10-23 16:22)
idle — waiting for work (3 task(s), last 2026-10-23 16:22)
parked — NEVER SCHEDULED ...
plus "Dispatch: 0/3 agent(s) have ever been given a task" and a warning that
spawning is not dispatching.
7 — /guides/ was reachable by nobody. New `guides` command lists it newest-first
with sizes and the preview/cat hints; help.html gained a Guides nav entry and
section with links to the published guides.
Verified live on v4.6.63: `guides` → 2 entries after seeding two files;
orchestrator-status shows 3x NEVER SCHEDULED on a fresh session (the packet's own
observation), and "waiting for work (3 task(s) ...)" once a counter is set, with
"Dispatch: 1/3".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Packet 4. The matcher tested the whole query as one substring against
name+description+tags: word order decided the outcome ("telemetry dashboard" = 0
while "telemetry" = 1), a sentence could never match, and skill bodies were never
searched at all.
- _skillTokens(): lowercase, split on non-word chars, drop <3-char words and a
stopword list; a query made only of stopwords keeps its literal form
- scoring: name 3, description 2, tags 2, body 1 + up to 1.0 for repeated hits,
multiplied by token coverage (0.5 + 0.5 * matched/total), +3 phrase bonus so word
order can never zero a real hit
- results carry score and matchedTokens ("2/3"); top 3 by score, name as tiebreak
- zero results now name the tokens searched and the closest skill names instead of
"No matching skills found"
Verified live on v4.6.62 via the real runTool entry point:
"telemetry dashboard" and "dashboard telemetry" both return the same 2 skills
(was 0 and 0); "how do I regenerate the skill index" returns skills-system-guide
first (8.96, 2/3) where a sentence previously matched nothing; "info" returns 3
body matches; a nonsense query returns 0 with the tokens and closest names.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Packet 2 (three surfaces, three answers). Measured first: `errors` scanned
/logs/current.log for [ERROR] lines, but that file only ever receives explicit
terminal.log('ERROR', ...) calls -- exactly one call site in the whole app -- so it
read 0. The dashboard added DebugSupervisor._errorDB plus failed tool calls (18),
and the world model read the debug DB alone (11).
- terminal.countErrors() / countLogErrors(): the single counter, exposed on the one
object all three surfaces already hold
- `errors` prints the canonical total with its breakdown, the entry list (hash,
count, severity, message), and reports the log scan separately as a mirror
- dashboard health.errors and health.errorBreakdown derive from it
- world model telemetry.recentErrors derives from it, plus errorsBySource
- terminal.log('ERROR', ...) now also records into DebugSupervisor, so the log is a
human-readable mirror of one store instead of a second store almost nobody wrote
Verified live on v4.6.61: with a seeded debug entry + failed tool call, all three
report 2; a single log('ERROR') call moves debug 1->2 and the total by exactly 1
(the mirror line is counted once, in the log, and reported separately).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Packet 1 and 3 (silent wrong answers from a shell-less terminal).
- detectComposition() in the security gate refuses ; && || | before dispatch.
`;` used to fail loudly, but `&&`/`||` folded the right-hand side into the left
command's argv (wc -l f && grep -c x f returned "42 42"), and a `|` arriving from
the AI run_command tool bypassed splitPipe entirely and degraded the pattern to
empty -- which matches every line. Operators inside quotes are untouched.
- wc accepts multiple paths: one line per file plus a total, per-file errors for
missing paths. `wc -c a b` used to print only `a` and exit 0.
- help.html: "There is no shell" callout at the top of the command reference,
plus the v4.6.57/58 changelog entry.
Verified live on v4.6.58 by replaying the exact call sequence used by both paths:
interactive `wc -c a b` -> "14 a / 10 b / 24 total"; `&&` and `;` refused;
`echo "a && b"` still echoes; AI run_command semantics with `|` and `&&` refused;
single command and multi-file wc still return real output.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.57)
Acting on the "Request to the newOS4 team" packet (spaces/agando/guides/,
re-verified by its author on 4.6.56). Three verified defects fixed:
Packet 5 (predictor arithmetic) - module-predictor.js
- the persisted field named mape held a SUM while predictor-status divided by the
paired count: renamed to errorSum, older payloads read through
- errorPct is stored raw; the 500% clamp is now render-only, so a 475x miss stays
visible instead of being flattened to the ceiling
- predictions are floored at 1 s and marked floored; a floored prediction is
consumed but never scored as 100% error (a 0 s prediction is not a prediction)
- observations shorter than 1.5 s (aborted/instant runs) are marked unusable and
no longer train _findSimilar - one 0.079 s run was rounding the next prediction
to 0 s
- status prints an unpaired and an unscored count alongside the mean
Packet 10 (verification cannot see its own fixes) - module-aiassist.js
- view_module now returns the DEPLOYED source, prefixed with file and version,
and only falls back to the _mods copy with a "may be stale" banner. The
unversioned _mods/<Class>.js edit cache used to shadow the deployed file
forever, so the viewer showed pre-fix code while the bundle was new.
Proven by seeding a stale _mods/PredictorModule.js and calling the tool.
Packet 1b (ls exits clean with no output) - module-fs.js
- ls rendered only via printClickable(), which writes straight to the DOM, so any
capture (AI run_command, diagnostics) saw an empty result and reported "empty
directory". It now always prints a text line and keeps the clickable chips when
nothing is capturing.
Test: modules/test/test-predictor-accuracy.js 34/34 (new cases Q, R, S).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The accuracy readout was structurally 0%: module-predictor.js:187 incremented the
denominator on every observed run while the numerator only moved when a stored
prediction existed, and predictions were stored only by the manual `predict`
command (the AI tool predict_task computed one and discarded it). Replaying the
AI loop's own call path with 6 tasks printed "Predictions made: 0, Accuracy: 0/6".
- pending predictions with a 15m TTL, consumed by threaded id -> task hash ->
cli-only similarity >= 0.5; unpaired runs never enter the denominator
- duration scored within 30% or 3s (integer predictions vs fractional observations)
- errorPct capped at 500% so one miss cannot poison the mean error
- schema v2: v1 correct/total counters dropped on load, observed runs and the
prediction counter kept; task text stored as an FNV-1a hash only
- tuning (confidence-threshold, min-data-points, pair-ttl) persists
- min-data-points now scales confidence (was declared, printed, settable, unread)
- AI loop registers a prediction per turn and threads its id; predict_task
returns predictionId; durations exclude chat-lock queue wait
- status/dashboard say "n/a" instead of "N/A%", and carry paired count, mean
error, turns error, pending and unpaired counts
- test: modules/test/test-predictor-accuracy.js (26 assertions)
Live on main.html v4.6.56: fresh session -> "Accuracy: n/a (0/0 paired)";
one paired run -> "100% (1/1 paired) - mean error 5% - turns +-0.0".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
was worth
Lifetime counts ranked an 8-day-old outage above today's live crash, and nothing asserted that a
shipped fix actually held: a regression of a fixed bug looked exactly like a new bug.
- api/debug/ingest.php: norm_key() stores `norm` (host/port/scheme/`?v=`/`//` stripped) — the client
hash covers the raw URL, so one outage became two signatures (`/ollama//api/tags` 18 203x and
`/ollama/api/tags` 16 540x). PHP + Python implementations verified byte-identical on 5 real
messages.
- scripts/newos4-ledger.py: data/maintenance/fixes.json maps norm -> {commit, fixed_at, note}, and
verify() returns REGRESSED / WATCHING / CLOSED / UNSEEN. "Seen after the fix" alone is not a
regression (a fix shipped 10d ago with one sighting 9d ago has been silent since) — regressed means
seen after the fix AND recent; otherwise the silence wins.
- scripts/newos4-triage.py: clusters (grouped by norm, recency-ordered), active<24h / stale>7d counts,
FIX| verdict lines; digest gains clusters + fixes and active/stale/cluster/regressed counters.
- api/debug/dashboard.php: Clusters / Active<24h / Stale>7d / Regressed columns and a red REGRESSED
detail line, so a regression is visible without reading JSON.
Seeded with the two fixes whose signatures exist: 2855c63 (pipe crash, WATCHING 69.3h silent) and
563a9be (emptyTrash, UNSEEN). Verdict logic proven on fixtures: recurrence 5m after fix -> REGRESSED,
silent 9d -> CLOSED, last seen 2d -> REGRESSED, fixed 5m ago/last seen 3d ago -> WATCHING.
Loop verified end to end: triage via newos4-maintenance.sh prints ACTIVE/CLUSTER/FIX lines,
maintain OK, skill audit 7/7, dashboard ALL VIEWPORTS OK.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
dashboard (v4.6.55)
Root cause: splitPipe() splits on every '|', so `help |`, `| wc -l`, `a || b` produced a stage with
no argv and executePipeChain() called parts[0].toLowerCase() on undefined — 989 occurrences, first
filed 2026-10-20, unfixable from the report because bug reports dropped the stack that named
executePipeChain:3955. Same unguarded read existed in _classify().
- terminal.js: _classify() returns an inert classification for empty argv; executePipeChain()
parses each stage once, filters stages with no argv (so the last real stage stays final), warns
only when the whole pipeline is empty. Last-stage test now compares against the filtered length.
- module-system.js: `bug` diagnostics include the stack (10 frames, 1200 chars); auto-filed reports
carry it too — they are the majority of the inbox and were previously message-only.
- report.php: per-stack 2 KB cap; fingerprint pinned to message/source/count so adding `stack`
cannot break dedupe against reports filed before today.
- ingest.php: first_frame() parsed once on ingest, stored as `frame` on each signature.
- newos4-triage.py: FRAME| line with the first application frame.
- dashboard.php: frame under each Top Errors row.
- modules/test/test-pipe-guard.js: ordering tripwire (guard must precede the parts[0] read).
Verified: test RED exit=1 -> GREEN exit=0; browser matrix on v4.6.55 (help |, | wc -l,
echo hi || echo bye, ls | | wc -l, |) -> 0 new error signatures, "|" warns;
auto-filed report now carries stack (RPT-...f3e31124, archived after the probe);
frame = modules/core/core-bundle.js:3955:42; maintain OK; skill audit 7/7.
|
| | |
|
| |
|
|
|
|
|
|
| |
2026-10-18
Report RPT-20261018-164646-89352963 ("this.fs.emptyTrash is not a function") was filed by the auto-filer and never fixed: terminal.js:1777 called a method VirtualFS never defined, so 'empty-trash --confirm' threw instead of emptying. Added emptyTrash() returning true only when it removed something.
Verified live on v4.6.54: put -> rm -> empty-trash --confirm => 'Trash emptied.'; second run => 'Trash is already empty.'; .trash entries 0.
|