@twinsearth/w2m-dsh-plugin 0.8.9 → 0.8.11

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 CHANGED
@@ -4,6 +4,128 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.8.11] — 2026-10-14
8
+
9
+ **The in-process join threw before it could pair — and a missing fingerprint took the fleet key with it.**
10
+
11
+ ### The defect that mattered
12
+
13
+ `startLocalStack` built `identityOptions`, loaded the identity, and then passed
14
+ `identityOptions.identity` to `createAgent` — but nothing ever assigned that property.
15
+ `loadOrCreateIdentity()` returns `{ identity, path, created }` and does not mutate the options it was
16
+ given, so `createAgent` received `undefined` and rejected it by contract:
17
+
18
+ ```
19
+ createAgent: identity.machine_id is required
20
+ ```
21
+
22
+ **The in-process join therefore threw on every start**, after `loadOrCreateIdentity` had already
23
+ created `device.json` and before anything could pair.
24
+
25
+ That single line accounts for the whole reported symptom, and every earlier explanation was reading
26
+ the wreckage:
27
+
28
+ | observed | why |
29
+ |---|---|
30
+ | `device.json` exists, `device_token: null` | the file is created one line above the throw |
31
+ | its mtime never moves across restarts | execution never reaches the pairing path |
32
+ | `w2m_devices` answers `401` | no token was ever obtained |
33
+ | `P2P_ANNOUNCE_FAILED: HTTP 401` | same token, same absence |
34
+
35
+ The evidence that finally settled it was the mtime, exactly as v0.8.9's own note said it would be —
36
+ but read the other way round: not "the write is missing" but "**nothing after the throw runs at all**".
37
+
38
+ ### The second gate: a missing fingerprint discarded a derived key
39
+
40
+ `resolve()` mints a serial number from a hardware fingerprint and the fleet key from the account's
41
+ `userId`. The key does not depend on the fingerprint — but it was only returned *after* the
42
+ fingerprint succeeded:
43
+
44
+ ```js
45
+ const hardware = await fingerprint();
46
+ if (!hardware.ok) return hardware; // accountFields -- and the fleet key -- thrown away
47
+ const sn = buildSn({ accountCode: account.accountCode, ... });
48
+ return { ok: true, sn, ...accountFields, ... };
49
+ ```
50
+
51
+ On the machine this was reported from, `readHardwareValues({ platform: 'win32' })` returns **zero**
52
+ usable values — both `MachineGuid` (via `reg query`) and the SMBIOS UUID (via PowerShell) come back
53
+ empty — so `fingerprint()` always failed and the fleet key was always discarded after being computed.
54
+
55
+ No amount of retrying could fix that, which is why v0.8.9's backoff did not help: the account *was*
56
+ readable, and the key *was* derived. The failure is now reported as what it is (`no_hardware_source`,
57
+ no SN), while `accountFields` still travels, so a caller that only needs to join a fleet is not held
58
+ hostage by a display identifier.
59
+
60
+ ### The join is no longer a one-shot prologue
61
+
62
+ Two changes to the shape, both consequences of the above:
63
+
64
+ - **The mount path is fast.** It spends two derivation attempts (~2 s), not six (~62 s):
65
+ `agent.start()` waits behind it, and any tool that asked to join waited behind that. The patient
66
+ retrying moved into a background loop beside the running agent, where nothing is blocked by it.
67
+ - **A tool call can finish the join.** A tool call is proof the account profile exists — the session
68
+ cannot answer a model request without it — so `w2m_status`, `w2m_devices` and `w2m_run` ask the
69
+ in-process agent to join if it has not already.
70
+
71
+ The rate limit is respected by a **cooldown, not by permanence**: a successful join is memoised for
72
+ the life of the process, while a *failed* one is retried after ten seconds. Memoising the failure
73
+ would have reproduced the defect being fixed — a machine holding a rejected promise for ever, which
74
+ is the "intermittent miss becomes permanent" trap v0.8.7 fell into.
75
+
76
+ ### Reporting
77
+
78
+ A machine that cannot join says so once, at warn, naming the reason — and says it again only if the
79
+ state changes. The previous wording guessed *"the profile may not be loaded yet"* for every
80
+ `ok: true` case, which after sixty seconds of backoff was usually the wrong explanation.
81
+
82
+ ### Verified
83
+
84
+ `node scripts/verify-join.mjs` — **16 checks, all passing.** It drives the join against a stub agent
85
+ (the real `start()` talks to the public relay for the life of the process, which is why nothing
86
+ exercised this path before) and asserts:
87
+
88
+ - the agent is constructed with a real `identity.machine_id` — the assertion that catches the defect
89
+ above, since `undefined` is what was being passed;
90
+ - nothing is paired while the account cannot be read, and no pairing is attempted without a key;
91
+ - the machine joins once the account answers, **and the token reaches `device.json`**;
92
+ - the pairing presents a 43-character derived fleet key and names the identity on disk;
93
+ - a machine holding a token never re-pairs, however many tools ask (`pair_rate_limit: 5`);
94
+ - a fingerprint failure still yields the fleet key, with `no_hardware_source` reported and no SN
95
+ invented.
96
+
97
+ Two of those checks found real problems while being written, which is the point of having them. The
98
+ first draft of the cooldown rate-limited the *keyless* case as well, so the mount attempt failed on a
99
+ cold account and the very next tool call silently did nothing — the reported symptom, reproduced by
100
+ its own fix. And the mount budget, which had been ~62 s since v0.8.9 with `agent.start()` and every
101
+ tool call waiting behind it, is now ~2 s.
102
+
103
+ The script reads and writes a throwaway identity (`DSH_DEVICE_FILE`), so it never touches the real
104
+ `~/.dsh/xclient/device.json`.
105
+
106
+ Measured on the Windows host this was reported from:
107
+
108
+ ```
109
+ late-account join
110
+ ok the agent is constructed
111
+ ok the agent receives a real identity (createAgent requires identity.machine_id)
112
+ ok nothing is paired while the account cannot be read
113
+ ok no pairing is attempted without a fleet key
114
+ ok the machine says it is in no fleet rather than staying silent
115
+ ok w2m_status is registered
116
+ ok the machine joins once the account answers
117
+ ok the token is persisted, not just held in memory
118
+ ok the pairing presents a derived fleet key
119
+ ok the pairing names the identity on disk
120
+ ok a machine holding a token never re-pairs
121
+ fleet key without a hardware fingerprint
122
+ ok no fingerprint means no serial number, and that is reported
123
+ ok no serial number is invented
124
+ ok the fleet key survives the fingerprint failure
125
+ ok the account code travels with it
126
+ all checks passed
127
+ ```
128
+
7
129
  ## [0.8.9] — 2026-10-14
8
130
 
9
131
  **The account answers lazily, and we asked once.**