@twinsearth/w2m-dsh-plugin 0.26.0 → 0.27.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 +45 -0
- package/package.json +1 -1
- package/src/plugin/tools.mjs +67 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ 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.27.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**A blocked fleet reports itself. The approval count is in `w2m_status`, and it arrives as a note.**
|
|
10
|
+
|
|
11
|
+
A queue visible only to someone who remembers to look at it is a queue that stays full, and **a fleet blocked
|
|
12
|
+
on approval looks exactly like an idle fleet.** `w2m_status` now carries an `approvals` block and, when
|
|
13
|
+
anything is waiting, a note placed **first** — ordering being the only emphasis a list of notes has:
|
|
14
|
+
|
|
15
|
+
> *"3 dispatch(es) are WAITING FOR AN APPROVER and will not run until one answers; read them with
|
|
16
|
+
> `w2m_approvals` (1 past their deadline); the oldest has been waiting 240s"*
|
|
17
|
+
|
|
18
|
+
### `constructed: false` is not `pending: 0`
|
|
19
|
+
|
|
20
|
+
A machine that has never tripped the permission gate should not be described as having an empty queue **as
|
|
21
|
+
though that were a measurement.** The block says which it is, and the note is emitted only for real pending
|
|
22
|
+
work.
|
|
23
|
+
|
|
24
|
+
### The whole chain, across three tools, asserted end to end
|
|
25
|
+
|
|
26
|
+
An ordinary actor (49) attempts a write → refused with `W2M_PERMISSION_NEEDS_APPROVAL` **carrying the queued
|
|
27
|
+
id** → `w2m_status` reports `pending: 1` and the blocking note **without being asked about approvals** → a
|
|
28
|
+
captain settles it → the signal clears.
|
|
29
|
+
|
|
30
|
+
### The default approver is the requester, and the index says so
|
|
31
|
+
|
|
32
|
+
The test discovered this rather than assuming it: a request records its subject as the machine's name, and an
|
|
33
|
+
approver who **defaults to that same name is the subject** — so `may_act` is false with
|
|
34
|
+
`APPROVAL_SELF_APPROVAL`. That is correct, and it is a real consequence of the defaults rather than a bug. The
|
|
35
|
+
test now names a distinct approver and **asserts the refusal first**, so the self-approval rule is pinned
|
|
36
|
+
rather than sidestepped.
|
|
37
|
+
|
|
38
|
+
### A payload silently dropped, caught by a test that found nothing
|
|
39
|
+
|
|
40
|
+
`W2MError` accepts only `code`, `message`, `hint`, `detail`, `cause` and `reason_code`. The first version
|
|
41
|
+
passed `approval_id` and `request` as **top-level options**, so they were **discarded without complaint** and
|
|
42
|
+
the refusal told a caller to await approval **with no way to name what they were waiting for.** They now
|
|
43
|
+
travel in `detail`, and the constructor's accepted keys are named in a comment beside the call.
|
|
44
|
+
|
|
45
|
+
Two of my own errors in this round were the same shape as the ones before it: a block hoisted into the **wrong
|
|
46
|
+
scope** (module level instead of inside `execute`, which `node --check` accepted because the name existed
|
|
47
|
+
*somewhere*), and a syntactic fix that put a statement **inside an object literal**. Both were caught by
|
|
48
|
+
running the suite, not by reading.
|
|
49
|
+
|
|
50
|
+
**1 new cross-tool test, 0 failures.**
|
|
51
|
+
|
|
7
52
|
## [0.26.0] — 2026-10-15
|
|
8
53
|
|
|
9
54
|
**`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.27.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",
|
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.27.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,
|