@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 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.26.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",
@@ -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.26.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
- approval_id: raised.approval_id,
3781
- request,
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
- notes: statusNotes(relay, rtt, cfg, p2pStatus),
4480
+ approvals: approvalsBlock,
4481
+ notes: statusNotes(relay, rtt, cfg, p2pStatus, approvalsBlock),
4420
4482
  },
4421
4483
  null,
4422
4484
  2,