Skip to content

Publish TensorFold server status on the device heartbeat - #143

Merged
ThinkOffApp merged 1 commit into
mainfrom
tensorfold-status
Oct 14, 2026
Merged

ThinkOffApp merged 1 commit into
mainfrom
tensorfold-status

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

What

The device heartbeat can now carry the live status of each TensorFold server in the model registry, under one new key on the device row: tensorfold. CodeWatch can render it as a card per machine. A new ide-agent-kit tensorfold-status [--json] prints the same object for a person.

It is read with GET /health, GET /metrics and GET /v1/slide/graph only. Never a completion, never a chat request, never /health?reset_peak=1. A token generated during a Living Weights lesson would be a heartbeat interfering with the lesson.

Why

IAK, TensorFold, CodeWatch and GroupMind are our core modules and should work as one system. Today a TensorFold box shows up on the dashboard as a name and a hand-written note in models.json (this Mac's says "TensorFold 0.2.0"; the server actually running is 0.6.0). This puts the server's own live numbers on the card instead.

How to turn it on

Off unless set. On the machine running uik-daemon (or ide-agent-kit intent daemon):

INTENT_TENSORFOLD_REGISTRY=~/ide-agent-kit/config/models.json
INTENT_TENSORFOLD_SCOPE=local   # default: only servers on this machine; `all` also reads remote entries

Then restart the daemon. The startup log gains one line, e.g. uik-daemon: tensorfold -> /Users/.../config/models.json (scope local, every 60s, GET only).

Which registry entries count: "engine": "tensorfold" (always reported, reachable or not), or no engine at all (auto-detected when /metrics has tensorfold: series or /health has TensorFold's shape). Any other engine value is never asked.

What the operator changes on this MacBook (not done here; the live checkout was not touched): add INTENT_TENSORFOLD_REGISTRY to the com.thinkoff.uik-daemon LaunchAgent and restart it. Optionally add "engine": "tensorfold" to the mb-tensorfold-nemotron entry in config/models.json and drop the stale "TensorFold 0.2.0" text from its box; auto-detection already finds it without that edit (measured).

What CodeWatch needs to render

CodeWatch already reads GET /api/v1/intent/{userId} and takes each device's fields flat from devices[name] (and from stale_device_details[name].state). The new key sits there:

"tensorfold": {
  "schema": 1,
  "checked_at": "2026-10-14T09:00:00.000Z",
  "scope": "local",
  "error": null,
  "servers": [{
    "id": "mb-tensorfold-nemotron", "host": "localhost", "port": 8091, "local": true,
    "source": "detected", "reachable": true, "reason": null,
    "model": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-MLX-4bit", "warming": false,
    "version": null, "backend": null, "device": null,
    "memory": { "used_gb": 18.7, "peak_gb": 40.7, "budget_gb": 96.2 },
    "requests": { "running": 0, "waiting": 0 },
    "tokens": { "prompt_total": 0, "generation_total": 0 },
    "tok_s": null, "tok_s_window_s": null, "decode_tok_s": null,
    "draft": { "drafted": 0, "accepted": 0, "acceptance": null },
    "ttft_p50_s": null, "kv_cache_usage": 0,
    "living_weights": { "available": false, "facts": null, "learning": null, "learned": null },
    "metrics": "ok",
    "checked_at": "2026-10-14T09:00:00.000Z"
  }]
}

Rules for the renderer:

  • Any value that could not be read is null, never 0. Render null as "unknown" (or "n/a" for rates and ratios).
  • error non-null means the registry could not be read: show the error, not "no servers".
  • reachable: false means nothing answered; reason says why in plain words.
  • metrics is ok, auth_required, auth_rejected or unavailable. On auth_required the counters are all null and the card should say "metrics: auth required".
  • local: true means the server runs on the machine whose row this is. With scope all, remote entries appear with local: false and their tailnet host.
  • Age: compare tensorfold.checked_at with now. The server merges a PATCH key by key, so if a publisher is turned off its last tensorfold block stays on the row; treat a block older than about 3 minutes as stale.
  • Units: memory in GB (base 10, one decimal, like mem_total_gb); kv_cache_usage and draft.acceptance are 0 to 1; ttft_p50_s and tok_s_window_s in seconds.
  • tok_s is generated tokens of requests that finished between two reads divided by the seconds between them; null on the first read and after a server restart. decode_tok_s is the server's own live decode rate where it reports one (native build).
  • living_weights.available: false on a 404 (served without --slide), null when unreadable. Counts only; fact text never leaves the machine.

Keys

/health is always open. If a server gates /metrics (native build with keys on, no --metrics-open), the reader reports metrics: "auth_required" and sends a key only when the entry is marked "engine": "tensorfold" and names its own keyFile, and only on /metrics and /v1/slide/graph. It never uses the fleet-wide LLM_API_KEY / LLM_API_KEY_FILE (new entryOnly option on resolveEntryToken), never sends a key while auto-detecting, never follows a redirect, and never copies the native server's requests_total{key="..."} labels into the output. The plan mentioned naming an env var for the key; the registry has no such field and its rule is "credentials are paths", so this uses the entry's existing keyFile. A keyEnv field is easy to add if wanted, but it would also have to go into the picker's selection file to stay coherent.

Behaviour on failure

  • Runs on its own timer at the heartbeat interval (60 s max, like the beat). The beat publishes the last snapshot and never waits on a TensorFold socket. The first read triggers one extra publish so the card fills in at once.
  • 3 s timeout per GET, response size capped, Connection: close on every request. Without it, a CLI run that exited with a kept-alive socket left a ConnectionResetError traceback in the 0.6.0 server's log (measured, then fixed and re-measured clean).
  • Never throws into the heartbeat. Unreachable is reachable: false; an unreadable registry is error, warned about at most every 5 minutes.

Also in this PR

  • Registry: new optional engine field (validated short name, lower-cased). Documented in config/models.README.md; config/models.example.json gains a local and a keyed GB10 TensorFold entry.
  • Docs: a short section in README.md, the payload and field meanings in packages/user-intent-kit/README.md, env vars in the uik-daemon header, CHANGELOG.

Known interaction, not changed here

TensorFold's default port is 8080, which is also the default INTENT_MODEL_ENDPOINT of the existing served-model probe, and that probe may send one short generation request to a loopback endpoint to prove what is loaded. On a machine where TensorFold listens on 8080, set INTENT_MODEL_GENERATE=0. Documented in the UIK README; a proper fix (read the served model from TensorFold's /health instead of generating) belongs in its own PR. This MacBook's TensorFold is on 8091, so it is not affected today.

Tests

npm test: 889 pass, 0 fail (864 before, 25 new in test/tensorfold-status.test.mjs). The new file was also run alone 5 times in a row, all green.

  • Fixtures test/fixtures/tensorfold/*-mlx-0.6.0.* and slide-graph-404.json are byte-for-byte GET answers from the live tensorfold 0.6.0 server on 127.0.0.1:8091. Its counters were all zero, so the traffic case edits counter values in that same recording.
  • Cases: live recording end to end, tok/s from deltas and counter reset, acceptance / TTFT p50 / fullest KV pool, 401 /metrics (mirrors the native key gate), key sent only where named, missing key file reported, fleet-wide key never substituted, redirect not followed, closed port, hung socket, thrown fetch, auto-detect / other engine / scope local / recheck, monitor off / unreadable registry / registry edited live, heartbeat PATCH payload, CLI human and --json, CLI binary end to end.
  • Every test server records every request and the suite asserts only those three GETs arrive; a negative control shows the recorder catches a POST. Mutation checks (key sent to /health, fleet-wide key fallback, key during auto-detect, fact text in the output, unread counters as 0, redirect followed, keep-alive) each turn a test red.

Measured live on this MacBook (GET only): ide-agent-kit tensorfold-status against the real config/models.json auto-detects the 8091 server, and uik-daemon pointed at a local fake intent API sent a device PATCH carrying the tensorfold block.

🤖 Generated with Claude Code

IAK now reads the live status of each TensorFold server in the model
registry and puts it on the device card under one `tensorfold` key, so
CodeWatch can show a card per machine. Opt in with
INTENT_TENSORFOLD_REGISTRY=<path to models.json> on uik-daemon or
`ide-agent-kit intent daemon`; INTENT_TENSORFOLD_SCOPE=local (default)
or all.

Read with GET /health, GET /metrics and GET /v1/slide/graph only, never
a completion. Runs on its own timer at the heartbeat interval and the
beat reads the last snapshot, so a dead server costs a stale card and
never a late heartbeat. Unreachable is reachable:false with the reason;
an unreadable registry is an error, never an empty list. tok/s comes
from generation_tokens_total deltas between reads. Living Weights
publishes fact counts only, never fact text. version, backend and device
are read from /health when present and null otherwise.

Registry: `engine` ("tensorfold" marks an entry, any other name skips
it, absent means auto-detect from /metrics or the /health shape). A key
is sent only to an entry marked TensorFold that names its own keyFile,
only on the two gated routes; the fleet-wide LLM_API_KEY fallbacks are
not used for a status read (resolveEntryToken gains entryOnly).

New `ide-agent-kit tensorfold-status [--json]` prints the same object.
Requests carry Connection: close: a CLI run that exited with a kept-alive
socket left a ConnectionResetError traceback in the 0.6.0 server log.

Tests: 25 new, fixtures recorded from the live 0.6.0 server on 8091,
plus 401 /metrics, 404 slide graph, unreachable, hung socket, redirect
and heartbeat payload cases.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T09:19:35.813254Z 905a5ac PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThinkOffApp
ThinkOffApp merged commit 917efb0 into main Oct 14, 2026
2 checks passed
ThinkOffApp pushed a commit that referenced this pull request Oct 14, 2026
Review finding (@codexmb): parseServeLine took /tensorfold-native serve
anywhere in a joined ps line, so `printf /tmp/tensorfold-native serve ...
--host example.invalid --api-key-file /tmp/dummy-key` parsed as an engine;
its --host was kept as the connect address, local:true was forced, and the
key file the argv named was read and sent there as a Bearer token.

Now, fail closed:
- Identity comes from the kernel, not argv: the executable `lsof -d txt`
  reports (macOS; `ps` comm is argv[0] there) or /proc/<pid>/exe (Linux)
  must be tensorfold-native, or Python running `-m tensorfold serve` or an
  absolute .../tensorfold entry script.
- Where to read comes from the pid's own LISTEN sockets (lsof, or
  /proc/<pid>/fd + net/tcp{,6}), loopback or wildcard only; --host is not
  parsed. A --port the pid does not listen on, several ports with no
  --port, non-loopback-only sockets, or no lsof: not read, reason in
  `skipped` (no default-port fallback any more).
- No key from a command line at all: --api-key-file is recorded only as
  "was given", never read. A key comes only from a registry entry marked
  engine tensorfold (as in #143), merged by port; the card says how.
- local is computed from the host again; collectTensorFoldStatus ignores a
  non-loopback discovered address whatever the caller passes.

Tests: @codexmb's repro, an argv[0] that lies (/bin/sleep), a remote --host,
a --port the pid does not hold, end to end with a 0600 dummy key file and
a key-file spy: no read, no Authorization header, only loopback URLs.
Negative control: the previous parser accepts the repro line. 907/907.
Live on this Mac: Python 8091, Studio 54635, bench 8101 all verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThinkOffApp added a commit that referenced this pull request Oct 14, 2026
)

* Discover running TensorFold engines for the device heartbeat

TensorFold Studio starts its own `tensorfold-native serve` on a new random
loopback port every launch, so no registry entry can name it and IAK could
not see it. The TensorFold status reader now also scans this user's
processes for `tensorfold-native serve` / `tensorfold serve` (one `ps -U
<uid> -o pid=,args=` on macOS, /proc on Linux with `comm` checked first,
no sudo), takes the port from --port or, when there is none, from the
listening socket via lsof, and the model and name from the command line.
Engines are merged with the registry by port (the entry keeps its id and
keyFile; an entry without an engine skips auto-detection) and published
as source "discovered". GET-only rules unchanged.

Keys come from files only: a registry keyFile, or the absolute
--api-key-file a discovered engine was started with, read in the
engine's own key-file format and refused when other users can read it.
Process environments and --api-key values on a command line are never
read or kept. TensorFold Studio makes a fresh key per launch, keeps it
in memory and passes it as TENSORFOLD_API_KEY (app/electron/agent/engine/
supervisor.ts), so its engine reports metrics "auth_required", with its
model taken from --name and labelled model_source "args".

On by default when INTENT_TENSORFOLD_REGISTRY is set;
INTENT_TENSORFOLD_DISCOVER=1 runs it without a registry, 0 (or
`tensorfold-status --no-discover`) turns it off. New payload fields:
name, app, model_source, memory.footprint_gb, top-level discovery
(ok / off / unavailable).

Tests: 15 new in test/tensorfold-discovery.test.mjs with ps, lsof and
GET fixtures recorded from the three engines running on this Mac
(Python 0.6.0, native 1.0.10, Studio), decoys, a fake /proc tree and a
keyed engine.

* Discovery: a command line is a claim, not configuration

Review finding (@codexmb): parseServeLine took /tensorfold-native serve
anywhere in a joined ps line, so `printf /tmp/tensorfold-native serve ...
--host example.invalid --api-key-file /tmp/dummy-key` parsed as an engine;
its --host was kept as the connect address, local:true was forced, and the
key file the argv named was read and sent there as a Bearer token.

Now, fail closed:
- Identity comes from the kernel, not argv: the executable `lsof -d txt`
  reports (macOS; `ps` comm is argv[0] there) or /proc/<pid>/exe (Linux)
  must be tensorfold-native, or Python running `-m tensorfold serve` or an
  absolute .../tensorfold entry script.
- Where to read comes from the pid's own LISTEN sockets (lsof, or
  /proc/<pid>/fd + net/tcp{,6}), loopback or wildcard only; --host is not
  parsed. A --port the pid does not listen on, several ports with no
  --port, non-loopback-only sockets, or no lsof: not read, reason in
  `skipped` (no default-port fallback any more).
- No key from a command line at all: --api-key-file is recorded only as
  "was given", never read. A key comes only from a registry entry marked
  engine tensorfold (as in #143), merged by port; the card says how.
- local is computed from the host again; collectTensorFoldStatus ignores a
  non-loopback discovered address whatever the caller passes.

Tests: @codexmb's repro, an argv[0] that lies (/bin/sleep), a remote --host,
a --port the pid does not hold, end to end with a 0600 dummy key file and
a key-file spy: no read, no Authorization header, only loopback URLs.
Negative control: the previous parser accepts the repro line. 907/907.
Live on this Mac: Python 8091, Studio 54635, bench 8101 all verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ThinkOffApp <233579950+ThinkOffApp@users.noreply.github.com>
Co-authored-by: Petrus Pennanen <petrus@example.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant