@twinsearth/w2m-dsh-plugin 0.9.2 → 0.9.4

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,123 @@ 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.9.4] — 2026-10-14
8
+
9
+ **A note that promised a verdict the relay no longer delivers, and the test that had locked the promise in.**
10
+
11
+ ### What v0.9.3 left behind
12
+
13
+ v0.9.3 made the relay skip an anchor nobody declared, so an unanchored dispatch now aggregates normally —
14
+ the live relay returns `state: consistent`, `counts.ok: 1` for a command that used to come back
15
+ `unverifiable`. The plugin, however, still opened every such dispatch with:
16
+
17
+ > this machine's working tree could not be fingerprinted, so peers have no anchor to match and
18
+ > **the aggregate will be unverifiable**
19
+
20
+ which was true when it was written and is now false. The fixed mechanism had been left with a sentence
21
+ telling operators to doubt a verdict that is correct, so the note now says what is actually the case:
22
+
23
+ > this machine's working tree could not be fingerprinted, so no anchor was declared: peers are compared
24
+ > with each other, not against a named tree
25
+
26
+ ### The test had encoded the promise
27
+
28
+ `test/tools.test.mjs` asserted `assert.match(value.note, /unverifiable/)`, so fixing the wording broke
29
+ the suite — and the tempting repair is to put the sentence back and keep the suite green. That would have
30
+ been the wrong way round: **the test was guarding a claim, not a behaviour.** It now asserts the claim:
31
+
32
+ - the note must state that no anchor was declared;
33
+ - it must state what that costs — agreement between peers, not with a named tree;
34
+ - and `assert.doesNotMatch(value.note, /unverifiable/)`, so the stale promise cannot be written back in.
35
+
36
+ The third is the one that earns its place. A positive match on a *new* sentence would have passed too and
37
+ guarded nothing; the negative case is what stops the old sentence returning.
38
+
39
+ ### Verified
40
+
41
+ - `test/relay.test.mjs` — 152 tests, 0 failures.
42
+ - `test/claims.test.mjs` — 47 pass. It caught this release's own omission on the first run: the version
43
+ was bumped to 0.9.4 while the newest CHANGELOG entry still said 0.9.3, and it refused with *"One of them
44
+ is the release; the other is a mistake."* Correct, and the reason this entry exists.
45
+ - `test/tools.test.mjs` — 90 pass, 1 failure: `w2m_run computes base_commit and base_tree from a real git
46
+ work tree`, which fails with `spawn git ENOENT` because **git is not installed on this host**. That is
47
+ the same environmental cause as the 38 `declared suite` failures, not a regression.
48
+ - `eslint .` — 0 errors, 127 warnings.
49
+
50
+ ## [0.9.3] — 2026-10-14
51
+
52
+ **An undeclared anchor was compared against the literal `null`, so every dispatch from a non-git machine was `unverifiable` — including the ones that succeeded.**
53
+
54
+ ### The report, and why it read as a lie
55
+
56
+ A relay was asked what happened to a task, and every answer came back the same way:
57
+
58
+ ```
59
+ status : unverifiable
60
+ reasons: ["base_commit \"a1e7dc042913b96da3ad68… != expected null"]
61
+ notes : ["aggregation refused: Step 1 anchor violation"]
62
+ ```
63
+
64
+ while the machine's own record said `lease_state: done`, `status: ok`, `exit_code: 0`. A reader has no way
65
+ to prefer either half, so a task that ran perfectly was reported as one that could not be trusted — and
66
+ the same verdict came back for **every** dispatch.
67
+
68
+ ### The cause
69
+
70
+ `report.mjs` Step 1 compared the anchor unconditionally:
71
+
72
+ ```js
73
+ if (canon(valueOf(env, 'base_commit')) !== canon(expected.base_commit)) {
74
+ ```
75
+
76
+ `expected.base_commit` comes from `task.base_commit ?? null`, and a dispatcher whose own tree is not a
77
+ git work tree — the ordinary case for the plugin running out of its profile directory — sends `null`. So
78
+ the comparison was *"the machine's real commit **equals null**"*, which no machine with an anchor can
79
+ satisfy. **A machine was marked untrustworthy for reporting its commit truthfully.**
80
+
81
+ That the two anchors disagree is the whole story. `base_tree` six lines below has always read:
82
+
83
+ ```js
84
+ if (expected.base_tree !== null && canon(valueOf(env, 'pre_tree_fingerprint')) !== canon(expected.base_tree)) {
85
+ ```
86
+
87
+ — it tolerates absence. `base_commit` did not. Same kind of statement, opposite handling, and the
88
+ asymmetry is the bug. The protocol cannot even distinguish the two cases: `state.mjs` normalises both an
89
+ absent field and an explicit `null` with `input.base_commit ?? null`, so "no anchor" and "anchor is null"
90
+ arrive identically and must be treated identically.
91
+
92
+ ### The fix
93
+
94
+ `base_commit` is enforced only when the dispatcher declared one, matching `base_tree`. The relaxation is
95
+ **reported, not silent**: an `unanchored: true` field on the aggregate plus a note saying
96
+
97
+ > no base_commit was declared, so the anchor check did not run: machine agreement here is agreement with
98
+ > each other, not with a tree the dispatcher named
99
+
100
+ because *"these machines agreed"* and *"nobody said what they should have agreed on"* must not read the
101
+ same to an operator. `consistent: true` from an unanchored run says the machines matched; it does not say
102
+ they ran the tree you meant.
103
+
104
+ ### Verified
105
+
106
+ - `test/relay.test.mjs` — **152 tests, 0 failures**, including two new ones: the undeclared anchor is
107
+ skipped and reported, and a **declared** anchor mismatch still refuses aggregation. The second exists
108
+ because the first is a relaxation, and a relaxation without a falsifying test is how a check quietly
109
+ disappears.
110
+ - `eslint .` — 0 errors.
111
+ - The rest of the pre-flight — gate inventory, test wiring, workflow wiring, entry points, packed tarball,
112
+ claims, CHANGELOG shape — all green. The one red gate is `declared suite`, whose 38 failures on this host
113
+ are 38 tests that shell out to `git`, which is not installed here.
114
+
115
+ ### Note on where task output lives
116
+
117
+ Investigating this also settled a question that had cost real time: the relay stores **no** task output. A
118
+ task record carries `machine_id`, `lease_state`, `outcome`, `status`, `exit_code`, `transport`, `p2p` and
119
+ `reasons` — and no `stdout`, no `stderr`, not even `stdout_sha256`, despite `COMPARABLE_FIELDS` naming
120
+ those fields. So "sync the output home" cannot be solved at the relay, by any port or proxy: the text was
121
+ never kept. Any durable record of what a command printed has to be written **on the machine that ran it** —
122
+ which is exactly where the fleet-memory work begins.
123
+
7
124
  ## [0.9.2] — 2026-10-14
8
125
 
9
126
  **The gate added in v0.8.11 pointed at a file that had never been committed, so it failed on all four CI platforms.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -180,7 +180,7 @@ const MAX_STAGES = 16;
180
180
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
181
181
  * overrides it at runtime for tests and packagers.
182
182
  */
183
- const PLUGIN_VERSION = '0.9.2';
183
+ const PLUGIN_VERSION = '0.9.4';
184
184
 
185
185
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
186
186
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -3874,8 +3874,21 @@ export async function apply(ctx, config = {}) {
3874
3874
  session: sessionKey,
3875
3875
  ...selectionReason,
3876
3876
  },
3877
+ /**
3878
+ * The note has to match what the relay will actually do.
3879
+ *
3880
+ * It used to read "...and the aggregate will be unverifiable", which was true when the relay
3881
+ * compared an absent anchor against the literal `null`: every machine with a real commit
3882
+ * violated it, so an unanchored dispatch could never aggregate. v0.9.3 made the relay skip a
3883
+ * check nobody declared (`base_commit` now behaves like `base_tree` always did), so the same
3884
+ * dispatch reaches a real verdict -- and the old wording would have told an operator to doubt
3885
+ * an `ok` result that is correct.
3886
+ *
3887
+ * What is still true, and is the thing worth saying: without an anchor there is no answer to
3888
+ * "did these machines run the same tree", so consistency here is agreement between machines.
3889
+ */
3877
3890
  note: anchors.base_tree === null
3878
- ? 'this machine\'s working tree could not be fingerprinted, so peers have no anchor to match and the aggregate will be unverifiable'
3891
+ ? 'this machine\'s working tree could not be fingerprinted, so no anchor was declared: peers are compared with each other, not against a named tree'
3879
3892
  : 'peers are checked against base_commit + base_tree + command_hash',
3880
3893
  },
3881
3894
  null,
@@ -216,7 +216,34 @@ export function aggregate(task, resultRecords = [], opts = {}) {
216
216
  m.reasons.push('envelope status = unverifiable');
217
217
  }
218
218
  if (missing.length > 0) m.reasons.push(`missing required fields: ${missing.join(',')}`);
219
- if (canon(valueOf(env, 'base_commit')) !== canon(expected.base_commit)) {
219
+ /**
220
+ * v0.9.3: `base_commit` is enforced only when the dispatcher declared one.
221
+ *
222
+ * The check used to be unconditional, so a dispatcher with **no** anchor -- one whose own tree is
223
+ * not a git work tree, which is the ordinary case for a machine that runs the plugin from its
224
+ * profile directory -- sent `base_commit: null`, and every result was then compared against the
225
+ * literal `null`. Any machine that *did* have an anchor reported its real commit and was marked
226
+ * `unverifiable` for telling the truth:
227
+ *
228
+ * expected.base_commit : null
229
+ * machine reported : a1e7dc042913b96da3ad68…
230
+ * reasons : ["base_commit \"a1e7dc04…\" != expected null"]
231
+ * notes : ["aggregation refused: Step 1 anchor violation"]
232
+ *
233
+ * which downgraded **every** dispatch from such a machine, including a wholly successful one, and
234
+ * made the verdict unreadable. The anchor exists to answer "did these machines run the same tree";
235
+ * with nothing declared there is no such question, and the honest verdict is that the run is
236
+ * unanchored -- not that it disagreed with `null`.
237
+ *
238
+ * `base_tree` three lines below has always read this way (`expected.base_tree !== null && …`), and
239
+ * the asymmetry was the bug: the two anchors are the same kind of statement and one of them
240
+ * tolerated absence while the other did not.
241
+ *
242
+ * An absent anchor is still *reported* rather than silently ignored -- `unanchored: true` on the
243
+ * aggregate and in the machine's own record -- because "these two machines agreed" and "nobody
244
+ * said what they should have agreed on" must not read the same to an operator.
245
+ */
246
+ if (expected.base_commit !== null && canon(valueOf(env, 'base_commit')) !== canon(expected.base_commit)) {
220
247
  m.reasons.push(`base_commit ${truncate(canon(env.base_commit), 24)} != expected ${truncate(canon(expected.base_commit), 24)}`);
221
248
  }
222
249
  if (expected.base_tree !== null && canon(valueOf(env, 'pre_tree_fingerprint')) !== canon(expected.base_tree)) {
@@ -429,6 +456,18 @@ export function aggregate(task, resultRecords = [], opts = {}) {
429
456
  created_by: task.created_by ?? null,
430
457
  cancelled: task.cancelled === true,
431
458
  expected,
459
+ /**
460
+ * v0.9.3: whether this task ran against a declared tree at all.
461
+ *
462
+ * `expected.base_commit === null` means the dispatcher declared no anchor -- its own tree is not a
463
+ * git work tree, which is the ordinary case for the plugin running out of a profile directory. The
464
+ * anchor check is therefore skipped rather than compared against the literal `null` (see Step 1).
465
+ *
466
+ * Reported explicitly because the two situations must not look alike: `consistent: true` from an
467
+ * unanchored run says "these machines agreed with each other", **not** "these machines ran the
468
+ * tree you meant". An operator comparing results needs to know which of those they are holding.
469
+ */
470
+ unanchored: expected.base_commit === null,
432
471
  machines: machines.map((m) => ({
433
472
  machine_id: m.machine_id,
434
473
  machine_name: m.machine_name,
@@ -451,7 +490,10 @@ export function aggregate(task, resultRecords = [], opts = {}) {
451
490
  })),
452
491
  steps,
453
492
  differences: step3.differences ?? [],
454
- notes,
493
+ notes: expected.base_commit === null
494
+ ? [...notes, 'no base_commit was declared, so the anchor check did not run: machine agreement here is '
495
+ + 'agreement with each other, not with a tree the dispatcher named']
496
+ : notes,
455
497
  counts: {
456
498
  refused: step0.refused.length,
457
499
  unverifiable: step1.violations.length,