@twinsearth/w2m-dsh-plugin 1.2.2 → 1.2.5

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.2.2"
8
+ "version": "1.2.5"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.2.2",
15
+ "version": "1.2.5",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.2.2",
4
+ "version": "1.2.5",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,176 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.2.5] — 2026-10-20
11
+
12
+ **Who is in the fleet, what they have read, who was named, and a retry that cannot double-post.**
13
+
14
+ ### Members and roles
15
+
16
+ `GET /v1/fleet/members` lists every machine with its name, role, join time, presence and last-seen time. `PUT
17
+ /v1/fleet/members/{id}` changes a role and requires the **operator** token, so the route sits with dispatch rather than behind the
18
+ device gate. Roles are `owner | worker | observer` and live in their own table: a device record is written by pairing and by
19
+ reconnect, and a role is an operator decision that must survive both. An unknown machine is a `worker`, never an `owner` -- a
20
+ re-pair cannot gain authority. The last owner cannot be demoted, because then nobody could readmit a machine.
21
+
22
+ ### Read watermarks
23
+
24
+ `PUT /v1/fleet/read/{session}` records how far this machine has read a conversation (its own token decides which machine);
25
+ `GET /v1/fleet/read/{session}` answers the watermark and an unread count of messages from others newer than it. Only forward: a
26
+ stale marker arriving late must not move the watermark backwards, or the count would grow for messages already seen.
27
+
28
+ ### Mentions
29
+
30
+ A message may carry `mentions: [machine_id]` (at most 10, validated). The row is still **one** row -- a mention is a
31
+ notification hint, not a delivery mechanism -- and the SSE notification says only whether **this** listener was named. It never
32
+ carries the text, and it does not carry the mention list.
33
+
34
+ ### Idempotent posts
35
+
36
+ `POST /v1/fleet/messages` accepts `idempotency_key` (per session). A retry after a lost response returns the **first** message
37
+ with `deduped: true` instead of storing the sentence twice. The key is bounded, like the message log.
38
+
39
+ ### Two defects the 1.2.4 tag carried, fixed here
40
+
41
+ The 1.2.4 commit (tag `v1.2.4`) had a red CI and a red Compat, both found before approval:
42
+
43
+ - **Compat**: `w2m_wait`'s `reducer_machine_id` declaration had been pasted **inside** `poll_ms`. Every local check was green;
44
+ the real host refused the whole entry ("parameters.poll_ms.reducer_machine_id is not supported by the value schema DSL"). The
45
+ parameter now sits where it belongs, and `schema-spec.mjs` gained `strayParameterProblems()` — a parameter declaration
46
+ nested inside another parameter is now refused where the tool is written, with two tests.
47
+ - **CI**: the cancellation HTTP test had been inserted inside an enclosing `it`. Node 24 ran it; Node 20/22 reported
48
+ `cancelledByParent`. The test now lives in its own top-level `describe`.
49
+
50
+ ### Verified
51
+
52
+ Gate GREEN twice, 15 of 15: 2628 tests / 2614 pass / 0 fail / 14 skip. New suites: fleet-members (6); fleet-messages grew to 11;
53
+ relay grew to 171 including two HTTP cases for members and watermarks.
54
+
55
+ The relay was deployed a fourth time from a hash-verified tarball with a state backup, and the new routes were exercised against
56
+ the live relay (members returned 8 machines; a role change returned 200; a watermark round-tripped; unread answered 0).
57
+
58
+ ### Not in this release
59
+
60
+ - **Offline replay of outbound chat** is not implemented. The agent's spool already holds undeliverable *results*; chat text is
61
+ not yet spooled and replayed.
62
+ - A two-machine end-to-end run of mentions/watermarks. The HTTP surface is tested against the fake relay and exercised once
63
+ against jp-shared, but not by two physical machines.
64
+ ## [1.2.4] — 2026-10-20
65
+
66
+ **Fleet collaboration, part two: cancellation, dependencies, a task board, questions on disk, and output that travels.**
67
+
68
+ ### Cancelling a task over HTTP
69
+
70
+ `state.cancelTask` has existed since the task ledger gained a cancel flag, but **no route reached it**. `POST /v1/tasks/{id}/cancel`
71
+ now does, with the same operator authorization dispatch requires, and `w2m_cancel` is the tool over it. Cancelling twice is not
72
+ an error: the ledger is the answer.
73
+
74
+ ### Prerequisites and a task board
75
+
76
+ - `POST /v1/task` accepts `depends_on[]`. Validated at **enqueue** time: unknown prerequisite, self-edge, duplicate edge and
77
+ cycles are refused by name (`src/relay/task-deps.mjs`), because a graph checked only when a worker picks the task up puts the
78
+ refusal on a machine that did not create the problem.
79
+ - `depends_on` / `assignee` travel on the task record.
80
+ - `w2m_tasks` (the fifteenth tool) is the board: it classifies every visible task as ready / active / waiting / blocked / done
81
+ (`src/plugin/task-graph.mjs`). A prerequisite outside the page is treated as **waiting**, never as absent.
82
+
83
+ ### Questions that survive a restart
84
+
85
+ `src/plugin/ask-store.mjs` keeps open questions in a JSONL file next to the profile (`ask-store.jsonl`, atomic rewrite, cap on
86
+ records). A reply that arrives after the deadline is recorded as `answered_after_timeout` rather than overwriting the timeout or
87
+ being dropped, and a settled question is never reopened. `w2m_chat` gained `action: "asks"`, and `await` settles the stored ask.
88
+
89
+ ### Output that travels
90
+
91
+ `src/util/dataflow.mjs` (moved from `src/plugin/` — the layer check refused an `agent -> plugin` edge) is now wired:
92
+
93
+ - a pipeline stage whose stdout exceeds 64 KiB **uploads it as a blob** (`PUT /v1/fleet/blobs/{sha256}`) and the envelope names
94
+ it (`blob_sha256`, `output_mode: "blob"`) instead of inlining head/tail;
95
+ - a failed upload **fails the stage** by name (`BLOB_UPLOAD_FAILED`) rather than leaving a reference to nothing;
96
+ - `w2m_wait` builds the reduce plan with `buildReducePlan` from the **raw** envelopes and marks the verdict `degraded` when a
97
+ shard is missing — but only when the caller declared `reducer_machine_id`, so a large pipeline result is not mistaken for a
98
+ partial reduce.
99
+
100
+ The end-to-end fixture this release adds (`test/agent.test.mjs`) runs a real agent against a fake relay: it asserts the blob name
101
+ is the hash of the bytes sent and that a refused upload fails the stage. Two real defects surfaced while writing it and are fixed
102
+ here: the stage stdout arrives as a `Buffer` (it was handed to the signature helper as a string), and a 70 KiB test argv exceeds
103
+ Windows' `CreateProcess` limit.
104
+
105
+ ### Retry and hand-off
106
+
107
+ - `POST /v1/task` accepts `max_attempts` (1..5, default 1) and `retry_on` (`expired|failed|partial|timeout`). Defaults keep
108
+ at-most-once for every existing caller; a malformed policy is `BAD_REQUEST`, not silently ignored.
109
+ - On lease expiry, `retry_on` containing `expired` performs the hand-off `halt=now` always had: attempt+1 and a re-offer to idle
110
+ machines (`_takeover`, which already refuses writing tasks to avoid double application).
111
+
112
+ ### mapreduce
113
+
114
+ `mode: "mapreduce"` is accepted with `reduce_argv` and an optional `reducer_machine_id`. The map half is ordinary dispatch; the
115
+ reduce half is **described, never launched** — the caller runs it as an explicit second task after `w2m_wait` says every shard is
116
+ present, so one task id never describes two executions.
117
+
118
+ ### Relay
119
+
120
+ The relay was deployed three times during this release (cancel route, retry policy, mapreduce) from hash-verified tarballs, each
121
+ with a state backup, a health check, and a 401 check on the pre-existing routes. **The relay name is separate from the package
122
+ version**: the tarball deployed was the 1.2.3-labelled build carrying this code, and is re-deployed from the released 1.2.4 tarball
123
+ below.
124
+
125
+ ### Verified
126
+
127
+ Gate GREEN twice, 15 of 15: 2610 tests / 2596 pass / 0 fail / 14 skip. Notable suites: agent 149 (147 pass, 2 skipped),
128
+ relay 169, tools 127, task-deps 13, task-graph 11, ask-store 8, turn 5, dataflow 7, dataflow-wiring 7.
129
+
130
+ ### Not verified
131
+
132
+ A two-machine run of cancel / retry / mapreduce end to end. The code paths are tested against a fake relay and against the real
133
+ relay's HTTP surface, but no test here runs two physical machines exchanging an expired lease.
134
+ ## [1.2.3] — 2026-10-20
135
+
136
+ **The packed artifact did not mount. This fixes that, and the reason it was not caught here.**
137
+
138
+ `v1.2.2` was released, published to npm, and **failed Compat**: the packed artifact would not mount in a real profile.
139
+
140
+ ```
141
+ JsonSchemaError: unsupported JSON schema: parameters.params.additionalProperties must be explicitly true or false
142
+ ```
143
+
144
+ `w2m_intent` declared `params: { type: "object", ... }` without stating `additionalProperties`. DSH refuses such a
145
+ schema **at mount time** -- in the profile of whoever installs the build -- and this repository had no check that would
146
+ have said so before shipping: the local loader gate mounted the artifact on a host whose compiler path differs, so it
147
+ stayed green while the real profile refused the entry.
148
+
149
+ ### What changed
150
+
151
+ - `w2m_intent`'s `params` now states `additionalProperties: true`, and every tool schema is checked when it is defined.
152
+ - New `src/plugin/schema-spec.mjs`: a pure walker over schema-shaped values, and `schemaProblems()` which returns every
153
+ object node that does not state `additionalProperties` as a boolean -- all of them at once, not the first one.
154
+ - `defineTool` now runs that check, so a tool with an under-specified object parameter fails where it is written rather
155
+ than in someone else's profile.
156
+ - `test/schema-spec.test.mjs`: 5 tests on the walker and the checker (nested properties, array items, unions, multiple
157
+ offenders, `true` accepted as well as `false`).
158
+
159
+ ### Panel polish
160
+
161
+ - **Thread grouping**: consecutive messages that share a thread are shown under one thread head. Only consecutive runs are
162
+ grouped, because re-sorting by thread would put an answer above the question it answers.
163
+ - **Unread count**: the trigger shows how many messages arrived since this conversation was last opened. The watermark is
164
+ local storage per conversation -- the relay stays stateless and no "who read what" is invented.
165
+
166
+ - **Task board**: the panel shows the relay's own task list (up to six rows: id prefix, mode, status) through a host
167
+ proxy (`/api/w2m/tasks`), so the browser never holds a device token.
168
+
169
+ Still missing from the panel: a timeout mark on an unanswered question, which needs the ask store from the turn state
170
+ machine.
171
+ ### Verified
172
+
173
+ Gate GREEN twice, 15 of 15: 2555 tests / 2541 pass / 0 fail / 14 skip.
174
+
175
+ ### Not verified
176
+
177
+ Compat itself: that workflow runs in its own environment, so this release is verified by the same local gate that missed
178
+ the defect. **Watch the Compat run for this tag.** If it fails again, the mount is failing for a different reason than the
179
+ schema and the log will say which entry and which fiber.
10
180
  ## [1.2.2] — 2026-10-20
11
181
 
12
182
  **Fleet collaboration, part one: shared-memory writes, a council of machines, and the panel that reaches them.**
@@ -22,33 +192,33 @@ Creating the tag through the API had already triggered one run, which created th
22
192
  collided with it. A release step that cannot be retried fails for the wrong reason, so `beta.yml` now refreshes an existing
23
193
  release (notes + assets with `--clobber`) instead of refusing, and says so in the log.
24
194
 
25
- ### What is in it
26
-
27
- - **Shared-memory writes**: `w2m_memory` gained `action: "append"` with `if_revision`. A stale revision comes back as a 409
28
- the caller reads (revision, how far behind, which records it has not seen) and is deliberately **not** retried in the
29
- transport -- retrying there turns a visible conflict into a silent overwrite.
30
- - **A council of machines**: `src/plugin/council-fleet.mjs` keeps the single-machine decision rule and changes the
31
- transport to chat messages on `council-<id>`, so every machine reads the same ballots and recounts the same outcome.
32
- Weights are a pure function of the ballot; a tie is refused; one ballot per machine; a late ballot is refused by name.
33
- `w2m_council` exposes `fleet-open` / `fleet-vote` / `fleet-decide` / `fleet-state` / `fleet-list`.
34
- - **The panel**: a recipient picker (default "all"), a thread chip on messages that belong to a thread, click-to-reply with
35
- a quoted line above the input, and the reply thread travelling with it. The host route refuses a single recipient that
36
- is not in this fleet (`CHAT_UNKNOWN_RECIPIENT`) and says `CHAT_ROSTER_UNAVAILABLE` when the roster could not be read at
37
- all -- a relay outage is not a typo in the recipient.
38
- - **A readable view over memory**: `w2m_memory` read now returns `view` (counts by machine and by status, one line per
39
- record). A row shape it does not recognise is shown as itself rather than dropped.
40
- - **The beta pipeline is repeatable**: creating the tag had triggered a run that created the release, so the explicit
41
- dispatch collided with it. The step now refreshes an existing release instead of refusing.
42
-
43
- ### Verified
44
-
45
- Gate GREEN twice, 15 of 15: 2550 tests / 2536 pass / 0 fail / 14 skip. tools 125 (three append cases), council-fleet 12,
46
- memory-view 6, client-bundle 25.
47
-
48
- ### Not verified
49
-
50
- The fleet council and a directed message across two live machines, and the panel against a second machine. Those need two
51
- machines on this build.
195
+ ### What is in it
196
+
197
+ - **Shared-memory writes**: `w2m_memory` gained `action: "append"` with `if_revision`. A stale revision comes back as a 409
198
+ the caller reads (revision, how far behind, which records it has not seen) and is deliberately **not** retried in the
199
+ transport -- retrying there turns a visible conflict into a silent overwrite.
200
+ - **A council of machines**: `src/plugin/council-fleet.mjs` keeps the single-machine decision rule and changes the
201
+ transport to chat messages on `council-<id>`, so every machine reads the same ballots and recounts the same outcome.
202
+ Weights are a pure function of the ballot; a tie is refused; one ballot per machine; a late ballot is refused by name.
203
+ `w2m_council` exposes `fleet-open` / `fleet-vote` / `fleet-decide` / `fleet-state` / `fleet-list`.
204
+ - **The panel**: a recipient picker (default "all"), a thread chip on messages that belong to a thread, click-to-reply with
205
+ a quoted line above the input, and the reply thread travelling with it. The host route refuses a single recipient that
206
+ is not in this fleet (`CHAT_UNKNOWN_RECIPIENT`) and says `CHAT_ROSTER_UNAVAILABLE` when the roster could not be read at
207
+ all -- a relay outage is not a typo in the recipient.
208
+ - **A readable view over memory**: `w2m_memory` read now returns `view` (counts by machine and by status, one line per
209
+ record). A row shape it does not recognise is shown as itself rather than dropped.
210
+ - **The beta pipeline is repeatable**: creating the tag had triggered a run that created the release, so the explicit
211
+ dispatch collided with it. The step now refreshes an existing release instead of refusing.
212
+
213
+ ### Verified
214
+
215
+ Gate GREEN twice, 15 of 15: 2550 tests / 2536 pass / 0 fail / 14 skip. tools 125 (three append cases), council-fleet 12,
216
+ memory-view 6, client-bundle 25.
217
+
218
+ ### Not verified
219
+
220
+ The fleet council and a directed message across two live machines, and the panel against a second machine. Those need two
221
+ machines on this build.
52
222
 
53
223
  ## [1.2.1] — 2026-10-18
54
224