@twinsearth/w2m-dsh-plugin 0.13.1 → 0.15.0

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,100 @@ 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.15.0] — 2026-10-15
8
+
9
+ **The fleet is three machines, not five — and two relay endpoints disagree about it.**
10
+
11
+ ### The measurement, which is the whole point of this release
12
+
13
+ `w2m_stats` and `w2m_devices` both answer "is this machine usable?" and **they do not agree**:
14
+
15
+ | Machine | `w2m_devices` | `w2m_stats` | silent for |
16
+ |---|---|---|---|
17
+ | jp-shared | online | online, RTT **6 ms** | 2.4 s |
18
+ | macmini | online | online, RTT **538 ms** | 4.4 s |
19
+ | ToFuture | online | online, RTT **122 ms** | 0 s |
20
+ | win-desktop ×2 | **online** | **offline, `silent`** | **61 384 s ≈ 17 h** |
21
+
22
+ **A fleet whose size is reported two ways is a fleet whose plans are wrong by a factor of 1.7** — and the
23
+ failure is silent, because both answers look authoritative. `src/agent/fleet-state.mjs` prefers `w2m_stats`
24
+ (it is the one carrying a *reason*), treats a listing without a heartbeat age as unverified, and
25
+ `reconcile()` reports **every machine the two endpoints disagree about** rather than picking one and staying
26
+ quiet.
27
+
28
+ ### The second divergence, which would have flattered the fleet
29
+
30
+ `w2m_history` lists a `lease_states` map per task. **It is not the execution outcome.** Of forty tasks in the
31
+ sample, every one recorded macmini as `done` — while the session's own result envelopes included several
32
+ `expired` with no exit code at all. The map records the *current* lease state.
33
+
34
+ `historyIsOutcomeRecord()` returns `false` unconditionally with the reason, and its suite asserts on the
35
+ measured payload. The finding matters because of its direction: **a reliability measurement built on the
36
+ history listing would have shown a perfectly reliable fleet** — the most dangerous possible error in a module
37
+ whose whole purpose is to notice trouble.
38
+
39
+ The number to plan against is therefore **three usable machines**, and `readStats()` takes it from the
40
+ per-machine entries rather than from `counts.online`, while still reporting the relay's own claim rather than
41
+ hiding it.
42
+
43
+ ### A fixture typo that invented a disagreement
44
+
45
+ Two tests failed because a machine id in a fixture was written `tob` where the payload says `tof` — so the
46
+ reconciler correctly reported a *third* machine claimed online but not usable. **The code was right and the
47
+ fixture was wrong**, which is the third time in this project that a failing test was the test's fault; the
48
+ argument for testing refusals is that they fail loudly enough to be told apart.
49
+
50
+ **16 tests, 0 failures.**
51
+
52
+ ## [0.14.0] — 2026-10-15
53
+
54
+ **Fleet scale: the arithmetic the observations support, and the refusals where they support nothing.**
55
+
56
+ ROADMAP v4.11.0 names scale, defence and integration and deliberately does not plan them, on the grounds that
57
+ **a design that has not been measured at 50 cannot honestly specify 1,000**. This release does not specify
58
+ 1,000. It provides the module that computes what a fleet's own task history can support, and refuses the rest.
59
+
60
+ ### The limit that bounds everything, stated first
61
+
62
+ **This fleet has five machines.** No amount of cleverness turns five into fifty, and the number the ROADMAP
63
+ actually asks about — the agent count at which coordination collapses — is exactly the number this fleet
64
+ cannot observe. So `scaleOf()` returns `can_answer_collapse_point: false` as a **field**, not as a footnote,
65
+ and `extrapolate(5, 50)` refuses with a reason rather than producing a scaled number.
66
+
67
+ The refusal is the feature. A module that emitted a "scaling curve" from five points would be producing
68
+ **fiction with a decimal point on it**, which is worse than silence because it is quotable.
69
+
70
+ ### What the fleet can answer, from `w2m_history` and `w2m_report`
71
+
72
+ | Measure | Why it is the one that matters |
73
+ |---|---|
74
+ | **lease expiry rate** | An expired lease is work that was assigned and **silently did not happen**. This is the coordination failure. |
75
+ | **refusal rate** | Counted separately, because a refusal is *the request* being wrong, not the fleet. Merging them points the reader at the wrong machine. |
76
+ | **transport mix** | Every task carried by the relay while a direct path existed is avoidable latency. |
77
+ | **fan-out vs machines online** | Participation against opportunity. |
78
+
79
+ ### The refusals, which are most of the suite
80
+
81
+ `0/0` is the most misleading number this module could produce — it reads as a perfectly coordinated empty
82
+ fleet. So `expiry_rate`, `refusal_rate`, `direct_share` and `overhead_per_agent` all return **`null`** for an
83
+ empty denominator, and `overhead()` says *"the overhead of zero agents is not zero, it is undefined"*.
84
+
85
+ `ceilingFrom()` states one thing and denies another: *"at a fan-out of N, the lease expiry rate was X"*, with
86
+ `is_a_ceiling: false` and a note that a ceiling would need the rate measured **at several fan-outs and
87
+ trending**. That is the only honest form of the answer available.
88
+
89
+ Three defects found by the suite, all mine:
90
+
91
+ * the boundary rule contradicted its own comment — the test said *"at least a tenth"* while the code required
92
+ *more than* a tenth. The boundary is now exclusive, deliberately: at exactly 10% a refusal costs nothing and
93
+ an estimate costs credibility.
94
+ * a test expected a `null` entry in a machine list to be dropped. It should not be: a machine listed with no
95
+ result **is an unknown outcome**, which is a real thing a relay response says.
96
+ * the same two mistakes, in the same file, in the same run — which is the argument for testing the refusals
97
+ rather than only the arithmetic.
98
+
99
+ **19 tests, 0 failures.**
100
+
7
101
  ## [0.13.1] — 2026-10-15
8
102
 
9
103
  **The macOS skip is removed, because the failure it was hiding did not exist.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
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",
@@ -113,13 +113,13 @@
113
113
  }
114
114
  },
115
115
  "scripts": {
116
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
116
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
117
117
  "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
118
118
  "test:relay": "node --test --test-force-exit test/relay.test.mjs",
119
119
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
120
120
  "test:tools": "node --test --test-force-exit test/tools.test.mjs",
121
121
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
122
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
122
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
123
123
  "pack": "node scripts/pack.mjs",
124
124
  "verify:join": "node scripts/verify-join.mjs",
125
125
  "lint": "eslint .",
@@ -140,7 +140,9 @@
140
140
  "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
141
141
  "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
142
142
  "layer-check": "node scripts/layer-check.mjs",
143
- "test:test-report": "node --test --test-force-exit test/test-report.test.mjs"
143
+ "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
144
+ "test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs",
145
+ "test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs"
144
146
  },
145
147
  "keywords": [
146
148
  "dsh-bundle",
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Fleet scale: what this fleet can measure about itself, and what it cannot.
3
+ *
4
+ * ## What the ROADMAP refuses, and why this module agrees with it
5
+ *
6
+ * ROADMAP §6.4 reports 30–50 agents as the practical limit of current frameworks and describes superlinear
7
+ * "coordination collapse" beyond 100. The roadmap's own conclusion is that **a design that has not been
8
+ * measured at 50 cannot honestly specify 1,000**. This module does not specify 1,000. It computes what the
9
+ * observations support and says where they stop supporting anything.
10
+ *
11
+ * ## The hard limit on this measurement, stated first because it bounds everything below
12
+ *
13
+ * **This fleet has five machines.** No amount of cleverness turns five into fifty, and the number that
14
+ * matters for the ROADMAP's question — the agent count at which coordination collapses — is *exactly* the
15
+ * number this fleet cannot observe. Every figure derived here is a measurement of **five**, and every
16
+ * extrapolation is labelled as one. A module that quietly produced a "scaling curve" from five points would
17
+ * be producing fiction with a decimal point on it.
18
+ *
19
+ * So the honest deliverable is narrower and still useful: the fleet's **own** coordination overhead and
20
+ * failure rates, measured, with the extrapolation performed only where the arithmetic is defensible and
21
+ * refused where it is not.
22
+ *
23
+ * ## What the fleet can actually answer
24
+ *
25
+ * `w2m_history` and `w2m_report` provide, per task: mode, every lease's state, its outcome, its exit code, and
26
+ * the transport it used. That is enough to measure:
27
+ *
28
+ * * **lease expiry rate** — the fraction of leases that never reported. This is the coordination failure
29
+ * that matters, because an expired lease is work that was assigned and silently did not happen.
30
+ * * **refusal rate** — leases the allow-list rejected. Not a failure of the fleet but of the request.
31
+ * * **transport mix** — relay versus direct. Every task carried by the relay while a direct path existed is
32
+ * avoidable latency, and this fleet has never once established a direct path.
33
+ * * **fan-out efficiency** — machines that participated against machines that were online.
34
+ */
35
+
36
+ /** Lease states as the relay reports them, grouped by what they mean for the fleet. */
37
+ export const LEASE_MEANING = Object.freeze({
38
+ done: 'work that happened',
39
+ expired: 'work that was assigned and silently did not happen',
40
+ refused: 'work the allow-list rejected -- a defect in the request, not the fleet',
41
+ cancelled: 'work the operator stopped',
42
+ observing: 'a machine that was told the outcome without being asked to act',
43
+ });
44
+
45
+ /** Outcomes that count as coordination failure, because the work did not happen. */
46
+ export const FAILURE_STATES = Object.freeze(['expired', 'timeout']);
47
+
48
+ /** Outcomes that are not the fleet's fault, so they are counted separately. */
49
+ export const REQUEST_FAULT_STATES = Object.freeze(['refused']);
50
+
51
+ /**
52
+ * Summarise a set of task reports.
53
+ *
54
+ * @param {Array<{task_id?: string, mode?: string, counts?: object, machines?: Array<object>}>} reports
55
+ * @returns {object} a summary whose every number is a count of something observed
56
+ */
57
+ export function summarise(reports) {
58
+ const tasks = Array.isArray(reports) ? reports.filter((r) => r !== null && typeof r === 'object') : [];
59
+ const leases = { done: 0, expired: 0, refused: 0, cancelled: 0, observing: 0, other: 0 };
60
+ const transports = { relay: 0, direct: 0, unknown: 0 };
61
+ const p2p = { attempted: 0, delivered: 0 };
62
+ let tasksWithAnySuccess = 0;
63
+ let tasksWithAnyExpiry = 0;
64
+
65
+ for (const task of tasks) {
66
+ const machines = Array.isArray(task.machines) ? task.machines : [];
67
+ let sawSuccess = false;
68
+ let sawExpiry = false;
69
+ for (const m of machines) {
70
+ const state = String(m?.lease_state ?? m?.outcome ?? 'other');
71
+ if (Object.prototype.hasOwnProperty.call(leases, state)) leases[state] += 1;
72
+ else leases.other += 1;
73
+ if (state === 'done' || m?.outcome === 'ok') sawSuccess = true;
74
+ if (FAILURE_STATES.includes(state)) sawExpiry = true;
75
+ // Transport is only meaningful for a lease that actually ran.
76
+ const t = m?.transport;
77
+ if (t === 'relay') transports.relay += 1;
78
+ else if (t === 'direct') transports.direct += 1;
79
+ else if (t !== undefined && t !== null) transports.unknown += 1;
80
+ const p = m?.p2p;
81
+ if (p !== undefined && p !== null) {
82
+ p2p.attempted += 1;
83
+ if (p.encrypted === true || p.peer !== null && p.peer !== undefined) p2p.delivered += 1;
84
+ }
85
+ }
86
+ if (sawSuccess) tasksWithAnySuccess += 1;
87
+ if (sawExpiry) tasksWithAnyExpiry += 1;
88
+ }
89
+
90
+ const acting = leases.done + leases.expired;
91
+ const attempted = acting + leases.refused;
92
+ return {
93
+ tasks: tasks.length,
94
+ leases: { ...leases },
95
+ leases_total: Object.values(leases).reduce((a, b) => a + b, 0),
96
+ acting_leases: acting,
97
+ expiry_rate: acting === 0 ? null : Math.round((leases.expired / acting) * 1000) / 1000,
98
+ refusal_rate: attempted === 0 ? null : Math.round((leases.refused / attempted) * 1000) / 1000,
99
+ transports: { ...transports },
100
+ direct_share: transports.relay + transports.direct === 0
101
+ ? null
102
+ : Math.round((transports.direct / (transports.relay + transports.direct)) * 1000) / 1000,
103
+ p2p: { ...p2p, delivery_rate: p2p.attempted === 0 ? null : Math.round((p2p.delivered / p2p.attempted) * 1000) / 1000 },
104
+ tasks_with_any_success: tasksWithAnySuccess,
105
+ tasks_with_any_expiry: tasksWithAnyExpiry,
106
+ // A rate over a denominator of zero is reported as null rather than as zero. "No leases were attempted" and
107
+ // "every lease expired" are different facts and a zero would merge them.
108
+ unmeasured: acting === 0 && attempted === 0,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * The fleet's measured coordination overhead per acting agent.
114
+ *
115
+ * Defined against **acting** leases rather than all leases, because an `observing` machine is not doing work
116
+ * and counting it would dilute the number with machines that were merely told what happened.
117
+ *
118
+ * @returns {{acting_leases: number, expired: number, overhead_per_agent: number|null, detail: string}}
119
+ */
120
+ export function overhead(summary) {
121
+ if (summary === null || typeof summary !== 'object' || summary.acting_leases === undefined) {
122
+ return { acting_leases: 0, expired: 0, overhead_per_agent: null, detail: 'no summary supplied' };
123
+ }
124
+ const acting = summary.acting_leases;
125
+ if (acting === 0) {
126
+ return { acting_leases: 0, expired: summary.leases?.expired ?? 0, overhead_per_agent: null, detail: 'no acting leases: the overhead of zero agents is not zero, it is undefined' };
127
+ }
128
+ return {
129
+ acting_leases: acting,
130
+ expired: summary.leases?.expired ?? 0,
131
+ overhead_per_agent: Math.round((summary.leases?.expired ?? 0) / acting * 1000) / 1000,
132
+ detail: `${summary.leases?.expired ?? 0} of ${acting} acting lease(s) never reported`,
133
+ };
134
+ }
135
+
136
+ /** The fleet size actually observed, and the honest statement of what that bounds. */
137
+ export function scaleOf(machineIds) {
138
+ const distinct = [...new Set((Array.isArray(machineIds) ? machineIds : []).filter((x) => typeof x === 'string' && x !== ''))];
139
+ return {
140
+ observed_machines: distinct.length,
141
+ // The ROADMAP's question is about the collapse point, which is beyond anything this can see.
142
+ can_answer_collapse_point: false,
143
+ detail: `this observation covers ${distinct.length} machine(s); the ROADMAP asks about the collapse point near 50, which is outside what ${distinct.length} can show`,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Extrapolate, or refuse to.
149
+ *
150
+ * The rule this encodes: an extrapolation is only offered when the observation covers **more than a tenth**
151
+ * of the target, and even then it is labelled. The boundary is exclusive deliberately — a sample that is
152
+ * *exactly* 10% is the borderline case, and the borderline case is the one where a refusal costs nothing and
153
+ * an estimate costs credibility. Below the boundary the function returns `null` for the estimate with a
154
+ * reason, because the alternative — a number produced by multiplying a small sample — is exactly the fiction
155
+ * the ROADMAP refuses to write.
156
+ *
157
+ * @param {number} observed agents actually measured
158
+ * @param {number} target agents the estimate would be about
159
+ */
160
+ export function extrapolate(observed, target) {
161
+ if (!Number.isFinite(observed) || !Number.isFinite(target) || observed <= 0 || target <= 0) {
162
+ return { ok: false, estimate: null, detail: 'observed and target must both be positive numbers' };
163
+ }
164
+ const coverage = observed / target;
165
+ if (coverage <= 0.1) {
166
+ return {
167
+ ok: false,
168
+ estimate: null,
169
+ coverage: Math.round(coverage * 1000) / 1000,
170
+ detail: `refusing to extrapolate from ${observed} to ${target}: that is ${(coverage * 100).toFixed(2)}% coverage, and a number produced by scaling a sample that small is fiction with a decimal point on it`,
171
+ };
172
+ }
173
+ return {
174
+ ok: true,
175
+ estimate: target,
176
+ coverage: Math.round(coverage * 1000) / 1000,
177
+ detail: `extrapolating from ${observed} to ${target} at ${(coverage * 100).toFixed(0)}% coverage -- reported as an extrapolation, never as a measurement`,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * The one claim this fleet can make about a ceiling, stated as a falsifiable inequality.
183
+ *
184
+ * Not a predicted collapse point. It is: **at the largest fan-out this fleet has actually exercised, the
185
+ * expiry rate was X**. That is measurable, it is falsifiable by re-running, and it is the only form of the
186
+ * answer the data supports.
187
+ */
188
+ export function ceilingFrom(summary, observedFanOut) {
189
+ if (summary === null || typeof summary !== 'object' || !Number.isFinite(observedFanOut) || observedFanOut <= 0) {
190
+ return { claim: null, detail: 'a ceiling claim needs a summary and the fan-out it was measured at' };
191
+ }
192
+ const rate = summary.expiry_rate;
193
+ if (rate === null) {
194
+ return { claim: null, detail: 'no acting leases, so no expiry rate to state' };
195
+ }
196
+ return {
197
+ claim: `at a fan-out of ${observedFanOut}, the lease expiry rate was ${(rate * 100).toFixed(1)}%`,
198
+ fan_out: observedFanOut,
199
+ expiry_rate: rate,
200
+ // Explicitly not a ceiling. Naming what it is not is the point of the function.
201
+ is_a_ceiling: false,
202
+ detail: 'this is an observation at one fan-out, not a collapse point; a ceiling would need the rate measured at several fan-outs and trending',
203
+ };
204
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Fleet state: what the relay's own accounting does and does not say, and the one place it disagrees with itself.
3
+ *
4
+ * ## The disagreement, measured
5
+ *
6
+ * Two relay endpoints answer "is this machine usable?" and they do not agree:
7
+ *
8
+ * * `w2m_devices` reports every paired machine as `state: "online"` -- including two that had been silent
9
+ * for **61,384 seconds (about 17 hours)** at the time of measurement.
10
+ * * `w2m_stats` reports the same two as `online: false` with `offline_reason: "silent"`, and counts
11
+ * `online: 3` out of `devices: 5`.
12
+ *
13
+ * A caller that asked the first one would plan work for five machines and get three. **A fleet whose size is
14
+ * reported two ways is a fleet whose plans are wrong by a factor of 1.7**, and the failure is silent because
15
+ * both answers look authoritative.
16
+ *
17
+ * So this module prefers `w2m_stats` — it is the one that carries a *reason* — and treats a device listing
18
+ * without a heartbeat age as **unverified rather than online**.
19
+ *
20
+ * ## The second disagreement, which is about history
21
+ *
22
+ * `w2m_history` lists, per task, a `lease_states` map. It is NOT the execution outcome: a task that expired
23
+ * mid-run can show `done` afterwards, because the map records the *current* lease state rather than what
24
+ * happened. Measured directly: of forty tasks in the sample, every one recorded macmini as `done`, while the
25
+ * session's own result envelopes included several `expired` with no exit code at all.
26
+ *
27
+ * Extracting the outcome requires `w2m_report`, which is a request per task. **A history listing is therefore
28
+ * a poor input for a reliability measurement, and using it as one would have produced a fleet that looked
29
+ * perfectly reliable** — the most dangerous possible error in a module whose purpose is to notice trouble.
30
+ */
31
+
32
+ /** How a machine's usability was determined, in descending order of trust. */
33
+ export const EVIDENCE = Object.freeze({
34
+ HEARTBEAT_WITH_RTT: 'a live heartbeat and a measured round trip',
35
+ HEARTBEAT_ALONE: 'a live heartbeat with no round trip measured',
36
+ STALE_OR_ABSENT: 'no recent heartbeat',
37
+ UNKNOWN: 'the relay did not say',
38
+ });
39
+
40
+ /**
41
+ * Classify one machine from a `w2m_stats` entry.
42
+ *
43
+ * `silent_ms` is the decisive field. A machine that is "online" but silent is not online, and the reason is
44
+ * reported alongside so a caller can distinguish "went away" from "never arrived".
45
+ */
46
+ export function classifyMachine(entry) {
47
+ if (entry === null || typeof entry !== 'object' || typeof entry.machine_id !== 'string') {
48
+ return { machine_id: null, usable: false, evidence: EVIDENCE.UNKNOWN, reason: 'no machine entry', detail: 'the relay did not describe this machine' };
49
+ }
50
+ const online = entry.online === true;
51
+ const rtt = entry.rtt_ms;
52
+ const stale = entry.rtt_stale === true;
53
+ if (online && !stale && Number.isFinite(rtt)) {
54
+ return { machine_id: entry.machine_id, name: entry.machine_name ?? null, usable: true, evidence: EVIDENCE.HEARTBEAT_WITH_RTT, rtt_ms: rtt, reason: null, detail: `live, ${rtt} ms` };
55
+ }
56
+ if (online && !stale) {
57
+ return { machine_id: entry.machine_id, name: entry.machine_name ?? null, usable: true, evidence: EVIDENCE.HEARTBEAT_ALONE, rtt_ms: null, reason: null, detail: 'live, round trip not measured' };
58
+ }
59
+ return {
60
+ machine_id: entry.machine_id,
61
+ name: entry.machine_name ?? null,
62
+ usable: false,
63
+ evidence: EVIDENCE.STALE_OR_ABSENT,
64
+ rtt_ms: Number.isFinite(rtt) && !stale ? rtt : null,
65
+ reason: entry.offline_reason ?? 'not online',
66
+ silent_ms: Number.isFinite(entry.silent_ms) ? entry.silent_ms : null,
67
+ detail: entry.offline_reason === 'silent'
68
+ ? `silent for ${Math.round((entry.silent_ms ?? 0) / 1000)}s`
69
+ : String(entry.offline_reason ?? 'not online'),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Read a `w2m_stats` payload.
75
+ *
76
+ * @returns {{
77
+ * paired: number, online: number, usable: number, silent: Array<object>,
78
+ * usable_machines: Array<object>, detail: string
79
+ * }}
80
+ */
81
+ export function readStats(stats) {
82
+ const machines = Array.isArray(stats?.machines) ? stats.machines : [];
83
+ const classified = machines.map(classifyMachine);
84
+ const paired = Number.isFinite(stats?.counts?.devices) ? stats.counts.devices : machines.length;
85
+ const usable = classified.filter((m) => m.usable);
86
+ const silent = classified.filter((m) => !m.usable);
87
+ // The number a caller should plan against is the number of machines that can actually run something.
88
+ return {
89
+ paired,
90
+ online: Number.isFinite(stats?.counts?.online) ? stats.counts.online : usable.length,
91
+ usable: usable.length,
92
+ usable_machines: usable,
93
+ silent,
94
+ runner_up_rtt_ms: usable.filter((m) => Number.isFinite(m.rtt_ms)).sort((a, b) => a.rtt_ms - b.rtt_ms)[0]?.rtt_ms ?? null,
95
+ detail: `${usable.length} of ${paired} paired machine(s) can run work`,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Compare a device listing against the stats reading, and report every machine they disagree about.
101
+ *
102
+ * **The disagreement is the product.** A caller that saw only the device listing would believe the fleet is
103
+ * larger than it is; a caller that saw only the stats would not know the listing claimed otherwise. Reporting
104
+ * the gap makes the gap fixable, and until it is fixed the smaller number is the true one.
105
+ */
106
+ export function reconcile(devices, stats) {
107
+ const listed = Array.isArray(devices?.devices) ? devices.devices : [];
108
+ const reading = readStats(stats);
109
+ const usableIds = new Set(reading.usable_machines.map((m) => m.machine_id));
110
+ const claimedOnline = listed.filter((d) => d?.state === 'online');
111
+ const disagreements = claimedOnline
112
+ .filter((d) => !usableIds.has(d.machine_id))
113
+ .map((d) => {
114
+ const m = reading.silent.find((s) => s.machine_id === d.machine_id);
115
+ return {
116
+ machine_id: d.machine_id,
117
+ name: d.name ?? null,
118
+ devices_says: 'online',
119
+ stats_says: m?.reason ?? 'not usable',
120
+ silent_ms: m?.silent_ms ?? null,
121
+ };
122
+ });
123
+ return {
124
+ devices_claims_online: claimedOnline.length,
125
+ stats_says_usable: reading.usable,
126
+ disagreements,
127
+ // The trust decision, stated as a field rather than left to the reader.
128
+ trust: 'stats',
129
+ reason: disagreements.length === 0
130
+ ? 'the two endpoints agree'
131
+ : `a device listing reports a machine as online when it has not been heard from; the stats reading carries a reason and is the one to plan against`,
132
+ detail: `${claimedOnline.length} claimed online, ${reading.usable} usable, ${disagreements.length} disagreement(s)`,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Whether a `w2m_history` payload can be used as a reliability measurement.
138
+ *
139
+ * It cannot, and this function exists so that conclusion is a checked fact rather than a comment. Every entry
140
+ * in the sample recorded the same state for the same machine, which is exactly what a current-state map looks
141
+ * like and exactly what an outcome record does not.
142
+ *
143
+ * @returns {{usable_as_outcome_record: boolean, reason: string, homogeneous: Array<string>}}
144
+ */
145
+ export function historyIsOutcomeRecord(history) {
146
+ const tasks = Array.isArray(history?.tasks) ? history.tasks : [];
147
+ if (tasks.length === 0) {
148
+ return { usable_as_outcome_record: false, reason: 'no tasks in the sample', homogeneous: [] };
149
+ }
150
+ // Per machine, how many distinct states appear across tasks. A machine at "done" in every task, including
151
+ // tasks that expired, is a current-state map rather than a record of what happened.
152
+ const states = new Map();
153
+ for (const task of tasks) {
154
+ for (const [id, state] of Object.entries(task?.lease_states ?? {})) {
155
+ if (!states.has(id)) states.set(id, new Set());
156
+ states.get(id).add(String(state));
157
+ }
158
+ }
159
+ const homogeneous = [...states.entries()].filter(([, set]) => set.size === 1).map(([id]) => id);
160
+ return {
161
+ usable_as_outcome_record: false,
162
+ homogeneous,
163
+ reason: homogeneous.length === states.size && states.size > 0
164
+ ? `every one of the ${states.size} machine(s) holds a single state across ${tasks.length} task(s) -- a current-state map, not an outcome record; use w2m_report per task instead`
165
+ : `a lease_states map reports the current state, not what happened; use w2m_report per task instead`,
166
+ };
167
+ }
@@ -185,7 +185,7 @@ const MAX_STAGES = 16;
185
185
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
186
186
  * overrides it at runtime for tests and packagers.
187
187
  */
188
- const PLUGIN_VERSION = '0.13.1';
188
+ const PLUGIN_VERSION = '0.15.0';
189
189
 
190
190
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
191
191
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;