<feed xmlns='http://www.w3.org/2005/Atom'>
<title>newos4/modules/core/ollama-manager.js, branch main</title>
<subtitle>newOS4 - the browser terminal that runs WebX11 user spaces</subtitle>
<id>https://newos.webx11.duckdns.org/newos4/atom?h=main</id>
<link rel='self' href='https://newos.webx11.duckdns.org/newos4/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/'/>
<updated>2026-11-02T21:18:09+00:00</updated>
<entry>
<title>refactor(dashboard): one dashboard — port billing/API keys, 301 the duplicate</title>
<updated>2026-11-02T21:18:09+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-02T21:18:09+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=e081d986ef0562cb9983fd1d8ff189bc0c13c975'/>
<id>urn:sha1:e081d986ef0562cb9983fd1d8ff189bc0c13c975</id>
<content type='text'>
Two dashboards existed and had drifted apart: root /dashboard.html (118 KB single file with
Subscription &amp; Usage + API Keys) and /spaces/dashboard.html (34 KB shell + assets/js/dashboard-*.js).
The nginx log settles which one is real: 77.211.5.129 requests /spaces/dashboard.html and never
the root page. Tonight's fixes landed on the root page twice before that was noticed.

Neither page was a superset, so deleting outright would have lost features:
  root only      -&gt; Subscription &amp; Usage card, API Keys card (create/revoke/reveal)
  spaces only    -&gt; 2FA/TOTP, change password, delete space, email change, files, logs

assets/js/dashboard-billing.js + assets/css/dashboard-billing.css (new)
  loadSubscription / loadApiKeys / cancelSubscription / createApiKey / revokeApiKey /
  copyNewApiKey / dismissNewKey + isProPlusPlan / capPlan, lifted verbatim from the root page,
  wrapped in an IIFE that re-exports the onclick handlers on window; getAuthHeaders() reads the
  modular app's localStorage.webx11_session_id; PRO_PLUS_PLANS carried over (its absence threw
  "PRO_PLUS_PLANS is not defined" and silently hid both cards on the first attempt)

spaces/dashboard.html
  the two cards inserted after the Activity Logs card, plus the css/js includes
  dashboard-auth.js showDashboard() calls loadSubscription() once the view is up

retirement
  root dashboard.html deleted (git rm); `location = /dashboard.html { return 301
  /spaces/dashboard.html$is_args$args; }` so bookmarks keep working and ?next= survives
  links repointed in ai-api.html (6), ai-course.html (3), models.html (1), newOS4 hint string
  (v4.6.146 + rebuilt bundles so the cached bundle cannot serve the old URL)

Measured (playwright, 390x844 WebView UA, agando's real session):
  subscriptionCard display block, height 315px, plan "Free", price "EUR0/mo", badge "Free",
  usage "$0.00 / inf", API-keys card hidden for a free plan, 0 console warnings
  /dashboard.html?next=/skills.html -&gt; 301 -&gt; /spaces/dashboard.html?next=/skills.html, 200,
  login card + Telegram button present
</content>
</entry>
<entry>
<title>fix(auth): one session writer for every login path; expire dead credentials</title>
<updated>2026-11-02T20:51:31+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-02T20:51:31+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=6ebfaf0323259d07ccb485af82fac70a7b42fdf6'/>
<id>urn:sha1:6ebfaf0323259d07ccb485af82fac70a7b42fdf6</id>
<content type='text'>
Third occurrence of the same class of bug, and the first fix is why: commit e95b7be
(2026-11-01, "multi-session .newsession v3") touched exactly ONE hunk of
auth-file-api.py — handle_login. handle_device_poll, handle_telegram_login and
handle_register kept overwriting the whole .newsession with a single-session v2
payload, so a phone sign-in logged the desktop out and the loser held a session id
no store knew: permanent `401 Invalid session or API key` (newOS4 asked for a login
it could never complete). Measured: agando's store held 1 entry (sha8 45cb7819) while
the phone sent 9ac607f1 for 3 h; 26 device-polls in 2 s from one page minted 26
cookies and only the last write survived, which also drained the shared nginx
`webx11` zone (11x 503 on /admin/api/*).

api/webx11_auth.py
  - add_session() is now THE writer: v3 `sessions[]` (cap 5) under a per-user
    `flock` (LOCK_DIR outside the spaces), plus the legacy `.session` mirror
  - module-level import so no handler has to reach for it lazily

api/auth-file-api.py
  - device-poll / telegram-login / register / recover-by-backup-code all call
    add_session(); a bare write_newsession_file() is now only the admin-gated
    handle_create_session
  - handle_device_poll runs its approved branch under a per-code `flock` and mints
    exactly one session per approved code (idempotent under a poll storm), with a
    cheap pre-lock reject so unvalidated codes cannot mint lock files
  - handle_me_cookie expires an unknown session cookie (Set-Cookie Max-Age=0,
    code SESSION_DEAD): the cookie is HttpOnly, so no page could ever clear a dead
    credential and every load re-sent it

api/newcrypto.py
  - write_newsession_file() is atomic (tmp + fsync + os.replace); the temp name
    carries pid+tid because a fixed `&lt;file&gt;.tmp` lets two concurrent writers of one
    store steal each other's temp file (reproduced: FileNotFoundError)

admin-login.html      single-flight poll (polling/finished guards) — 26 calls -&gt; 1
dashboard.html        device-code fallback when the Telegram popup has no opener
spaces/newos4/*       401 -&gt; session-repair notice + endpoint normalisation, v4.6.144
nginx                 /api/auth/ + /api/security/ on zone wx11auth (120r/m, burst 60)
admin/scripts/verify-session-writers.py  NEW static guard, wired into smoke-auth.sh

Proof (live, after restart): 5 concurrent polls on one approved code -&gt; 5x approved,
1 distinct session id, store v3 with 2 entries, pre-fix session still valid; dead
cookie -&gt; 401 + Set-Cookie expiry, valid cookie -&gt; 200; 200-call auth burst -&gt; 503s
only on /api/auth/, /admin/ stayed 302; smoke-auth.sh PASS. Guard RED (3 offenders)
-&gt; GREEN. Backups: /home/darkbot/backups/authfix-20261102_214424
</content>
</entry>
<entry>
<title>feat(newos4): configurable tool budgets, per-model defaults, real tool telemetry</title>
<updated>2026-11-02T19:46:18+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-02T19:46:18+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=44e390f3e08acb3ad13daea0dbf365424eeb7d95'/>
<id>urn:sha1:44e390f3e08acb3ad13daea0dbf365424eeb7d95</id>
<content type='text'>
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 -&gt; loop profile -&gt; model profile -&gt;
  ai-config -&gt; 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 -&gt; 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 -&gt;
500 chars with truncated true; ai-config write+restore probe 6/6.
</content>
</entry>
<entry>
<title>fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read as one line</title>
<updated>2026-10-27T16:08:42+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-10-27T16:08:42+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=d2588d521b304d0141cfba1f5c8dd4ec50492097'/>
<id>urn:sha1:d2588d521b304d0141cfba1f5c8dd4ec50492097</id>
<content type='text'>
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 -&gt; 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 -&gt; 3 lines
  ("connecting on first use…", "Connecting to dead…", "Auto-connect failed — …"); manual
  connect() -&gt; 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is
  conditional, not blanket.
</content>
</entry>
<entry>
<title>fix(newos4): name the real Ollama failure, bound auto-connect, fix the review findings</title>
<updated>2026-10-27T13:41:20+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-10-27T13:41:20+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=0ccd2cbc4cc15e0a362b08d76a8bd814abfb9ed9'/>
<id>urn:sha1:0ccd2cbc4cc15e0a362b08d76a8bd814abfb9ed9</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>fix(newos4): connect to Ollama on first use, honest write_file, list_files sees /sys</title>
<updated>2026-10-27T13:13:35+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-10-27T13:13:35+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=ea3acceac673b3cb6d84d38f968f057082d8ccb8'/>
<id>urn:sha1:ea3acceac673b3cb6d84d38f968f057082d8ccb8</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>auto: 2026-07-09_06:00 changes</title>
<updated>2026-07-09T04:00:32+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-07-09T04:00:32+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82'/>
<id>urn:sha1:cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82</id>
<content type='text'>
</content>
</entry>
<entry>
<title>auto: 2026-06-24_06:01 changes</title>
<updated>2026-06-24T04:01:05+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-06-24T04:01:05+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=b08aa5b479395441462effed59ff1ee02d9d8c7f'/>
<id>urn:sha1:b08aa5b479395441462effed59ff1ee02d9d8c7f</id>
<content type='text'>
</content>
</entry>
<entry>
<title>auto: 2026-06-18_06:00 changes</title>
<updated>2026-06-18T04:00:40+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-06-18T04:00:40+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=fba6df22dae297c89fa0df065628dddded66c69c'/>
<id>urn:sha1:fba6df22dae297c89fa0df065628dddded66c69c</id>
<content type='text'>
</content>
</entry>
<entry>
<title>auto: 2026-06-17_06:01 changes</title>
<updated>2026-06-17T04:01:20+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-06-17T04:01:20+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=cebacc27a48168e666679018f8e27156d12d4527'/>
<id>urn:sha1:cebacc27a48168e666679018f8e27156d12d4527</id>
<content type='text'>
</content>
</entry>
</feed>
