@twinsearth/w2m-dsh-plugin 0.26.0 → 0.28.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 +116 -0
- package/package.json +5 -4
- package/src/plugin/tools.mjs +67 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,122 @@ 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.28.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**First real-machine verification of the recent work, and a tool that packs four answers into one exit code.**
|
|
10
|
+
|
|
11
|
+
### The measurement
|
|
12
|
+
|
|
13
|
+
The MacMini's checkout was updated to 0.27.0 and its own suites run there, with **the exit code produced by
|
|
14
|
+
that machine** as the evidence:
|
|
15
|
+
|
|
16
|
+
| Suite on MacMini | Result |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `permission.test.mjs` | pass |
|
|
19
|
+
| `fleet-scale.test.mjs` | pass |
|
|
20
|
+
| `fleet-state.test.mjs` | pass |
|
|
21
|
+
| `crypto-blake2s.test.mjs` | pass |
|
|
22
|
+
|
|
23
|
+
Those four cover the code written in this span — the permission gate, the fleet arithmetic, the endpoint
|
|
24
|
+
reconciliation, and the crypto capability report. This is the goal's "verify on the real machine with exit
|
|
25
|
+
codes and artifacts" satisfied for that body of work, rather than inferred.
|
|
26
|
+
|
|
27
|
+
### `scripts/machine-test-packet.mjs`
|
|
28
|
+
|
|
29
|
+
A dispatch carries **one** exit code, so running four suites in one `node --test` answers only "did anything
|
|
30
|
+
fail". This packs **two bits per file** into a single byte: `10` passed, `01` failed, `00` absent, `11`
|
|
31
|
+
not measured. **`00` is distinguished from `01` on purpose** — "the file is not on this machine" is a
|
|
32
|
+
deployment finding, "the file ran and failed" is a code finding.
|
|
33
|
+
|
|
34
|
+
**Four dispatches were spent on this channel before the tool existed, two of them on my own misreadings:**
|
|
35
|
+
|
|
36
|
+
* a TAP parser matching `^ok ` that counted **nothing**, because Node indents its subtests. It returned
|
|
37
|
+
`pass 0 fail 0` and I nearly read that as "no tests ran".
|
|
38
|
+
* a summary regex that matched nothing, so the exit code described **my parser** rather than the code under
|
|
39
|
+
test.
|
|
40
|
+
|
|
41
|
+
Both were the same mistake in different clothes, and it is the mistake this project keeps making: **assuming
|
|
42
|
+
the shape of output instead of reading it.**
|
|
43
|
+
|
|
44
|
+
### Three defects in the tool itself, all caught by its own suite
|
|
45
|
+
|
|
46
|
+
* **The docstring claimed "zero is the all-clear", and that is false.** One passing file is `2`, four are
|
|
47
|
+
`170`. **Zero means every file was ABSENT** — a total deployment failure the old claim would have had a
|
|
48
|
+
reader take as a clean run. `allPassedCode()` now computes the value and the test asserts the arithmetic.
|
|
49
|
+
* **The guard against emitting `126`/`127` was never reached** by the inputs I guessed. Enumerating the whole
|
|
50
|
+
two-bit space found the only two combinations that land on a shell-owned value
|
|
51
|
+
(`[PASSED, WAITED, WAITED, FAILED]` and `[WAITED, WAITED, WAITED, FAILED]`).
|
|
52
|
+
* **The reassuring message was unreachable**, because it tested `code === 0` rather than the outcomes.
|
|
53
|
+
`packOutcomes([])` returns `0`, so the one code that reads as success was attached to the one input that
|
|
54
|
+
measured nothing.
|
|
55
|
+
|
|
56
|
+
### The budget, and the fifth time I hit the same ceiling
|
|
57
|
+
|
|
58
|
+
The first version took a 40-second timeout **per file** and ran four files: a 160-second worst case against
|
|
59
|
+
this fleet's **~50-second lease ceiling** (`heartbeatIntervalMs 10s × missedHeartbeats 2 + leaseGraceMs 30s`).
|
|
60
|
+
It **expired** on the real machine, and an expired lease carries **no exit code at all** — so the measurement
|
|
61
|
+
was lost rather than reported negative. That is the fifth time in this project that a dispatch was built
|
|
62
|
+
without respect for the ceiling.
|
|
63
|
+
|
|
64
|
+
The fix is a **total budget** (`DEFAULT_BUDGET_MS`, 30s) with a per-file cap, and files that miss their turn
|
|
65
|
+
are named `WAITED` rather than dropped — a packet that quietly omitted the last file would look like a clean
|
|
66
|
+
run of the ones it managed. A per-file timeout answers "is this file hung"; only a total budget answers "will
|
|
67
|
+
the answer survive the trip".
|
|
68
|
+
|
|
69
|
+
### What the real machine refused, which is a finding and not a bug
|
|
70
|
+
|
|
71
|
+
`node scripts/machine-test-packet.mjs ...` came back **`COMMAND_NOT_ALLOWED`**. The MacMini's allow-list
|
|
72
|
+
admits `node -e` and `node --test`; it does not admit an arbitrary script path, and the packet **spawns
|
|
73
|
+
`node --test` internally**, so it would be refused a second time. The tool is correct and tested; it needs an
|
|
74
|
+
allow-list entry to run there, and **that is a change nobody has made.**
|
|
75
|
+
|
|
76
|
+
**11 tests, 0 failures.**
|
|
77
|
+
|
|
78
|
+
## [0.27.0] — 2026-10-15
|
|
79
|
+
|
|
80
|
+
**A blocked fleet reports itself. The approval count is in `w2m_status`, and it arrives as a note.**
|
|
81
|
+
|
|
82
|
+
A queue visible only to someone who remembers to look at it is a queue that stays full, and **a fleet blocked
|
|
83
|
+
on approval looks exactly like an idle fleet.** `w2m_status` now carries an `approvals` block and, when
|
|
84
|
+
anything is waiting, a note placed **first** — ordering being the only emphasis a list of notes has:
|
|
85
|
+
|
|
86
|
+
> *"3 dispatch(es) are WAITING FOR AN APPROVER and will not run until one answers; read them with
|
|
87
|
+
> `w2m_approvals` (1 past their deadline); the oldest has been waiting 240s"*
|
|
88
|
+
|
|
89
|
+
### `constructed: false` is not `pending: 0`
|
|
90
|
+
|
|
91
|
+
A machine that has never tripped the permission gate should not be described as having an empty queue **as
|
|
92
|
+
though that were a measurement.** The block says which it is, and the note is emitted only for real pending
|
|
93
|
+
work.
|
|
94
|
+
|
|
95
|
+
### The whole chain, across three tools, asserted end to end
|
|
96
|
+
|
|
97
|
+
An ordinary actor (49) attempts a write → refused with `W2M_PERMISSION_NEEDS_APPROVAL` **carrying the queued
|
|
98
|
+
id** → `w2m_status` reports `pending: 1` and the blocking note **without being asked about approvals** → a
|
|
99
|
+
captain settles it → the signal clears.
|
|
100
|
+
|
|
101
|
+
### The default approver is the requester, and the index says so
|
|
102
|
+
|
|
103
|
+
The test discovered this rather than assuming it: a request records its subject as the machine's name, and an
|
|
104
|
+
approver who **defaults to that same name is the subject** — so `may_act` is false with
|
|
105
|
+
`APPROVAL_SELF_APPROVAL`. That is correct, and it is a real consequence of the defaults rather than a bug. The
|
|
106
|
+
test now names a distinct approver and **asserts the refusal first**, so the self-approval rule is pinned
|
|
107
|
+
rather than sidestepped.
|
|
108
|
+
|
|
109
|
+
### A payload silently dropped, caught by a test that found nothing
|
|
110
|
+
|
|
111
|
+
`W2MError` accepts only `code`, `message`, `hint`, `detail`, `cause` and `reason_code`. The first version
|
|
112
|
+
passed `approval_id` and `request` as **top-level options**, so they were **discarded without complaint** and
|
|
113
|
+
the refusal told a caller to await approval **with no way to name what they were waiting for.** They now
|
|
114
|
+
travel in `detail`, and the constructor's accepted keys are named in a comment beside the call.
|
|
115
|
+
|
|
116
|
+
Two of my own errors in this round were the same shape as the ones before it: a block hoisted into the **wrong
|
|
117
|
+
scope** (module level instead of inside `execute`, which `node --check` accepted because the name existed
|
|
118
|
+
*somewhere*), and a syntactic fix that put a statement **inside an object literal**. Both were caught by
|
|
119
|
+
running the suite, not by reading.
|
|
120
|
+
|
|
121
|
+
**1 new cross-tool test, 0 failures.**
|
|
122
|
+
|
|
7
123
|
## [0.26.0] — 2026-10-15
|
|
8
124
|
|
|
9
125
|
**`w2m_approvals`: the approval chain is reachable by an operator, not only by the suite.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.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/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.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/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.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/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.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 .",
|
|
@@ -145,7 +145,8 @@
|
|
|
145
145
|
"test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs",
|
|
146
146
|
"test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
|
|
147
147
|
"test:installed-version": "node --test --test-force-exit test/installed-version.test.mjs",
|
|
148
|
-
"test:permission": "node --test --test-force-exit test/permission.test.mjs"
|
|
148
|
+
"test:permission": "node --test --test-force-exit test/permission.test.mjs",
|
|
149
|
+
"test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs"
|
|
149
150
|
},
|
|
150
151
|
"keywords": [
|
|
151
152
|
"dsh-bundle",
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -193,7 +193,7 @@ const MAX_STAGES = 16;
|
|
|
193
193
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
194
194
|
* overrides it at runtime for tests and packagers.
|
|
195
195
|
*/
|
|
196
|
-
const PLUGIN_VERSION = '0.
|
|
196
|
+
const PLUGIN_VERSION = '0.28.0';
|
|
197
197
|
|
|
198
198
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
199
199
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -1118,6 +1118,7 @@ function safeFilePart(value) {
|
|
|
1118
1118
|
*/
|
|
1119
1119
|
function canonicalJson(value) {
|
|
1120
1120
|
if (value === undefined) return 'null';
|
|
1121
|
+
|
|
1121
1122
|
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
|
|
1122
1123
|
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
|
|
1123
1124
|
const keys = Object.keys(value).sort();
|
|
@@ -3087,9 +3088,26 @@ function readAggregate(body) {
|
|
|
3087
3088
|
* @param {object} p2p The block from {@link readP2PStatus}.
|
|
3088
3089
|
* @returns {string[]}
|
|
3089
3090
|
*/
|
|
3090
|
-
function statusNotes(relay, rtt, cfg, p2p) {
|
|
3091
|
+
function statusNotes(relay, rtt, cfg, p2p, approvalsNote = null) {
|
|
3091
3092
|
const notes = [];
|
|
3092
3093
|
|
|
3094
|
+
/**
|
|
3095
|
+
* v0.26.0: blocked work announces itself.
|
|
3096
|
+
*
|
|
3097
|
+
* The whole reason the count is in `w2m_status` at all. A note is what a caller reads without asking a
|
|
3098
|
+
* further question, and "three dispatches are waiting for an approver" is the single most actionable
|
|
3099
|
+
* sentence this tool can emit: the fleet is not idle, it is stopped, and it is stopped on a person.
|
|
3100
|
+
*
|
|
3101
|
+
* It is placed first because ordering is the only emphasis a list of notes has.
|
|
3102
|
+
*/
|
|
3103
|
+
if (approvalsNote !== null && Number.isFinite(approvalsNote.pending) && approvalsNote.pending > 0) {
|
|
3104
|
+
notes.push(
|
|
3105
|
+
`${approvalsNote.pending} dispatch(es) are WAITING FOR AN APPROVER and will not run until one answers; ` +
|
|
3106
|
+
`read them with \`w2m_approvals\`${approvalsNote.overdue > 0 ? ` (${approvalsNote.overdue} past their deadline)` : ''}` +
|
|
3107
|
+
`${Number.isFinite(approvalsNote.oldest_waiting_ms) ? `; the oldest has been waiting ${Math.round(approvalsNote.oldest_waiting_ms / 1000)}s` : ''}`,
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3093
3111
|
/**
|
|
3094
3112
|
* §7's one-line note, and the reason it exists at all: the shared server is a *default*, and a
|
|
3095
3113
|
* default that an operator cannot see is a machine quietly talking to somebody else's host. The
|
|
@@ -3777,8 +3795,12 @@ export async function apply(ctx, config = {}) {
|
|
|
3777
3795
|
'W2M_PERMISSION_NEEDS_APPROVAL: ' + gate.decision.detail,
|
|
3778
3796
|
{
|
|
3779
3797
|
hint: 'this dispatch is queued awaiting an approver; approve or reject it with w2m_approvals, then dispatch again',
|
|
3780
|
-
|
|
3781
|
-
|
|
3798
|
+
// Carried in `detail`, not as top-level options: `W2MError` accepts only code, message, hint,
|
|
3799
|
+
// detail, cause and reason_code, so anything else passed here is DROPPED SILENTLY. The first
|
|
3800
|
+
// version of this passed `approval_id` at the top level and the test caught it by finding
|
|
3801
|
+
// nothing -- which is exactly the kind of omission that would otherwise have reached a user as
|
|
3802
|
+
// "await approval" with no way to name what they are waiting for.
|
|
3803
|
+
detail: { approval_id: raised.approval_id, request },
|
|
3782
3804
|
},
|
|
3783
3805
|
);
|
|
3784
3806
|
}
|
|
@@ -4327,6 +4349,45 @@ export async function apply(ctx, config = {}) {
|
|
|
4327
4349
|
await p2p.ensureStarted();
|
|
4328
4350
|
const p2pStatus = await readP2PStatus(cfg, p2p, Number.isInteger(relay.peers_announced) ? relay.peers_announced : null);
|
|
4329
4351
|
|
|
4352
|
+
/**
|
|
4353
|
+
* v0.26.0: how much work is waiting for an approver.
|
|
4354
|
+
*
|
|
4355
|
+
* ## Why it belongs in `w2m_status` rather than only in `w2m_approvals`
|
|
4356
|
+
*
|
|
4357
|
+
* A queue that is only visible to someone who remembers to look at it is a queue that stays full,
|
|
4358
|
+
* and a fleet that is blocked on approval looks exactly like a fleet that is idle. This makes the
|
|
4359
|
+
* blocked state **report itself**, which is the difference between a tool an operator has and a
|
|
4360
|
+
* signal an operator receives.
|
|
4361
|
+
*
|
|
4362
|
+
* `constructed` distinguishes "nothing is waiting" from "nothing has ever asked". They are the same
|
|
4363
|
+
* number and very different facts, and a machine that never tripped the permission gate should not
|
|
4364
|
+
* be described as having an empty queue as though that were a measurement.
|
|
4365
|
+
*/
|
|
4366
|
+
const approvalsBlock = (() => {
|
|
4367
|
+
if (approvals === null) {
|
|
4368
|
+
return {
|
|
4369
|
+
constructed: false,
|
|
4370
|
+
pending: null,
|
|
4371
|
+
detail: 'no approval has ever been requested on this machine, so no queue exists yet; this is not the same as an empty queue',
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
const pendingRows = approvals.all();
|
|
4375
|
+
const overdue = pendingRows.filter((r) => r.overdue === true).length;
|
|
4376
|
+
return {
|
|
4377
|
+
constructed: true,
|
|
4378
|
+
pending: pendingRows.length,
|
|
4379
|
+
overdue,
|
|
4380
|
+
// The oldest request is the one whose loss costs most, so it is reported rather than left for a
|
|
4381
|
+
// reader to sort out of the rows.
|
|
4382
|
+
oldest_waiting_ms: pendingRows.length === 0
|
|
4383
|
+
? null
|
|
4384
|
+
: Math.max(...pendingRows.map((r) => r.waiting_ms ?? 0)),
|
|
4385
|
+
detail: pendingRows.length === 0
|
|
4386
|
+
? 'the queue exists and nothing is waiting'
|
|
4387
|
+
: `${pendingRows.length} dispatch(es) waiting for an approver${overdue > 0 ? `, ${overdue} overdue` : ''}`,
|
|
4388
|
+
};
|
|
4389
|
+
})();
|
|
4390
|
+
|
|
4330
4391
|
return JSON.stringify(
|
|
4331
4392
|
{
|
|
4332
4393
|
ok: true,
|
|
@@ -4416,7 +4477,8 @@ export async function apply(ctx, config = {}) {
|
|
|
4416
4477
|
: targetRoutes.reason,
|
|
4417
4478
|
sessions_with_a_target: defaultSelectionStore.list().length,
|
|
4418
4479
|
},
|
|
4419
|
-
|
|
4480
|
+
approvals: approvalsBlock,
|
|
4481
|
+
notes: statusNotes(relay, rtt, cfg, p2pStatus, approvalsBlock),
|
|
4420
4482
|
},
|
|
4421
4483
|
null,
|
|
4422
4484
|
2,
|