@twinsearth/w2m-dsh-plugin 0.50.0 → 0.53.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 +190 -0
- package/mcp.json +4 -0
- package/package.json +6 -3
- package/plugin.json +14 -0
- package/skills/fleet-control/SKILL.md +91 -0
- package/skills/memory-sync/SKILL.md +69 -0
- package/skills/resource-schedule/SKILL.md +72 -0
- package/skills/session-ref/SKILL.md +76 -0
- package/src/plugin/tools.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,196 @@ 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.53.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**The shared-memory layer gets a suite, and it corrected three things this session had already "verified".**
|
|
10
|
+
|
|
11
|
+
### The layer was measured by hand; now it is measured by the suite
|
|
12
|
+
|
|
13
|
+
Field-level omission at level 0, the refusal of an unproducible ancestor, the level 0/70 difference — **all of
|
|
14
|
+
that was real evidence, produced through actual dispatches to an actual machine.** It was also **not
|
|
15
|
+
repeatable**: it lived in a session, not in the suite. **A measurement that cannot be re-run is a memory of a
|
|
16
|
+
measurement.**
|
|
17
|
+
|
|
18
|
+
`test/memory-layer.test.mjs` adds **22 assertions**, and the suite grew to 84 files.
|
|
19
|
+
|
|
20
|
+
### Three corrections, each of which the hand-run had got wrong
|
|
21
|
+
|
|
22
|
+
**1. `evidence` costs 50, not 70.**
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
FIELD_LEVELS = { subject: 0, body: 0, evidence: 50 }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A level-70 dispatch succeeded, and **70 was written down as the threshold.** It is not: 49 is refused and 50 is
|
|
29
|
+
allowed, and **50 is not arbitrary — it is the bottom of the `captain` band**, which is why the number is what
|
|
30
|
+
it is.
|
|
31
|
+
|
|
32
|
+
**2. `confidence` is an invariant, not a high-cost field.**
|
|
33
|
+
|
|
34
|
+
`NEVER_ACCEPTED.confidence` = *"a receiver does not adopt the sender's certainty (**invariant, not a
|
|
35
|
+
default**)"*. Level 90 is refused exactly as level 0 is, and there is no level that satisfies it. **The reason
|
|
36
|
+
string says "invariant" on purpose: a reader who thinks it is a threshold will try to raise it.**
|
|
37
|
+
|
|
38
|
+
**3. `FIELD_UNDECLARED` does not mean "forbidden", and the two APIs answer different questions.**
|
|
39
|
+
|
|
40
|
+
| call | question | `origin` / `observed_at` / `lineage` |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `acceptField` | *may a receiver choose this?* | `FIELD_UNDECLARED` — no rule lets a receiver pick it |
|
|
43
|
+
| `acceptWithAuthority` | *what does this receiver keep?* | **present in `accepted`** — they are inherited |
|
|
44
|
+
|
|
45
|
+
**So `UNDECLARED` means "not a receiver's to choose", not "refused".** Reading it as a refusal would have the
|
|
46
|
+
receiver drop the ancestry that makes the block meaningful — and `INHERITED_FIELDS` plus
|
|
47
|
+
`RECEIVER_CHOOSABLE_FIELDS` partition the seven exactly, so neither list can change without the other.
|
|
48
|
+
|
|
49
|
+
### Four assertions of mine that were wrong, and the last one is the interesting one
|
|
50
|
+
|
|
51
|
+
* **`ancestry` takes a `Map`, not an array**, and returns `{chain, missing, cycle, depth}` — **there is no `ok`
|
|
52
|
+
and no `code`.**
|
|
53
|
+
* **`MISSING_PARENT` is not an `ancestry` outcome.** `ancestry` **collects** gaps in `missing` — *"knowing how
|
|
54
|
+
much of a history is absent is more useful than knowing that some of it is"* — while `acceptBlock` is what
|
|
55
|
+
**refuses**, returning `accepted: null`.
|
|
56
|
+
* **`isDescendantOf` returns `{descendant, chain, missing, checked}`, not a boolean.** It returns more than a
|
|
57
|
+
boolean because that is its point: *"a claim that descends from something this machine already thought is an
|
|
58
|
+
**echo**, not corroboration, and **a fleet that cannot tell those apart will confidently agree with
|
|
59
|
+
itself**."* A parent that is not held gives `descendant: false` — and my assertion had demanded `true`.
|
|
60
|
+
* **I could not construct a cycle, and that turned out to be the finding.** `blockHash` covers `lineage`, so
|
|
61
|
+
rebuilding a block changes its hash and the edge no longer closes — **a real cycle would need a hash
|
|
62
|
+
preimage.** The walk produces a *diamond* instead, and `CYCLE` is reachable **only** from a `have` map whose
|
|
63
|
+
keys disagree with the blocks under them: a corrupted or hostile store, where **terminating is the point,
|
|
64
|
+
because an unbounded walk on hostile input is a denial of service.**
|
|
65
|
+
|
|
66
|
+
**Three attempts at that test asserted a cycle the code was right not to report.** The fourth printed the
|
|
67
|
+
traversal instead of predicting it, which is the only one of the four that produced a fact.
|
|
68
|
+
|
|
69
|
+
## [0.52.0] — 2026-10-15
|
|
70
|
+
|
|
71
|
+
**Agent Plugins 1.0.0 manifests, validated against the published schemas — and the honest note about who
|
|
72
|
+
reads them.**
|
|
73
|
+
|
|
74
|
+
### What shipped
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
plugin.json root manifest, Agent Plugins 1.0.0, $schema pinned to the canonical identifier
|
|
78
|
+
mcp.json schema-valid with an EMPTY server map -- see below
|
|
79
|
+
skills/ unchanged; the four SKILL.md files were already the portable form
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`scripts/plugin-manifest-check.mjs` validates both, `pre-flight` gained a fourteenth check, and
|
|
83
|
+
`test/agent-plugins.test.mjs` adds fourteen assertions across both manifests and the skills.
|
|
84
|
+
|
|
85
|
+
### The schemas were read, and the constraints are written out rather than fetched
|
|
86
|
+
|
|
87
|
+
`https://agent-plugins.org/schemas/1.0.0/plugin.schema.json` and `.../mcp.schema.json` are both live, and the
|
|
88
|
+
rules come from them — including the `name` pattern `^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$`,
|
|
89
|
+
**which refuses `--` and `..` and which I would not have guessed.** They are **not fetched at check time**:
|
|
90
|
+
a validator that needs the network cannot run offline, and one that skips itself when the fetch fails reports
|
|
91
|
+
success for the case it could not measure.
|
|
92
|
+
|
|
93
|
+
**What this cannot check, stated rather than implied:** the schema's own description says *"the Agent Plugins
|
|
94
|
+
specification defines additional semantic and operational requirements."* Path containment, symlink
|
|
95
|
+
resolution, MCP transport support and the `PLUGIN_ROOT`/`PLUGIN_DATA` expansion rules are **not** expressible
|
|
96
|
+
here and **are not claimed**.
|
|
97
|
+
|
|
98
|
+
### `mcp.json` ships with zero servers, and that is the honest version
|
|
99
|
+
|
|
100
|
+
The request specified MCP as the unified calling protocol. **DSH has no MCP client.** Measured across all
|
|
101
|
+
8543 small code and config files in the application bundle: `plugin.json` 0, `mcp.json` 0, `agent-plugins` 0,
|
|
102
|
+
**and `mcpServers` 0 occurrences in host-owned code** — the 202 hits are third-party SDKs inside
|
|
103
|
+
`node_modules`, which support MCP without this host reading a config for it.
|
|
104
|
+
|
|
105
|
+
**And there is no MCP server in this package.** So the file is schema-valid with an empty map: a portable
|
|
106
|
+
client reads it and starts nothing, which is true. **The alternative — declaring a server that does not
|
|
107
|
+
exist — is a manifest that launches a failure**, and shipping that to satisfy a specification would be the
|
|
108
|
+
same mistake as advertising a capability the code lacks.
|
|
109
|
+
|
|
110
|
+
### The one rule that is not from a schema caught me immediately
|
|
111
|
+
|
|
112
|
+
`plugin.json`'s `version` must equal `package.json`'s. **On the validator's first run it failed: the manifest
|
|
113
|
+
said 0.52.0 and `package.json` still said 0.51.0.** That is the whole justification for the rule — DSH loads
|
|
114
|
+
plugins through `package.json#dsh` and a Cordis bundle patch, **so nothing in this host ever reads
|
|
115
|
+
`plugin.json`.** A manifest with no consumer has no way to notice it is stale.
|
|
116
|
+
|
|
117
|
+
**A file nobody reads is a file that cannot be wrong.** The schema check and the drift check are the only
|
|
118
|
+
things standing between this manifest and quiet decay.
|
|
119
|
+
|
|
120
|
+
### Four assertions that were wrong, mine
|
|
121
|
+
|
|
122
|
+
* **`frontmatter('---\nname: a\n')` returns `null`, not `{ data: undefined }`** — an unclosed block is not
|
|
123
|
+
frontmatter, and my assertion demanded the shape the code does not return. **The code was right.**
|
|
124
|
+
* **I typed `disableModelInvoction`** in the rejected-spelling set. **A misspelled name in a rejection list
|
|
125
|
+
is a name that is not rejected** — the exact failure the list exists to prevent.
|
|
126
|
+
* **I exported a contract object above the constants it read**, so importing the module threw
|
|
127
|
+
`Cannot access 'PLUGIN_FIELDS' before initialization`.
|
|
128
|
+
* **I wrote the `implementations` Map entry as `['name']: () => …`** inside an array of pairs, matching an
|
|
129
|
+
anchor line without reading the structure around it. **This is the third time in this project that an
|
|
130
|
+
edit matched an anchor string instead of the block containing it.**
|
|
131
|
+
|
|
132
|
+
Both validators now export their rules and keep the CLI in `main()`, so **the suite asserts them without
|
|
133
|
+
spawning a child** — a test that needs a subprocess is skipped wherever piped stdio is refused, which is
|
|
134
|
+
exactly where a check must still run.
|
|
135
|
+
|
|
136
|
+
## [0.51.0] — 2026-10-15
|
|
137
|
+
|
|
138
|
+
**Four Agent Skills, written against the loader's real contract — and a gate that makes the contract matter.**
|
|
139
|
+
|
|
140
|
+
### The four skills
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
skills/
|
|
144
|
+
├── fleet-control/SKILL.md the four modes, the nine verdicts, every refusal code
|
|
145
|
+
├── memory-sync/SKILL.md field-level acceptance, omission versus null, ancestry
|
|
146
|
+
├── session-ref/SKILL.md the dsh-session: codec, its three checks, its silent trap
|
|
147
|
+
└── resource-schedule/SKILL.md lease states, the ~50s ceiling, what "expired" means
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Every rule in them was measured before it was written.** The refusal codes are the ones this session
|
|
151
|
+
actually produced, the ~50-second ceiling is one that was hit more than eight times, and the `dsh-session:`
|
|
152
|
+
section describes a codec whose three checks were read out of `@deepseek-ai/dsh-session-reference` — **including
|
|
153
|
+
the one that surprises people, where a payload that decodes correctly is still refused for not being
|
|
154
|
+
*canonical*.**
|
|
155
|
+
|
|
156
|
+
### The contract came from the loader, not from a specification
|
|
157
|
+
|
|
158
|
+
`parseSkillFile` reads **`name`** and **`description`**, tests the name against
|
|
159
|
+
`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, and **rejects three renamed spellings outright** —
|
|
160
|
+
`disableModelInvocation`, `modelInvocable`, `userInvocable` — in favour of the hyphenated forms. On any
|
|
161
|
+
failure it logs a warning and **returns**.
|
|
162
|
+
|
|
163
|
+
**That is the whole reason this ships with a gate.** A skill that violates the contract is not an error: it
|
|
164
|
+
simply never appears, the directory still looks shipped, and nothing is red. **It is a file whose only symptom
|
|
165
|
+
of being wrong is not being there.**
|
|
166
|
+
|
|
167
|
+
### New check: `agent skills`, and the twelfth became the thirteenth
|
|
168
|
+
|
|
169
|
+
`scripts/skill-check.mjs` mirrors the loader's rules, and `pre-flight` now runs it.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
PASS agent skills (frontmatter satisfies the loader, so no skill is silently dropped): 4 skill(s) satisfy the loader contract
|
|
173
|
+
pre-flight: 13 pass, 0 fail, 1 unverifiable (13/13 registered check(s) reported) -- GREEN
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
It is bound in **three** places — `DECLARED_CHECKS`, `GATE_INVENTORY` (five artifact rows: the validator and
|
|
177
|
+
the four `SKILL.md` files), and the implementation map — because this repository's own rule is that a
|
|
178
|
+
declared check with no artifact, or an artifact with no check, is red in both directions.
|
|
179
|
+
|
|
180
|
+
### Two mistakes of mine, both worth keeping visible
|
|
181
|
+
|
|
182
|
+
**The check reported `exit null` and nothing else.** The cause was `result.error` — an `EPERM` from
|
|
183
|
+
`spawnSync` under a `workspace-write` sandbox that had replaced the `danger-full-access` of earlier runs. **The
|
|
184
|
+
code was right and the environment was refusing.** But the diagnosis was bad on its own terms: **a check that
|
|
185
|
+
prints an exit status and drops the reason names nothing**, so the failure now says
|
|
186
|
+
`EPERM: spawnSync …` and the next reader does not have to rediscover the sandbox boundary.
|
|
187
|
+
|
|
188
|
+
**I edited the `implementations` Map into invalid syntax** — `['name']: () => …` inside an array of pairs —
|
|
189
|
+
because I matched an anchor line without reading the structure it sat in. **This is the same error shape as
|
|
190
|
+
the `accept_block` branch in 0.41.0, and it is twice now.**
|
|
191
|
+
|
|
192
|
+
**The skills ship** — `skills` is in `package.json#files` beside `docs`, so the four `SKILL.md` files are in
|
|
193
|
+
the tarball. **They load only when a host points `DSH_BUNDLED_SKILL_DIR` at the directory**, which is a host
|
|
194
|
+
setting rather than something the package can assert; the alternative roots are `<project>/.dsh/skills`,
|
|
195
|
+
`<project>/.agents/skills`, `<dshHome>/skills` and `~/.agents/skills`.
|
|
196
|
+
|
|
7
197
|
## [0.50.0] — 2026-10-15
|
|
8
198
|
|
|
9
199
|
**The matrix gains the two defects and the WeChat path — and the counts are read from the table, not typed.**
|
package/mcp.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.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",
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
"./package.json": "./package.json"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
+
"mcp.json",
|
|
23
|
+
"plugin.json",
|
|
22
24
|
"lib",
|
|
23
25
|
"src",
|
|
24
26
|
"bin",
|
|
25
27
|
"deploy",
|
|
26
28
|
"docs",
|
|
29
|
+
"skills",
|
|
27
30
|
"cordis.patch.yml",
|
|
28
31
|
"README.md",
|
|
29
32
|
"CHANGELOG.md",
|
|
@@ -113,13 +116,13 @@
|
|
|
113
116
|
}
|
|
114
117
|
},
|
|
115
118
|
"scripts": {
|
|
116
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.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/council-session.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/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.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",
|
|
119
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.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/council-session.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/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.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/agent-plugins.test.mjs test/memory-layer.test.mjs test/chatops-fleet-join.test.mjs",
|
|
117
120
|
"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
121
|
"test:relay": "node --test --test-force-exit test/relay.test.mjs",
|
|
119
122
|
"test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
|
|
120
123
|
"test:tools": "node --test --test-force-exit test/tools.test.mjs",
|
|
121
124
|
"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/plan-sec-cmd.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/council-session.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/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.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",
|
|
125
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.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/council-session.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/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.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/agent-plugins.test.mjs test/memory-layer.test.mjs test/chatops-fleet-join.test.mjs",
|
|
123
126
|
"pack": "node scripts/pack.mjs",
|
|
124
127
|
"verify:join": "node scripts/verify-join.mjs",
|
|
125
128
|
"lint": "eslint .",
|
package/plugin.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "twinsearth-w2m-dsh",
|
|
4
|
+
"version": "0.53.0",
|
|
5
|
+
"description": "Cross-platform fleet execution between Windows and macOS: one DeepSeek Harness account, a Rabbit relay, and signed device identity. Ships four Agent Skills describing fleet dispatch, field-level memory acceptance, session references, and resource scheduling.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"agent-skills",
|
|
9
|
+
"fleet",
|
|
10
|
+
"multi-machine",
|
|
11
|
+
"w2m",
|
|
12
|
+
"dsh"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fleet-control
|
|
3
|
+
description: Use when dispatching a command to the W2M fleet, choosing between replicate, split, broadcast and pipeline, interpreting a verdict's aggregate state or a per-machine refusal_reason, or diagnosing why a task was partial instead of consistent. Covers the four modes, the anchors that decide comparability, and every refusal code a dispatch can return.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fleet control
|
|
7
|
+
|
|
8
|
+
Dispatch one command to many machines and read the verdict honestly. The tools are `w2m_run`, `w2m_wait`,
|
|
9
|
+
`w2m_report`, `w2m_devices`, `w2m_stats`, `w2m_history`, `w2m_status`.
|
|
10
|
+
|
|
11
|
+
## The relay carries one exit code and no output
|
|
12
|
+
|
|
13
|
+
**This is the single fact that changes how you read every result.** A result envelope carries
|
|
14
|
+
`exit_code`, `stdout_sha256`, `stdout_bytes`, `stdout_normalized_sha256`, `stderr_sha256`, and `artifacts` —
|
|
15
|
+
**never stdout itself.** So:
|
|
16
|
+
|
|
17
|
+
- You can compare two machines byte-for-byte and you cannot read what either printed.
|
|
18
|
+
- To get a value back, encode it in the **exit code**. One bit per question, eight bits per exit code.
|
|
19
|
+
- To get bytes back, write a file and declare it in `artifacts`.
|
|
20
|
+
|
|
21
|
+
Do not write a task that prints its answer and expect to read it. **The verdict is the product.**
|
|
22
|
+
|
|
23
|
+
## Pick the mode by what you mean, not by how many machines answer
|
|
24
|
+
|
|
25
|
+
| Mode | Meaning | `index_total` |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `replicate` | every machine runs the whole command, results are compared | must be 1 |
|
|
28
|
+
| `split` | each machine takes its share by index | the number of slices |
|
|
29
|
+
| `broadcast` | **one** machine runs it; the rest are told the outcome and never run anything | 1 |
|
|
30
|
+
| `pipeline` | every machine runs the whole chain in order, stopping at the first failing stage | 1 |
|
|
31
|
+
|
|
32
|
+
`broadcast` needs `executor_machine_id`. Without it the first machine the relay lists is chosen
|
|
33
|
+
deterministically — **so the same task record describes the same execution twice**, which is the point.
|
|
34
|
+
|
|
35
|
+
**`broadcast`'s observers are recorded as `observing` and are never counted as having verified the result.**
|
|
36
|
+
If you want N machines to agree, you want `replicate`, not `broadcast`.
|
|
37
|
+
|
|
38
|
+
**A single command is wrapped as a one-stage chain and run through the pipeline loop.** There is exactly one
|
|
39
|
+
spawn path, so a mode-specific claim about `cwd` or argv must hold for the chain form too.
|
|
40
|
+
|
|
41
|
+
## Read the verdict before you act on it
|
|
42
|
+
|
|
43
|
+
Nine states, and **the difference between two of them is the difference between "it ran" and "it never
|
|
44
|
+
started"**:
|
|
45
|
+
|
|
46
|
+
| State | Means |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `consistent` | every machine that answered agreed |
|
|
49
|
+
| `divergent` | machines answered, and their comparable fields differ |
|
|
50
|
+
| `divergent-platform` | **the differences are fully explained by OS/arch** — not a defect |
|
|
51
|
+
| `partial` | some machines answered and others did not |
|
|
52
|
+
| `failed` | a machine ran the command and it returned non-zero |
|
|
53
|
+
| `unverifiable` | the comparison could not be made |
|
|
54
|
+
| `timeout` | the wait elapsed |
|
|
55
|
+
| `cancelled` | an operator cancelled it |
|
|
56
|
+
| `degraded` | the relay reports a reduced condition |
|
|
57
|
+
|
|
58
|
+
**`exit_code: null` with `lease_state: expired` means the machine never ran it.** `exit_code` non-zero with
|
|
59
|
+
`lease_state: done` means it ran and failed. **A non-zero exit is a result; an expiry is an absence.**
|
|
60
|
+
|
|
61
|
+
## Refusals are values, not exceptions
|
|
62
|
+
|
|
63
|
+
A refusal is a machine declining before execution, and it is reported per machine:
|
|
64
|
+
|
|
65
|
+
| `refusal_reason` | Means | Your move |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `COMMAND_NOT_ALLOWED` | the entry is not in **that machine's** allow-list | check the allow-list; do not retry blindly |
|
|
68
|
+
| `COMMAND_REPARSING_ARGUMENT` | an argument would make the interpreter load code | name the flag in an entry of its own, or stop |
|
|
69
|
+
| `COMMAND_EXECUTABLE_PATH_MISMATCH` | `argv[0]` is not the executable that entry names | call the entry by the name it declares |
|
|
70
|
+
| `COMMAND_EXECUTABLE_RELATIVE_PATH` | `argv[0]` is a relative path | it names a different file in every directory, which is why it is refused |
|
|
71
|
+
| `CWD_OUTSIDE_PROJECT` | `cwd_rel` escapes the project | pass `cwd_outside_project: true` **only** when that is intended |
|
|
72
|
+
| `PATH_INVALID` | no such cwd | **A missing directory is reported as a missing executable** — read the detail, not just the name |
|
|
73
|
+
|
|
74
|
+
**Allow-lists are per machine.** The same command can be allowed on one host and refused on another in the
|
|
75
|
+
same task, and that is a `partial` verdict rather than a bug.
|
|
76
|
+
|
|
77
|
+
## Anchors decide whether the comparison was meaningful
|
|
78
|
+
|
|
79
|
+
`base_commit`, `base_tree`, and `command_hash` are computed from the dispatcher's working tree; each machine
|
|
80
|
+
reports its own `pre_tree_fingerprint`. **When the dispatcher's own tree cannot be fingerprinted, no anchor is
|
|
81
|
+
declared and the note says so** — agreement is then agreement *with each other*, not with a named tree.
|
|
82
|
+
|
|
83
|
+
**Read that note.** A `consistent` verdict with no anchor is weaker than it looks, and `w2m_report` states
|
|
84
|
+
which case you are in.
|
|
85
|
+
|
|
86
|
+
## Before dispatching
|
|
87
|
+
|
|
88
|
+
1. `w2m_devices` — who is actually online. **A machine listed may still refuse the next command.**
|
|
89
|
+
2. `w2m_stats` — the cheap shape of the fleet, and whether the relay restarted.
|
|
90
|
+
3. Choose the mode from the *meaning*, not the machine count.
|
|
91
|
+
4. After the wait, read `counts`, `notes`, and every `refusal_reason` **before** reading the aggregate state.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-sync
|
|
3
|
+
description: Use when writing or reading a block through w2m_memory, when a field was omitted rather than nulled, when a block names an ancestor the caller cannot produce, or when deciding whether a fact belongs in durable memory at all. Covers field-level acceptance, the per-actor cost of each field, and the refusal codes that mean "kept, but not for you".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory sync
|
|
7
|
+
|
|
8
|
+
`w2m_memory` stores blocks that carry **provenance**, and it decides field by field what each reader may see.
|
|
9
|
+
The tool is small; the rules are the part that matters.
|
|
10
|
+
|
|
11
|
+
## Acceptance is per field, and omission is not null
|
|
12
|
+
|
|
13
|
+
A block is a set of named fields. When an actor's authority cannot cover a field, **the field is absent from
|
|
14
|
+
what they receive — it is not present-with-`null`, and it is not an error.**
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
accepted = [subject, body] refused = [evidence]
|
|
18
|
+
hasOwnProperty('evidence') === false
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Test for the key, not for a falsy value.** `if (!block.evidence)` cannot tell "omitted" from "empty", and
|
|
22
|
+
those mean different things: one is a permission decision, the other is content.
|
|
23
|
+
|
|
24
|
+
**Never reconstruct a refused field.** If you need it, the answer is a higher-authority actor or a different
|
|
25
|
+
block, not a guess.
|
|
26
|
+
|
|
27
|
+
## A block cannot name an ancestor its caller cannot produce
|
|
28
|
+
|
|
29
|
+
Providing a block whose `parent` is not in the caller's own reachable set is refused as `BLOCK_MISSING_PARENT`,
|
|
30
|
+
and **the block is rejected rather than re-rooted.**
|
|
31
|
+
|
|
32
|
+
**This is deliberate.** Silently re-rooting would make the ancestry claim unfalsifiable — every block would
|
|
33
|
+
"fit" somewhere, and the chain would stop carrying information. When you hit this refusal, supply the missing
|
|
34
|
+
ancestor or drop the claim; do not look for a flag that relaxes it.
|
|
35
|
+
|
|
36
|
+
## Cost is a property of the field, not of the request
|
|
37
|
+
|
|
38
|
+
Authority bands, and what they reach:
|
|
39
|
+
|
|
40
|
+
| Band | Range | Reaches |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `ordinary` | 0–49 | the low-cost fields |
|
|
43
|
+
| `captain` | 50–69 | more, including some evidence |
|
|
44
|
+
| `org_admin` | 70–89 | `evidence` and above |
|
|
45
|
+
| `system_admin` | 90+ | everything |
|
|
46
|
+
|
|
47
|
+
**A field's cost is declared where the field is declared**, and the acceptance decision is made against the
|
|
48
|
+
actor's level at write and at read. So the same block read by two actors is two different objects, and that is
|
|
49
|
+
the feature rather than a leak.
|
|
50
|
+
|
|
51
|
+
Do not design a workflow that depends on a high-authority actor being present to make a low-authority read
|
|
52
|
+
work. **Model the read you actually need.**
|
|
53
|
+
|
|
54
|
+
## Writing: say what you can prove
|
|
55
|
+
|
|
56
|
+
- **Name the ancestor only if you hold it.** A parent you cannot produce fails the whole block.
|
|
57
|
+
- **Put the load-bearing fact in the cheapest field that can carry it.** A fact in `evidence` is invisible to
|
|
58
|
+
everyone below `org_admin`, so a block whose meaning lives in `evidence` means nothing to most readers.
|
|
59
|
+
- **One block, one claim.** Ancestry is how blocks relate; packing two claims into one block makes each
|
|
60
|
+
unreadable to whoever may see only one of them.
|
|
61
|
+
- **Expect omission to be silent.** Nothing in the result says "a field was hidden from you" beyond the field
|
|
62
|
+
being absent, so a reader who needs a field must check for it.
|
|
63
|
+
|
|
64
|
+
## Before you write
|
|
65
|
+
|
|
66
|
+
1. What is the **cheapest field** that can carry this fact?
|
|
67
|
+
2. Do you **hold the parent** you are about to name?
|
|
68
|
+
3. Who needs to read this, and **what band do they hold**? If the answer is "everyone", the fact does not
|
|
69
|
+
belong in a costly field.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resource-schedule
|
|
3
|
+
description: Use when deciding how to consume the fleet's capacity across machines, when a task came back partial because a machine was offline, when a lease expired instead of failing, or when a long command needs a timeout that fits inside the agent's execution ceiling. Covers lease states, the ~50s agent ceiling, w2m_update's schedule, and why an expiry is not a failure.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Resource scheduling
|
|
7
|
+
|
|
8
|
+
The fleet is a small pool of heterogeneous machines, and most disappointing results come from treating it as
|
|
9
|
+
a queue of identical workers.
|
|
10
|
+
|
|
11
|
+
## Lease states are the scheduling vocabulary
|
|
12
|
+
|
|
13
|
+
| State | Means | Retry? |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `queued` | handed to the machine | — |
|
|
16
|
+
| `offered` | published, not yet taken | — |
|
|
17
|
+
| `done` | **the command ran.** `exit_code` is meaningful, including non-zero | no |
|
|
18
|
+
| `refused` | the machine declined before running; read `refusal_reason` | only after fixing the cause |
|
|
19
|
+
| `expired` | **nobody ran it.** `exit_code` is `null` | yes — the machine was not there |
|
|
20
|
+
| `observing` | `broadcast` chose another executor; this machine was told, not asked | no, by design |
|
|
21
|
+
|
|
22
|
+
**`expired` and `failed` are different findings with different responses.** An expiry says the fleet lacked
|
|
23
|
+
capacity; a failure says the command was wrong. Aggregating them into "it didn't work" loses the only
|
|
24
|
+
information that decides what to do next.
|
|
25
|
+
|
|
26
|
+
## The ~50 second ceiling, measured repeatedly
|
|
27
|
+
|
|
28
|
+
**An agent-side execution has a ceiling near 50 seconds, and it has been hit more than eight times in a single
|
|
29
|
+
working session.** Symptoms are an expiry rather than an error, so it reads as a missing machine.
|
|
30
|
+
|
|
31
|
+
Consequences for scheduling:
|
|
32
|
+
|
|
33
|
+
- **Keep a dispatched command under the ceiling**, or expect the lease to expire and the task to come back
|
|
34
|
+
`partial`.
|
|
35
|
+
- `--timeout` and `timeout_ms` bound **the whole pipeline chain, not each stage.** Three 20-second stages
|
|
36
|
+
inside a 50-second timeout will be cut off at the third.
|
|
37
|
+
- **A long job belongs on one machine with a written artifact, not replicated.** Replicating a job that
|
|
38
|
+
outlives the ceiling multiplies the expiries by the machine count.
|
|
39
|
+
|
|
40
|
+
## Offline is the normal case, not an error
|
|
41
|
+
|
|
42
|
+
`w2m_devices` lists machines the relay holds a lease for; `w2m_stats` reports how many are online.
|
|
43
|
+
**These two have disagreed in practice** — a machine can be listed and answer `silent`. Treat "online" as
|
|
44
|
+
"worth trying", and read the per-machine outcome afterwards.
|
|
45
|
+
|
|
46
|
+
**Partial is therefore the expected verdict for any replicated task on a fleet with offline members**, and it
|
|
47
|
+
is not a signal that the command was wrong. Check `counts.expired` before concluding anything about the code.
|
|
48
|
+
|
|
49
|
+
## Time is not the only scarce resource
|
|
50
|
+
|
|
51
|
+
- **One writable lease at a time** is available via `require_exclusive_write`. Two machines writing the same
|
|
52
|
+
paths without it is a race the relay will not prevent for you.
|
|
53
|
+
- **`write_scope` is project-relative prefixes.** A write outside the declared scope is not made, so a task
|
|
54
|
+
that needs to write must declare where.
|
|
55
|
+
- **Read-only is the default.** A dispatch that must not modify anything needs no flags — that is the default,
|
|
56
|
+
and `write: true` is the deliberate opt-in.
|
|
57
|
+
|
|
58
|
+
## Scheduled work in this system
|
|
59
|
+
|
|
60
|
+
- **Update checks run at 00:00, 03:00 and 05:00 Beijing time**, and a new version is installed **only after
|
|
61
|
+
verifying the hash published with it**. A check that cannot reach the network comes back as a **recorded
|
|
62
|
+
error**, which is deliberately different from "already up to date" — **do not read a negative result as a
|
|
63
|
+
positive one.**
|
|
64
|
+
- **An install takes effect at the next start.** Nothing hot-swaps, and `w2m_status` reports
|
|
65
|
+
`restart_required` so the deferral is visible rather than silent.
|
|
66
|
+
|
|
67
|
+
## Scheduling checklist
|
|
68
|
+
|
|
69
|
+
1. `w2m_devices` or `w2m_stats` — **how much capacity actually exists right now.**
|
|
70
|
+
2. Estimate the runtime against **the ~50s ceiling**, and reshape the work if it exceeds it.
|
|
71
|
+
3. Choose `replicate` only when you need agreement; otherwise pick the smallest set that answers the question.
|
|
72
|
+
4. After the wait, read `counts` first: `expired` is capacity, `failed` is the command, `refused` is policy.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-ref
|
|
3
|
+
description: Use when a session must be named across tools, when a w2m tool takes a session_id, when constructing or parsing a dsh-session: URI, or when the error "invalid session reference URI" appears. Covers the canonical encoding, the three checks a URI must pass, and which per-session state the W2M plugin keys by session id.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session references
|
|
7
|
+
|
|
8
|
+
Several things in this system are **per session**, and naming a session is where they break when a caller
|
|
9
|
+
guesses at the format.
|
|
10
|
+
|
|
11
|
+
## The canonical URI is not the session id with a prefix
|
|
12
|
+
|
|
13
|
+
`dsh-session:` carries a **base64url-encoded JSON string**, not the raw id:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
dsh-session: + base64url(JSON.stringify(sessionId))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**A DSH session id is a UUID** (36 characters), so the encoded form is its JSON string quoted and re-encoded —
|
|
20
|
+
**it looks nothing like the id.** Writing `dsh-session:` + the raw UUID produces a URI that fails decoding.
|
|
21
|
+
|
|
22
|
+
To build one, call `formatSessionReferenceMention({ sessionId, label })` from
|
|
23
|
+
`@deepseek-ai/dsh-session-reference`. **Never concatenate the scheme by hand.**
|
|
24
|
+
|
|
25
|
+
## Three checks, and the error names none of them
|
|
26
|
+
|
|
27
|
+
`decodeSessionReferenceUri` rejects a URI that fails any of:
|
|
28
|
+
|
|
29
|
+
1. it does not start with `dsh-session:`;
|
|
30
|
+
2. the payload is not `/^[A-Za-z0-9_-]+$/`;
|
|
31
|
+
3. the payload does not decode to a **JSON string**, or **re-encoding it does not reproduce the URI
|
|
32
|
+
byte-for-byte**.
|
|
33
|
+
|
|
34
|
+
All three throw the same `invalid session reference URI "<uri>"` with code
|
|
35
|
+
`SESSION_REFERENCE_INVALID_REFERENCE`. **The message names the URI and never the check**, so diagnose by
|
|
36
|
+
re-encoding: take the decoded value, encode it again, and compare.
|
|
37
|
+
|
|
38
|
+
**Check 3 is the one that surprises people.** It requires the *canonical* encoding, not merely a decodable
|
|
39
|
+
one — base64url has several spellings for the same bytes, and a non-canonical one is refused even though it
|
|
40
|
+
decodes correctly.
|
|
41
|
+
|
|
42
|
+
## Bare URIs are extracted too, and that is a real trap
|
|
43
|
+
|
|
44
|
+
The text pattern matches two forms:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
@[label](dsh-session:payload) an explicit Markdown mention -- fails on ANY malformed URI
|
|
48
|
+
dsh-session:payload a bare URI -- taken as a reference when the payload is base64url-shaped
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**So a `dsh-session:`-looking string inside ordinary prose is parsed as a reference**, and in an explicit
|
|
52
|
+
mention a malformed URI is a hard failure rather than a fallback to text. **If you are quoting a URI —
|
|
53
|
+
in a changelog, a bug report, an example — quote it in a fenced code block or break the scheme**, or a parser
|
|
54
|
+
downstream will try to resolve it.
|
|
55
|
+
|
|
56
|
+
## Per-session state in this plugin
|
|
57
|
+
|
|
58
|
+
`w2m_run` accepts an optional `session_id`, and the fleet-target store keys the session's chosen machine by it:
|
|
59
|
+
|
|
60
|
+
- **The bucket is the session id**, so one chat's target cannot silently steer another chat's dispatch.
|
|
61
|
+
- **A dispatch with no session in reach falls back to a single `(no-session)` bucket** rather than throwing —
|
|
62
|
+
so a bare `execute()` in a test and a future non-session caller share one bucket instead of two.
|
|
63
|
+
- `session_id` is capped at **200** characters, which exists to bound the number of buckets, not the bytes.
|
|
64
|
+
A DSH session id is 36 characters, so the cap is room to spare rather than a limit you should approach.
|
|
65
|
+
|
|
66
|
+
**Pass the id you actually hold.** A fabricated id creates a bucket nothing else will ever read, which looks
|
|
67
|
+
like a target that silently fails to apply.
|
|
68
|
+
|
|
69
|
+
## Diagnosing an invalid-reference error
|
|
70
|
+
|
|
71
|
+
1. **Find where the URI was built.** If it was `'dsh-session:' + id`, that is the bug — build it with the
|
|
72
|
+
codec instead.
|
|
73
|
+
2. **Re-encode and compare.** Decode the payload, `JSON.stringify` it, `base64url` it, and compare to the
|
|
74
|
+
original. A difference means check 3.
|
|
75
|
+
3. **Look for prose.** A URI quoted in a message body is extracted as a bare reference, so the failure may be
|
|
76
|
+
nowhere near the code that handles sessions.
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
|
|
|
208
208
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
209
209
|
* overrides it at runtime for tests and packagers.
|
|
210
210
|
*/
|
|
211
|
-
const PLUGIN_VERSION = '0.
|
|
211
|
+
const PLUGIN_VERSION = '0.53.0';
|
|
212
212
|
|
|
213
213
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
214
214
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|