aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-aiassist.js
Commit message (Collapse)AuthorAgeFilesLines
* release: v4.6.151 — wget that actually saves, web_fetch that actually pagesHEADv4.6.151mainAgando9 hours1-11/+51
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): read_file pages were silently truncated; delegation no longer ↵Agando4 days1-5/+39
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): memory-graph loads stop rewriting user data; unknown sizes say ↵Agando4 days1-2/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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'.
* perf(newos4): delegated agent runs no longer serialise; a waiter can't steal ↵Agando4 days1-10/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): chat() threw on every tool-using turn and never returned its answerAgando4 days1-1/+8
| | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(newos4): configurable tool budgets, per-model defaults, real tool telemetryAgando4 days1-19/+132
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read ↵Agando10 days1-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): the loop guard reads the config it documentsAgando10 days1-6/+24
| | | | | | | | | | | 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.
* fix(newos4): agent registration is absolute-path and honest about what it storedAgando10 days1-4/+13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): help names the owner of an undocumented command; loop lessons ↵Agando10 days1-16/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): bound one turn's tool context, honest skill-create, quarantine ↵Agando10 days1-2/+42
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): name the real Ollama failure, bound auto-connect, fix the ↵Agando10 days1-4/+8
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): connect to Ollama on first use, honest write_file, list_files ↵Agando10 days1-35/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(newos4): context window warns at 80% and says what trimming dropped ↵Agando2026-10-231-3/+26
| | | | | | | | | | | | | | | | | | (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 ..."]'.
* fix(newos4): world_model_query default path parses again (v4.6.83-84)Agando2026-10-231-1/+4
| | | | | | | | | | | | | _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.
* fix(chat): three concurrency holes found reviewing my own patch (v4.6.77)Agando2026-10-231-2/+3
| | | | | | | | | | | | | | 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...'}.
* fix(newos4): agent read gets its own chat cursor (v4.6.76)Agando2026-10-231-5/+15
| | | | | | | | | | | | | | 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.
* feat(newos4): hermes command, auto-poll, chat_hermes tool, token reset (v4.6.73)Agando2026-10-231-0/+49
| | | | | | | | | | | | | | | | | | | 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).
* fix(newos4): tokenised body-aware skill search for load_skills (v4.6.62)Agando2026-10-231-7/+76
| | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): one error counter for errors/dashboard/world-model (v4.6.61)Agando2026-10-231-4/+6
| | | | | | | | | | | | | | | | | | | | | 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).
* fix(newos4): predictor arithmetic, deployed-source viewer, ls text output ↵Agando2026-10-231-4/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (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).
* fix(newos4): measure predictor accuracy over paired predictions (v4.6.56)Agando2026-10-231-12/+35
| | | | | | | | | | | | | | | | | | | | | | | | | 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".
* fix(newos4): dashboard shows the real world model and stops dying after 10 s ↵Agando2026-10-221-0/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | (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.
* feat(newos4): world-model-ask answers with the LLM, grounded in the model ↵Agando2026-10-221-5/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | (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.
* fix(newos4): coreutils flags + loop-guard arg key, both found by the agent's ↵Agando2026-10-221-5/+12
| | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): GLM review follow-ups for AI read/context (v4.6.47)Agando2026-10-211-23/+70
| | | | | | | | | | | | | - 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
* feat(newos4): AI paged file reads, durable tool traces, 12k context budget, ↵Agando2026-10-211-24/+82
| | | | | | | | | | 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
* newOS4 v4.6.34: running tool ledger in the prompt (D4)Agando2026-10-181-0/+16
|
* newOS4 v4.6.33: progress-based turn budget (D3)Agando2026-10-181-0/+22
|
* newOS4 v4.6.31: tool-result memo cache with epoch invalidation (D2)Agando2026-10-181-1/+35
|
* newOS4 v4.6.30: per-agent session isolation (D1)Agando2026-10-181-5/+15
|
* newOS4 v4.6.29: progress-aware loop guard + agent attributionAgando2026-10-181-21/+91
|
* fix: sequential safe-tool execution — parallel fan-out corrupted ↵Agando2026-09-291-4/+7
| | | | print-capture attribution + shadow diff pairing
* newOS4 v4.6.15: reentrant print-capture stack, AI tail -f block, read-only ↵Agando2026-09-291-17/+16
| | | | sed in safeRead, security test harness (21 checks)
* fix: regex captures full command name + suggestion in unknown-cmd tool errorAgando2026-09-291-1/+1
|
* fix: parse per-call captured output for unknown-cmd error (race-free under ↵Agando2026-09-291-4/+4
| | | | Promise.all)
* newOS4 v4.6.14: race-safe AI loop guard (depth counter), cover _capture* ↵Agando2026-09-291-6/+15
| | | | entry points, structured JSON tool error
* newOS4 v4.6.13: typo suggestion bar (one-tap fix), consistent unknown-cmd ↵Agando2026-09-291-1/+4
| | | | messages, AI loop guard
* auto: 2026-07-09_06:00 changesAgando2026-07-091-26/+908
|
* auto: 2026-07-02_06:00 changesAgando2026-07-021-15/+318
|
* auto: 2026-06-25_06:00 changesAgando2026-06-251-22/+502
|
* auto: 2026-06-24_06:01 changesAgando2026-06-241-1/+26
|
* auto: 2026-06-23_18:00 changesAgando2026-06-231-7/+14
|
* auto: 2026-06-23_06:00 changesAgando2026-06-231-0/+2
|
* auto: 2026-06-22_18:00 changesAgando2026-06-221-2/+7
|
* auto: 2026-06-18_18:00 changesAgando2026-06-181-13/+33
|
* auto: 2026-06-18_06:00 changesAgando2026-06-181-5/+26
|
* auto: 2026-06-17_06:01 changesAgando2026-06-171-7/+189
|
* auto: 2026-06-16_06:00 changesAgando2026-06-161-1/+6
|
* auto: 2026-06-13_18:00 changesAgando2026-06-131-9/+115
|