| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.161)
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/<user>/_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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.159)
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 -> 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
<script> 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 <!-- autonomy: review --> 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 -> "12345" and object value -> JSON, and the boot
log now carries 0 SafeRegister warnings (52 messages, 0 JS errors) with SimpleGUIModule
registered.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Each item is symptom -> cause -> 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 (<reason>)"). 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 -> 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
S1 (reported by agent-os4-maint-01, wrongly refuted by the chat auto-reply):
wget existed but was unusable and dishonest.
- Cause 1: _cmdWget used the page's own fetch(url), so every cross-origin URL
died on CORS ("Failed to fetch"). Measured in the served v4.6.150 page:
fetch(cgit README) threw, /api/fetch returned 200/1954 bytes.
- Cause 2: _vfsWriteBytes ignored writeFile()'s boolean, so the command printed
a green tick when the target directory did not exist. Measured:
fs.writeFile('nosuchdir/x.txt') === false.
- Cause 3: bytes were round-tripped through String.fromCharCode, mangling UTF-8.
- Fix: fetch through /api/fetch (raw source), mkdir -p, writeFile + read-back
verification, error instead of a tick on failure. Register curl with
-s/-o parsing delegating to the same writer so the two cannot drift.
S2 (same reporter): web_fetch could not page and said it could.
- Cause: the handler honoured max_chars up to 50 000, then _clipToolResult kept
toolResultChars-200 = 11 800, so 38 200 chars vanished silently; offset was
not in the schema and was never read, so next_offset pointed at a page the
caller could not request again.
- Fix: offset/next_offset/total_chars/source_bytes in the contract; the page is
built, measured against the clip budget and shrunk until the envelope fits,
so next_offset - returned_chars is always 0.
- Also fixed: _clipToolResult appended "0 of N chars withheld" when the content
already fitted, claiming a loss that did not happen.
C1: fetch <url> --save=FILE β the only terminal path from a remote URL to the
VFS, so a 90 KB source page no longer has to be read through 3 000-char
scrollback.
Proxy: /api/extract now returns source_bytes (raw page size) and
content_chars_utf16. Python len() counts code points while JS String.length
counts UTF-16 units; 7 astral characters in the changelog left a 7-char gap at
the end of a 90 KB page (measured). _html_ok declares charset=utf-8.
Verified: node --check on all four modules; py_compile; bundles rebuilt;
served bytes identical to disk for all five assets at ?v=v4.6.151; live in the
browser: curl -s -o review/main.md <cross-origin> saved 1954 chars with the
correct head, a 404 wrote no file, fetch --save wrote 91 518 chars,
web_fetch p1 returned 11 754 with next_offset 11 754 (gap 0), page 2 started at
a different offset with next 23 496.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
invents "completed"
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 -> 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
unknown
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 -> 'β').
maintain 26/26, live v4.6.141: _effectiveWeight 0.2 with the edge object unchanged,
_formatSize(null) 'β', '0 B' still '0 B'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
a lock
Closes the second half of STEER-20261102-201045-ab08.
- chat() gated the main-turn lock on nothing, so orchestrate-parallel's fan-out
(each sub-task calling chat() on the same instance with its own agentTag) ran
strictly one at a time. Measured 2x300ms delegated runs: 617ms before, 306ms
now; the two 200ms user turns still take 400ms (main path stays serialised).
- the 10s lock timeout called this._chatLock = false and printed "force
released", which cleared a lock the waiter did not own - that released the
main turn's lock while it was still running. A waiter now throws and the owner
is the only writer.
- delegated runs no longer clear the user's cancel flag on entry (an agent start
used to reset this._cancelled, disarming a Stop the user had pressed).
- tool-log entries carry the run they belong to and _recentToolLog(tag, n) is
what feeds the run log line and _postRunReview, so concurrent runs no longer
consume each other's entries.
Known limit, unchanged: _currentTag is a single field, so under true concurrency
an entry can be attributed to the wrong run (telemetry only - the answers are
per-session). Namespacing it properly needs per-call state threading.
Test: modules/test/test-chat-concurrency.js, 7 assertions, RED before
(H1 617ms, H3 force-released, H5 no tags) and GREEN now; maintain 24/24.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regression from 32608bc, caught by the .chat() caller sweep before it aged:
- `recent` was referenced in chat()'s finally block but never declared, so any
turn with toolTurns > 0 threw ReferenceError there and chat() REJECTED.
delegateTask's catch turned that into "agent failed"; the 'completed'
fallback was only ever reachable on tool-free turns.
- chat() still had no return at all, so a tool-free turn resolved undefined and
module-orchestrator.js:1250 fell through to `|| 'completed'`.
Both fixed: `const recent` is now built from _toolCallLog, and chat() returns
{ content: finalAnswer, toolTurns } before the archive block (a return at the
end of the method would let _archiveSession swallow the answer).
Verified with /tmp/probe-chat-hotfix.js, 6/6: tool-using turn resolves
{content:'WORKER ANSWER', toolTurns:2}, tool-free turn resolves the answer,
the orchestrator's own projection returns the answer instead of 'completed',
and _chatLock is released. Run against 32608bc the same probe says
"recent is not defined" - i.e. it reproduces the shipped defect.
Not included: the _chatLock serialisation (orchestrate-parallel still runs
sub-tasks one at a time and the 10s force-release can clear the main turn's
lock). That stays open in STEER-20261102-201045-ab08.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema
promised 32000).
Budgets (module-aiassist.js + new modules/core/model-profiles.js):
- one resolver, precedence: call arg -> loop profile -> model profile ->
ai-config -> code default. Keys: read-page-chars, read-result-chars,
tool-context-budget, fetch-chars.
- model classes tiny/small/mid/large keyed by id then by size token; `mid`
reproduces the old numbers so nothing moves for a mid model.
- _clipToolResult now clips the `content` field, never the serialised JSON
envelope: clipping the envelope deleted truncated/next_offset, so a 16k page
lost its continuation pointer (the reported symptom).
- read_file's schema no longer promises a number the model class may not give.
Telemetry:
- _recordToolCall keeps sizes, duration, ok and a 120-char arg preview instead
of the whole payload; ai-stats prints calls / ok% / avg ms / KB / truncation
from localStorage webx11_tool_stats.
Server (api/proxy-api.py):
- /api/extract honours max_chars (default 12000, cap 100000) and returns
content_chars + truncated; the bs4 branch is no longer uncapped while the
regex fallback was silently cut at 10 000.
BEHAVIOUR CHANGE: num_ctx for a model with no explicit setting now follows the
class default (16384 -> 65536 for cloud/large, 8192 for tiny). Cloud models bill
on tokens used, so cost is unchanged; local models get a bigger KV cache.
Two build traps, both now documented in AGENTS.md:
- model-profiles.js must be the FIRST entry in CORE_FILES and must export the
MODEL_PROFILES global (a TDZ typeof throws; without the global every budget
silently falls back to the code default).
- a budget key needs the setter branch AND the constructor loader at
module-aiassist.js:49 - persisting without restoring looks fine until reload.
Verified: test-tool-context-bound 23/23 (RED before: E1-E4 + TypeError on
_toolLimit); live bundle read_page 32000 / result 12000 / budget 48000 /
fetch 12000 and _ctxTokens 65536 for gemma4:31b-cloud; /api/extract 28970 ->
500 chars with truncated true; ai-config write+restore probe 6/6.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
as one line
Two things I had flagged as "worth your attention" and one leftover nit.
- num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so
a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side,
which is exactly the "it forgot what it just read" class of report. Every Ollama request now
carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the
OpenAI-compatible branch is untouched because num_ctx is not one of its parameters.
- Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead
endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail.
- write_file's failure message no longer echoes a trailing slash.
Verification:
- test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored
override is used, junk/too-small values fall back, and the /v1/chat/completions branch does not
receive it. RED on HEAD: TypeError reading 'num_ctx' of undefined. Suite 20/20.
- Live v4.6.117, capturing the real request: {"num_ctx":16384} on /api/generate with
model gemma4:31b-cloud, reply "OK".
- Live v4.6.118, dead endpoint (127.0.0.1:9, fallback disabled): automatic -> 3 lines
("connecting on first useβ¦", "Connecting to deadβ¦", "Auto-connect failed β β¦"); manual
connect() -> 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is
conditional, not blanket.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(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 ..."]'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
_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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(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".
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.51)
Two defects, both verified live before/after:
1. The World Model panel always said "Not synthesized yet". The page had a
renderer for data.worldModel and _collectDashboardData() never set it -- the
model was right there in WorldModelModule._model. Now collected and mapped to
the renderer's exact keys (model has modules.loaded/skills.count/memory.nodes,
the page reads modules/skills/memoryNodes), plus changesSinceLastSnapshot and
the recent-error sample, with an honest "not synthesized" fallback.
2. The page discarded any payload older than 10 s and its own 5 s auto-refresh
then showed "Cannot connect to newOS4". Stale payloads are now rendered with
their age ("Data as of 1:20:14 AM - 3m ago"), labelled amber past 5 min.
Also: the Refresh button wrote a localStorage key nobody read -> the terminal now
listens for it and re-collects (page says "No terminal tab answered" when no
terminal tab is open); storage events repaint instantly; the dead /api/dashboard
fetch (404 on all three paths, 5 s stall) is gone; `dashboard --watch` / `--stop`
re-collects every 5 s; header says the data is this browser's instance.
Verified v4.6.51: panel rows "Synthesized 34s ago / v4.6.51 / 46-22 / 136-3 /
1 nodes 0 edges / OK / none recorded"; 130 s-old payload still renders with
"- 3m ago" and no banner (previously "Cannot connect"); storage event moves the
footer 13s -> 0s; watch timer advances the payload, --stop clears it; mobile
360/390 px all OK; 0 console errors.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(v4.6.50)
`world-model-ask` was a 10-keyword lookup: any real question returned
"Unknown query. Try: modules, skills, ...". The documented example
("what changed since yesterday") compared nothing β it restated the current
snapshot.
- _modelDigest(): whitelisted snapshot (measured 942-965 chars on fixtures,
~3.3 KB live) + recent error sample + changesSinceLastSnapshot
- _buildAskPrompt(): "answer only from this JSON, never invent numbers, name the
command that would produce missing data"
- ask(): connects Ollama on demand, answers via chat(), falls back to the facts
path with a visible warning; --facts keeps the instant lookup
- _diffSincePrev(): keeps one previous snapshot in /system/world-model.prev.json
so "what changed" is a real diff (files 136 -> 140)
- world_model_query tool returns the snapshot (digest or detail:full) instead of
ten canned strings; no nested LLM call inside a tool call
- help text now says what it does; node test modules/test/test-world-model-ask.js
(11 assertions, runs the real class)
Verified live v4.6.50: facts "Loaded 46 modules" | LLM "The snapshot does not
contain information regarding the cause of memory growth ... use
world-model-refresh" | diff "files 136 -> 140" | tool snapshot 3,334 bytes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
live run (v4.6.49)
Agent session surfaced two real defects:
1. The A4 prompt told it to use grep -n / sed -n 'N,Mp' / wc -l / head -N β none of those flags
parsed: grep treated '-n' as the pattern (6/6 variants failed), wc read '-l' as a filename,
head looked for a file named '-2', sed had no range parser. Fixed the tools, not just the text:
grep now accepts -n/-i/-c/-v/-l (line numbers were already printed), wc accepts -l/-w/-c,
head accepts -N and -n N, sed accepts Np and N,Mp, tail no longer prints a blank line for a
trailing newline. Prompt re-ranked to the working forms.
2. Loop guard keyed calls by tool name plus the raw arguments field. When arguments was already
an object (parsed upstream), the key became 'run_command:[object Object]', collapsing distinct
batched commands into one signature and producing false "called 3x" warnings. Now the key is
JSON.stringify(arguments) whenever it is not a string (3 sites: sig, _sigR, _sigD).
Verified live v4.6.49: grep -n/-c, wc -l, head -2, sed -n '2,3p', tail -n 1 all correct.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
- loop guard: a mutation unblocks the replay but no longer erases the repeat counter
(identical mutating calls could loop forever)
- tool traces are per agent tag, flushed in a catch as well as on success, ring-buffered
(first 2 + last 10) so verification reads survive
- read_file notes are metadata only (path/range/next_offset); no file bodies, no secrets,
and web_search/web_fetch output is excluded from durable context (injection surface)
- trace is role:'system' (was 'assistant' β the model re-read it as its own answer)
- surrogate-safe pagination, 500 floor / 16000 cap, prompt limits aligned
- prompt now names checks that exist in the in-page terminal (test, grep -n, wc -l)
- compressed summary capped at 4000 chars; denied-destructive tool result carries tool_call_id
|
| |
|
|
|
|
|
|
|
|
| |
read/tool/verify prompt rules (v4.6.46)
- read_file: offset/limit + JSON {total_chars,truncated,next_offset} (was a hard 8000-char cut with no resume)
- tool results now leave a per-turn trace that is persisted into session context and survives compression
- maxContextTokens 2000 -> 12000; compression keeps 8 exchanges and 400 chars/msg
- system prompt: page big files, prefer grep -n / sed -n / head, re-read + verify after every edit
- verified in the live browser: 30k file pages 0->8000->16000, prompt section present, v4.6.46 served
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
| |
print-capture attribution + shadow diff pairing
|
| |
|
|
| |
sed in safeRead, security test harness (21 checks)
|
| | |
|
| |
|
|
| |
Promise.all)
|
| |
|
|
| |
entry points, structured JSON tool error
|
| |
|
|
| |
messages, AI loop guard
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|