@mmmbuto/nexuscrew 0.8.52-rc.9 → 0.8.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +340 -0
- package/frontend/dist/assets/index-BJ-_5bxw.js +93 -0
- package/frontend/dist/assets/{index-_kotgT8Z.css → index-CYi_lhCg.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +11 -0
- package/frontend/dist/version.json +1 -1
- package/lib/audio/dispatch.js +6 -1
- package/lib/cells/scope-guard.js +239 -0
- package/lib/cells/scope.js +144 -0
- package/lib/cli/commands.js +279 -4
- package/lib/fleet/builtin.js +129 -4
- package/lib/fleet/catalogs/alibaba-token-plan-qwen3.8.json +3 -3
- package/lib/fleet/definitions.js +128 -7
- package/lib/fleet/managed.js +229 -20
- package/lib/fleet/model-probe.js +129 -0
- package/lib/fleet/routes.js +27 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/mcp/server.js +35 -1
- package/lib/mcp/tools.js +67 -4
- package/lib/nodes/commands.js +86 -3
- package/lib/nodes/identity.js +232 -0
- package/lib/nodes/store.js +89 -0
- package/lib/notify/notifier.js +7 -0
- package/lib/notify/routes.js +75 -3
- package/lib/proxy/federation.js +100 -8
- package/lib/server.js +100 -4
- package/lib/settings/pairing-coordinator.js +16 -0
- package/lib/settings/public-peering-routes.js +24 -1
- package/lib/settings/routes.js +1 -1
- package/lib/tmux/actions.js +33 -1
- package/package.json +1 -1
- package/frontend/dist/assets/index-Cefi613r.js +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,346 @@ All notable changes to NexusCrew are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.8.53 — 2026-08-07 — "Coming Back Up, and Saying So"
|
|
8
|
+
|
|
9
|
+
Automatic updates are on by default and check every six hours, so a node
|
|
10
|
+
installs a new release and restarts itself without anyone watching. That makes
|
|
11
|
+
the restart path the most consequential code in the product, and this release
|
|
12
|
+
is about what happens when it doesn't work: today a node was measured staying
|
|
13
|
+
down for over twenty minutes after a restart, with its tunnel still up, while
|
|
14
|
+
nothing anywhere said so.
|
|
15
|
+
|
|
16
|
+
- **A restart now confirms the service came back.** `nexuscrew restart` used to
|
|
17
|
+
report success as soon as the restart *command* returned — which says the
|
|
18
|
+
command ran, not that anything is answering. The two look identical to whoever
|
|
19
|
+
reads the exit code. The check already existed and was already used by two
|
|
20
|
+
other paths: the auto-updater waits for health and fails if it doesn't come,
|
|
21
|
+
and the Fleet bootstrap does the same, with a comment that literally says "a
|
|
22
|
+
verified restart is needed". It was missing precisely on the command a person
|
|
23
|
+
types by hand — the one where no other code is checking on your behalf.
|
|
24
|
+
|
|
25
|
+
- **A service that exited after a restart is brought back, once.** On a phone
|
|
26
|
+
there is no service manager to raise the process again: if it goes, it stays
|
|
27
|
+
gone. That was measured — a node stayed down for over twenty minutes after a
|
|
28
|
+
restart, with its reverse tunnel still up, and nothing brought it back. Now,
|
|
29
|
+
when the service does not answer, the port decides what happens: free means
|
|
30
|
+
the process is gone and it is started again *once*; still busy means something
|
|
31
|
+
is holding it without serving, and retrying would only hide that. Once and no
|
|
32
|
+
more — repeating turns a fault into a loop. On a machine where a service
|
|
33
|
+
manager owns the runtime nothing is started alongside it: that is the
|
|
34
|
+
manager's job, and a process it does not know about would race its own unit
|
|
35
|
+
for the port and outlive a stop.
|
|
36
|
+
|
|
37
|
+
**Why the process exited is not established.** A plausible story — that the
|
|
38
|
+
restart did not wait for the old process before starting the new one — turned
|
|
39
|
+
out to be wrong on inspection: it does wait, and has since 0.8.17. So this is
|
|
40
|
+
a recovery for a failure whose cause is still open, not a fix for a known one,
|
|
41
|
+
and it is worth knowing which of the two you are relying on.
|
|
42
|
+
|
|
43
|
+
- **"Peer unreachable" no longer covers two different failures.** With a reverse
|
|
44
|
+
SSH channel, a device that is not connected leaves no listener and the
|
|
45
|
+
connection is *refused*; a device that is connected but whose NexusCrew has
|
|
46
|
+
died accepts the connection and then *resets* it. Those need opposite
|
|
47
|
+
remedies — one is fixed on the network, the other by going to the device —
|
|
48
|
+
and one message sent the investigation to the wrong place half the time. It
|
|
49
|
+
did: four hours were spent in federation and pairing while the defect was a
|
|
50
|
+
service that had not come back on a phone. The two are now named, with the
|
|
51
|
+
port, and an error nobody recognises is still reported as before rather than
|
|
52
|
+
guessed into a layer.
|
|
53
|
+
|
|
54
|
+
- **The interface reloads itself after the node updates.** With automatic
|
|
55
|
+
updates on, a node updates and restarts while an open app keeps running the
|
|
56
|
+
old bundle. The only way out was closing and reopening it — the banner had to
|
|
57
|
+
be tapped, and because of a service-worker defect fixed in 0.8.52, tapping it
|
|
58
|
+
did not work either. The app now applies the new bundle by itself. Only for
|
|
59
|
+
the case a reload can fix: when the package on the server is newer than the
|
|
60
|
+
interface it serves, no amount of reloading changes that, and it is left
|
|
61
|
+
alone. If the mismatch survives the reload it is not retried — a reload loop
|
|
62
|
+
makes the app unusable, which is far worse than a banner, so the banner
|
|
63
|
+
remains as the fallback. What you were typing is not lost: the composer draft
|
|
64
|
+
was already kept across reloads, which is what made this acceptable. The
|
|
65
|
+
version check now also repeats about once a minute rather than running only
|
|
66
|
+
when the app starts — an app left open in front of someone is precisely the
|
|
67
|
+
case this exists for, and checking only at startup would have made it work
|
|
68
|
+
solely for people who had already closed and reopened it.
|
|
69
|
+
|
|
70
|
+
- **`nexuscrew autoupdate on|off|status`.** The switch already existed — a
|
|
71
|
+
persisted setting, on by default, with a checkbox in Settings. It was missing
|
|
72
|
+
from the command line, which is where you need it: when a node has updated
|
|
73
|
+
itself and the service did not come back, the interface is the thing you
|
|
74
|
+
cannot open. With the service running the command goes through the API rather
|
|
75
|
+
than writing the file, because writing the file would leave the running
|
|
76
|
+
process with the old value — the setting would read "off" while updates kept
|
|
77
|
+
happening on schedule, and a switch that reads off without switching anything
|
|
78
|
+
off is worse than no switch. If the service is up but not answering, nothing
|
|
79
|
+
is written at all.
|
|
80
|
+
|
|
81
|
+
- **An error now says when the tool bridge is older than the hub.** Updating
|
|
82
|
+
NexusCrew does not update the MCP bridge of an already running session: that
|
|
83
|
+
process started with the previous code and keeps it until the session is
|
|
84
|
+
restarted. The symptom is cruel — you install a fix, try again, and get the
|
|
85
|
+
*old* error, so you conclude the fix does not work and go looking where the
|
|
86
|
+
defect is not. The check runs only on the error path, which costs nothing in
|
|
87
|
+
normal use and is the only moment it helps; it cannot be cached at startup
|
|
88
|
+
either, since the version that changes is the hub's, and it changes while the
|
|
89
|
+
bridge is running.
|
|
90
|
+
|
|
91
|
+
- **A long message sent to a cell is no longer pasted and left unsent.** The
|
|
92
|
+
wait between the bracketed paste and the Enter was a constant, while the time
|
|
93
|
+
a terminal interface needs to swallow a paste grows with its size — above a
|
|
94
|
+
certain length the client collapses it, the Enter lands while it is still
|
|
95
|
+
being processed, and it is swallowed. The message then sat in the composer
|
|
96
|
+
while the sender got a delivery receipt. Measured on the same target eleven
|
|
97
|
+
minutes apart: 2900 characters were never processed for nine hours, sixty
|
|
98
|
+
characters were being worked on in twelve seconds. The wait now grows with the
|
|
99
|
+
text, and below 500 characters nothing changes — those were already reliable,
|
|
100
|
+
and slowing them would be a cost paid by everyone for a defect that is not
|
|
101
|
+
theirs. This narrows the window rather than closing it: a receipt still means
|
|
102
|
+
paste and Enter, not acceptance.
|
|
103
|
+
|
|
104
|
+
## 0.8.52 — 2026-08-07 — "What a Peer May See, and What a Cell May Reach"
|
|
105
|
+
|
|
106
|
+
- **Each node now has a cryptographic identity, and it changes nothing yet.**
|
|
107
|
+
Every installation generates an Ed25519 key pair; the private half stays in
|
|
108
|
+
its own file, readable only by its owner, and never leaves the device. Public
|
|
109
|
+
halves are exchanged during pairing — inside the act that consumes the
|
|
110
|
+
one-time invite, which is the only moment at which the operator has decided,
|
|
111
|
+
*on both machines*, that these two nodes know each other. Binding a key
|
|
112
|
+
anywhere else would bind it to a channel the peer controls alone.
|
|
113
|
+
|
|
114
|
+
Nothing is gated on it. A node running an older version sends no key and
|
|
115
|
+
pairs exactly as before; a malformed key is ignored rather than refused,
|
|
116
|
+
because a fault here must never be able to stop you from pairing a device.
|
|
117
|
+
This is the first step of a per-node authority model, and it is deliberately
|
|
118
|
+
the step with no effect: what it buys is *time*. A key bound today has a
|
|
119
|
+
history in six months; one bound when permissions start depending on it has
|
|
120
|
+
none.
|
|
121
|
+
|
|
122
|
+
Pairing is the only thing that writes a key: no other path sets one, so no
|
|
123
|
+
key can be replaced by anything a peer merely asserts. That is the property
|
|
124
|
+
worth having, and it is currently guaranteed by there being no second writer
|
|
125
|
+
rather than by a check — the check belongs with the first path that learns a
|
|
126
|
+
key outside pairing, and arrives with it.
|
|
127
|
+
|
|
128
|
+
**What this does not do yet**: peers paired before this version have no key
|
|
129
|
+
and will not have one until they are paired again, so on an existing
|
|
130
|
+
installation the directory starts out empty. Keys are learned only during
|
|
131
|
+
pairing, and replacing one on a peer that already has it means removing that
|
|
132
|
+
peer and pairing it again.
|
|
133
|
+
|
|
134
|
+
- **A paired node can now be restricted to a subset of your cells.** Pairing
|
|
135
|
+
was all or nothing: a peer saw every cell on the hub and could act on all of
|
|
136
|
+
them. The permission lives in the node store of the hub that owns the cells,
|
|
137
|
+
never in the body of a request, and it is keyed on the cell id — which is
|
|
138
|
+
unique and immutable — rather than the tmux session, which is derived and
|
|
139
|
+
accepts non-canonical overrides. A missing field means `all`: a fail-closed
|
|
140
|
+
default would have silenced an entire fleet on the first upgrade without
|
|
141
|
+
anyone deciding anything, so narrowing stays an explicit act. `selected` with
|
|
142
|
+
an empty list means *no cells*, and it is a different thing from an absent
|
|
143
|
+
field — which is exactly why the mode is its own field instead of being
|
|
144
|
+
inferred from the array.
|
|
145
|
+
|
|
146
|
+
The guard sits at the head of the `/api` router rather than on each route,
|
|
147
|
+
because the channels a cell name leaks through are many and growing: reads
|
|
148
|
+
(`/cells`, `/fleet/status` — the list the remote PWA actually uses,
|
|
149
|
+
`/fleet/definitions`, `/sessions` including the terminal `preview`, `/decks`,
|
|
150
|
+
and the `records` of `/diagnostics/logs`) and actions (fleet up, down,
|
|
151
|
+
restart, engine, boot; `cells/send`; creating and deleting sessions; files)
|
|
152
|
+
pass through the same predicate. Every route declares its target in a table
|
|
153
|
+
instead of having one guessed, and an undeclared route that names a cell is
|
|
154
|
+
refused — a 403 on something legitimate is noticed and fixed, a channel
|
|
155
|
+
nobody sees is not. Defining cells is denied to a restricted peer, or it
|
|
156
|
+
would create the cell it is missing and act on that.
|
|
157
|
+
|
|
158
|
+
The WebSocket attach honours it too, and without that the rest would be
|
|
159
|
+
decoration: `/ws` attaches a PTY *by session name*, so a peer whose cell we
|
|
160
|
+
had hidden from every list could still attach by guessing `cloud-Dev`. An
|
|
161
|
+
out-of-scope session is treated as nonexistent — the same code as one that
|
|
162
|
+
really is not there, because answering "it exists but you may not" reveals
|
|
163
|
+
precisely what the scope hides. The scope is set from the node's sheet in the
|
|
164
|
+
interface and with `nexuscrew nodes cells <node> all|none|Cell1,Cell2`.
|
|
165
|
+
|
|
166
|
+
**This is not the admin/user class.** Within what it can see, a node in scope
|
|
167
|
+
is still trusted as its owner. Scope answers *which cells*, not *what
|
|
168
|
+
authority* — the second is a layer that does not exist yet.
|
|
169
|
+
|
|
170
|
+
- **A peer that is not yours does not inherit a restriction of yours.** Two
|
|
171
|
+
peers of the same hub, which do not know each other, stopped seeing each
|
|
172
|
+
other's cells. On the node that answers, the chain is [A, B, C]: the delivering
|
|
173
|
+
peer B is in its store, the origin A is not — and has no reason to be, since a
|
|
174
|
+
transitive peer arrives through a hub that was authorized to route it.
|
|
175
|
+
Treating the unknown as `none` looked like prudence and was a door shut in the
|
|
176
|
+
face of legitimate traffic. The delivering peer's restriction still applies in
|
|
177
|
+
full, and an origin that *is* in the store still carries its own across
|
|
178
|
+
multiple hops; an unknown *deliverer* stays fail-closed, because that one
|
|
179
|
+
spoke to us and authenticated.
|
|
180
|
+
|
|
181
|
+
- **VL micro-devices are nodes inside NexusCrew.** A bounded bridge carries a
|
|
182
|
+
micro-device's cells to the hub: the node appears in the Settings list and in
|
|
183
|
+
the sidebar, its session is readable in full width, and its commands come from
|
|
184
|
+
the capabilities the device declares rather than from a hardcoded list. The
|
|
185
|
+
hub accepts the `prompt` verb with the same 4 KiB ceiling as the device — two
|
|
186
|
+
different numbers would mean handing the node commands it will refuse while
|
|
187
|
+
returning a `submitted` that reads as "it left". Nodes are aggregated across
|
|
188
|
+
*all* authorized owners, not just the local one: an owner that does not answer
|
|
189
|
+
shows as unavailable instead of silently reading as "no nodes", every node is
|
|
190
|
+
tagged with the owner it came from, and a command is routed to the node's
|
|
191
|
+
actual owner rather than quietly to the local endpoint.
|
|
192
|
+
|
|
193
|
+
What does **not** cross the boundary: the arguments and results of tools.
|
|
194
|
+
They are at once the largest and the most dangerous — files, command output,
|
|
195
|
+
possible secrets — and they do not leave even truncated. On the hub the events
|
|
196
|
+
stay in memory and nothing more, one ring per node, lost on restart: the
|
|
197
|
+
durable copy is the journal on the device, and persisting here would extend
|
|
198
|
+
in time a visibility that today is only live. A gap is always reported with
|
|
199
|
+
its count, because a silent absence reads as "nothing happened".
|
|
200
|
+
|
|
201
|
+
- **Models can be declared in the configuration, without waiting for a
|
|
202
|
+
release.** The model catalogue lived in the package, so a provider publishing
|
|
203
|
+
a new id made it unusable until the next version — four managed profiles have
|
|
204
|
+
`strictModels`, and there an out-of-catalogue id is not a warning, it is a
|
|
205
|
+
cell that does not start. `fleet.json` now accepts `models` alongside
|
|
206
|
+
`engines`, persisted with them: id, the managed profile it applies to, and the
|
|
207
|
+
fields the client expects. The built-in catalogue remains the list of *known*
|
|
208
|
+
models and becomes a default rather than a wall.
|
|
209
|
+
|
|
210
|
+
Before declaring one you can ask whether it works, from the models window:
|
|
211
|
+
the check queries the provider's model list — which costs no tokens and
|
|
212
|
+
answers exactly the question asked — and replies with a closed set of
|
|
213
|
+
outcomes. A provider that does not expose the list gives `unverified`, never
|
|
214
|
+
`unknown-model`: "I don't know" must not read as "it does not exist", or the
|
|
215
|
+
right model would be declared nonexistent. And `unverified` is not `ok`: a
|
|
216
|
+
proof that was not obtained does not authorize saying it works.
|
|
217
|
+
|
|
218
|
+
Declared models travel with the backup — a round trip used to lose them, and
|
|
219
|
+
the engine that used them was then refused on restore — and they are written
|
|
220
|
+
*before* the engines, in the same mutation, because that order is what makes
|
|
221
|
+
the restore succeed. They also follow the engines across the federation:
|
|
222
|
+
`define-engine` was federated and `define-model` was not, so administering a
|
|
223
|
+
paired node stopped halfway, and the models window now acts on the node you
|
|
224
|
+
are *looking at* — testing a remote node's model against your own fleet
|
|
225
|
+
answers a different question than the one asked.
|
|
226
|
+
|
|
227
|
+
- **`qwen3.8-max-preview` became `qwen3.8-max` without stopping the cells that
|
|
228
|
+
used the old name.** The preview was promoted and the id changed; with
|
|
229
|
+
`strictModels` that is not a warning but a cell that will not start, and two
|
|
230
|
+
cells in the live configuration used the old name. The rename goes through an
|
|
231
|
+
alias declared in one place and applied *before* the gate, so the old name
|
|
232
|
+
resolves — and resolves to the new one, which means the configuration
|
|
233
|
+
converges by itself at the first rewrite instead of lagging behind a silent
|
|
234
|
+
compatibility. The alias covers declared renames, not arbitrary ids.
|
|
235
|
+
|
|
236
|
+
- **The same model with and without a tag now gets the same context window.**
|
|
237
|
+
`deepseek-v4-flash:0731` found itself with 180k instead of 1M after upgrading
|
|
238
|
+
to 0.8.51: the context map is keyed without the tag, the direct lookup missed
|
|
239
|
+
it, and the generic 200000 fallback took over — the cell started, worked, and
|
|
240
|
+
had a fifth of the context it should have. The fix is not to truncate at the
|
|
241
|
+
first colon, because some keys have a tag that is part of the model's identity
|
|
242
|
+
(`qwen3.5:397b`, `mistral-large-3:675b`) and a blind normalization would break
|
|
243
|
+
those two to fix this one. The exact key is tried first, the base name only
|
|
244
|
+
after.
|
|
245
|
+
|
|
246
|
+
- **A cell can be given a named subset of MCP servers.** Cells share one
|
|
247
|
+
configuration, so every cell reached every tool the operator had installed,
|
|
248
|
+
and there was no way to say otherwise. A cell may now declare
|
|
249
|
+
`mcp` with the names it is allowed, and an absent field is not an empty one —
|
|
250
|
+
without it nothing changes, which is what every existing cell gets. Only
|
|
251
|
+
*names* travel, in the process arguments:
|
|
252
|
+
the obvious design would generate one file per cell containing the server
|
|
253
|
+
*definitions*, and on a real installation some of those carry credentials in
|
|
254
|
+
their environment, so the secrets would have been duplicated once per cell.
|
|
255
|
+
Granting is done by denying the complement, because a broader deny beats a
|
|
256
|
+
narrower allow in the client; `mcp: []` uses the wildcard and is therefore the
|
|
257
|
+
exact case. The complement is enumerated from the three sources a session
|
|
258
|
+
really loads — the user configuration, the local scope of the cell's working
|
|
259
|
+
directory, and a project `.mcp.json` — and the window says so, since a server
|
|
260
|
+
from a source nobody enumerates would pass while the operator believed it
|
|
261
|
+
excluded.
|
|
262
|
+
|
|
263
|
+
- **A cell isolated by credential no longer loses its tools.** NexusCrew gives
|
|
264
|
+
the Claude client a private configuration directory on certain credential
|
|
265
|
+
profiles, which is right — it separates the keys. But the client keeps the MCP
|
|
266
|
+
list in that same file, and in a private profile that list is empty: measured,
|
|
267
|
+
0 servers against the 8 of the main configuration. A cell there ran with no
|
|
268
|
+
memory, no notifications and no web access, and from outside it just looked
|
|
269
|
+
like a cell that does not use its tools. The private profile now points at the
|
|
270
|
+
main configuration for the server list, so the isolation stays on the keys
|
|
271
|
+
where it belongs.
|
|
272
|
+
|
|
273
|
+
- **A failed cell lookup now says which of the three things went wrong.** One
|
|
274
|
+
message covered every failure — cell absent, node unreachable, id unknown to
|
|
275
|
+
this hub — and it named the cell, so a mistyped node id was read as a missing
|
|
276
|
+
cell and the investigation went where the defect was not. The three cases are
|
|
277
|
+
now distinguished, and the third suggests copying the id from the directory
|
|
278
|
+
rather than retyping it. The same diagnosis was applied to the VL tools.
|
|
279
|
+
|
|
280
|
+
- **An alert to the operator now crosses the federation.** The notification
|
|
281
|
+
channel was born as cell → operator *on the same host*, with three local
|
|
282
|
+
anchors: the bridge speaks only to loopback, the event hub is a set in that
|
|
283
|
+
process's memory, and the push keys are files of that installation. Anyone
|
|
284
|
+
working from another node received nothing, while the reply reported success
|
|
285
|
+
because those counters count *attempts*, not deliveries. A notification now
|
|
286
|
+
accepts an exact target instance; with a remote one the request travels the
|
|
287
|
+
existing federated route and the node that owns the screen delivers with its
|
|
288
|
+
own subscriptions and its own keys — so a real web push, with the app closed.
|
|
289
|
+
|
|
290
|
+
- **A refusal the hub writes down nowhere does not exist.** A node could not
|
|
291
|
+
enable Share and the hub refused it thirty times, leaving no trace: the error
|
|
292
|
+
lived only in the toast on the device, which is the one place the hub's
|
|
293
|
+
administrator cannot look. The cause — a reverse-port grant that named another
|
|
294
|
+
peer's port — surfaced only by reading sshd's log as root. The hub now records
|
|
295
|
+
what it refused and why, with the port it attempted, because "channel not
|
|
296
|
+
ready" does not say where to look. The record lives in the diagnostic buffer
|
|
297
|
+
in memory, so it is readable while the service runs and is lost on restart —
|
|
298
|
+
which is enough for the case it was written for, a refusal repeating now, and
|
|
299
|
+
is not a durable audit trail. Related: `nodes test` with no argument now
|
|
300
|
+
tests every direct peer in parallel and ends with the line that existed
|
|
301
|
+
nowhere — which nodes report themselves as shared while their reverse channel
|
|
302
|
+
does not answer.
|
|
303
|
+
|
|
304
|
+
- **Minting a pairing invite works again on a paired installation.** After
|
|
305
|
+
0.8.51 moved minting to the local installation, the button could not succeed
|
|
306
|
+
on a hub-paired node: the interface still implicitly picked the first outbound
|
|
307
|
+
peer and delegated the minting to it — a path that now answers 404 — and the
|
|
308
|
+
local form disappeared entirely whenever a hub existed, so no route was left.
|
|
309
|
+
An invite is now always minted for the installation that issues it, and the
|
|
310
|
+
panel says which one, which was half the reason the previous behaviour went
|
|
311
|
+
unnoticed.
|
|
312
|
+
|
|
313
|
+
- **The "new version" banner can be dismissed.** The update button messaged a
|
|
314
|
+
waiting service worker and waited for it to take over, but the worker
|
|
315
|
+
registered no message listener: the message fell into nothing, the fallback
|
|
316
|
+
reload fired, the worker stayed waiting, and on reload the banner came back.
|
|
317
|
+
Permanent by construction, with a button that could not turn it off. Anyone
|
|
318
|
+
with a stuck worker needs to do nothing — the new one activates itself.
|
|
319
|
+
|
|
320
|
+
- **Mobile fixes.** The keyboard closed on every letter typed into a node's
|
|
321
|
+
prompt field: the sheet's focus effect was armed on a callback the parents
|
|
322
|
+
recreate on each poll, so every polling round stole the focus back from under
|
|
323
|
+
your thumb. `prompt` is now a field rather than a trigger — the interface
|
|
324
|
+
built its buttons from the declared capabilities and sent every verb with no
|
|
325
|
+
arguments, which the device correctly refused. The models window can be left
|
|
326
|
+
without saving: it had only a save button, and while it also closes with
|
|
327
|
+
Escape or a click outside, a phone has no Escape and the backdrop may be out
|
|
328
|
+
of reach. VL cells no longer appear twice in the switcher, and the VL event
|
|
329
|
+
list is idempotent by sequence number — on a slow link the poll period expired
|
|
330
|
+
before the answer, the next tick started from the old cursor, and identical
|
|
331
|
+
rows piled up.
|
|
332
|
+
|
|
333
|
+
- **Interface language.** Three labels stayed in English whatever the language;
|
|
334
|
+
the cell prompt now explains what it does outside Italian too; two help texts
|
|
335
|
+
promised things about credentials that had stopped being true, and one
|
|
336
|
+
described a backup content that had changed. The end-of-turn label said
|
|
337
|
+
"session ended" on every reply — host, node, cell and pump were all measurably
|
|
338
|
+
alive.
|
|
339
|
+
|
|
340
|
+
- **The governance surface is pinned by name.** The routes that change who may
|
|
341
|
+
do what on a node are unreachable from the federation for an implicit reason:
|
|
342
|
+
they are not in the allowlist. That holds until someone adds a line, and on
|
|
343
|
+
2026-08-04 the opposite had happened. A test now enumerates them and fixes
|
|
344
|
+
them — and declares what it does *not* cover: `/vl-nodes/invite` is federated
|
|
345
|
+
by design, and if that ever changes it should change with someone noticing.
|
|
346
|
+
|
|
7
347
|
## 0.8.51 — 2026-08-04 — "Who May Let Someone In"
|
|
8
348
|
|
|
9
349
|
- **Minting a pairing invite no longer crosses the federation.** An invite is
|