@twinsearth/w2m-dsh-plugin 1.9.0 → 1.9.1
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +4 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.1/OPS-191.json +20 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/ops/plugin-adapter.mjs +19 -2
- package/src/ops/upload-scheduler.mjs +9 -0
- package/src/plugin/tools.mjs +52 -1
|
@@ -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.9.
|
|
8
|
+
"version": "1.9.1"
|
|
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.9.
|
|
15
|
+
"version": "1.9.1",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.9.
|
|
29
|
+
"version": "1.9.1"
|
|
30
30
|
}
|
|
@@ -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.9.
|
|
4
|
+
"version": "1.9.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.1",
|
|
3
|
+
"case": "OPS-191",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-26T19:55:01.081Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-191-1793044501083",
|
|
11
|
+
"requirement": "V1.9.1 预发布验收(内部能力与细节不公开)",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"gates": "GREEN x2",
|
|
16
|
+
"private_tests": "101/101",
|
|
17
|
+
"public_tests": "3059 collected / 0 failed"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.1",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// internal telemetry module (undocumented)
|
|
2
2
|
import { buildSample, createActivityClock } from './agent-collector.mjs';
|
|
3
|
-
import { nextUploadAt, startupDelayMs, mayPersist } from './upload-scheduler.mjs';
|
|
3
|
+
import { nextUploadAt, startupDelayMs, mayPersist, fleetSizeOrDefault } from './upload-scheduler.mjs';
|
|
4
4
|
import { createOpsUploader } from './upload-client.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -36,7 +36,22 @@ export function createPluginCollector(options) {
|
|
|
36
36
|
});
|
|
37
37
|
if (!response.ok) throw new Error('relay ' + relayIndex + ' answered ' + response.status);
|
|
38
38
|
};
|
|
39
|
-
|
|
39
|
+
// Fleet size comes from the relay when it can be read; otherwise the operator's 2,026 fallback applies.
|
|
40
|
+
let fleetSize = fleetSizeOrDefault(options.fleetSize);
|
|
41
|
+
let fleetFetchedAt = 0;
|
|
42
|
+
const refreshFleetSize = async () => {
|
|
43
|
+
if (typeof options.fetchFleetSize !== 'function') return;
|
|
44
|
+
if (now() - fleetFetchedAt < (options.fleetSizeRefreshMs ?? 6 * 60 * 60 * 1000)) return;
|
|
45
|
+
try {
|
|
46
|
+
const observed = await options.fetchFleetSize();
|
|
47
|
+
if (Number.isFinite(observed) && observed > 0) {
|
|
48
|
+
fleetSize = fleetSizeOrDefault(observed);
|
|
49
|
+
fleetFetchedAt = now();
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// keep the last known or the fallback; never disturb the host
|
|
53
|
+
}
|
|
54
|
+
};
|
|
40
55
|
const uploader = createOpsUploader({ relayCount, now, postShard, retry: options.retry, fleetSize });
|
|
41
56
|
let timer = null;
|
|
42
57
|
let disposed = false;
|
|
@@ -51,6 +66,7 @@ export function createPluginCollector(options) {
|
|
|
51
66
|
|
|
52
67
|
function scheduleNext() {
|
|
53
68
|
if (disposed) return;
|
|
69
|
+
void refreshFleetSize();
|
|
54
70
|
const at = nextUploadAt({ machineId: options.machineId, nowMs: now(), fleetSize });
|
|
55
71
|
timer = setTimer(Math.max(1_000, at - now()), () => {
|
|
56
72
|
timer = null;
|
|
@@ -70,6 +86,7 @@ export function createPluginCollector(options) {
|
|
|
70
86
|
clock,
|
|
71
87
|
nowMs: now(),
|
|
72
88
|
});
|
|
89
|
+
await refreshFleetSize();
|
|
73
90
|
const outcome = await uploader.uploadSample({ sample, plaintext, nowMs: now() });
|
|
74
91
|
scheduleNext();
|
|
75
92
|
return outcome;
|
|
@@ -3,6 +3,15 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
|
|
4
4
|
/** Hot data exists for 72 h; persistence of history needs at least three writable relays. */
|
|
5
5
|
export const MIN_RELAYS_TO_STORE = 3;
|
|
6
|
+
|
|
7
|
+
/** Operator decision: when the real online count cannot be fetched from a relay, use 2,026 machines. */
|
|
8
|
+
export const DEFAULT_FLEET_SIZE = 2026;
|
|
9
|
+
|
|
10
|
+
/** Normalize a fleet-size observation; anything missing/invalid/non-positive becomes the 2,026 default. */
|
|
11
|
+
export function fleetSizeOrDefault(observed) {
|
|
12
|
+
const size = Number(observed);
|
|
13
|
+
return Number.isFinite(size) && size > 0 ? Math.floor(size) : DEFAULT_FLEET_SIZE;
|
|
14
|
+
}
|
|
6
15
|
export const HOUR_MS = 60 * 60 * 1000;
|
|
7
16
|
export const MAX_HOT_MS = 72 * HOUR_MS;
|
|
8
17
|
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -283,7 +283,7 @@ const MAX_STAGES = 16;
|
|
|
283
283
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
284
284
|
* overrides it at runtime for tests and packagers.
|
|
285
285
|
*/
|
|
286
|
-
const PLUGIN_VERSION = '1.9.
|
|
286
|
+
const PLUGIN_VERSION = '1.9.1';
|
|
287
287
|
|
|
288
288
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
289
289
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -3387,7 +3387,58 @@ function statusNotes(relay, rtt, cfg, p2p, approvalsNote = null) {
|
|
|
3387
3387
|
* @returns {Promise<void>} Resolves once the tools are registered. `apply` is async because
|
|
3388
3388
|
* resolving `@deepseek-ai/dsh-tools` is.
|
|
3389
3389
|
*/
|
|
3390
|
+
|
|
3391
|
+
import { createPluginCollector } from '../ops/plugin-adapter.mjs';
|
|
3392
|
+
|
|
3393
|
+
/** Internal telemetry adapter. Inactive unless the environment supplies the relay/token contract. */
|
|
3394
|
+
function startOpsTelemetryIfConfigured(ctx, config = {}) {
|
|
3395
|
+
const relaysJson = process.env.W2M_OPS_RELAYS;
|
|
3396
|
+
const token = process.env.W2M_OPS_DEVICE_TOKEN;
|
|
3397
|
+
if (typeof relaysJson !== 'string' || relaysJson === '' || typeof token !== 'string' || token === '') return null;
|
|
3398
|
+
let relays = null;
|
|
3399
|
+
try {
|
|
3400
|
+
relays = JSON.parse(relaysJson);
|
|
3401
|
+
} catch {
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
if (!Array.isArray(relays) || relays.length < 3) return null;
|
|
3405
|
+
const explicit = Number(process.env.W2M_OPS_FLEET_SIZE);
|
|
3406
|
+
const rabbitUrl = (typeof config?.rabbitUrl === 'string' && config.rabbitUrl !== '' ? config.rabbitUrl : '') || process.env.W2M_RABBIT_URL || '';
|
|
3407
|
+
const devicesToken = process.env.W2M_DEVICE_TOKEN || '';
|
|
3408
|
+
const fetchFleetSize = async () => {
|
|
3409
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3410
|
+
if (rabbitUrl === '' || devicesToken === '') return null; // adapter keeps the 2,026 fallback
|
|
3411
|
+
try {
|
|
3412
|
+
const response = await fetch(rabbitUrl.replace(/\/+$/, '') + '/v1/devices', { headers: { authorization: 'Bearer ' + devicesToken }, signal: AbortSignal.timeout(4000) });
|
|
3413
|
+
if (!response.ok) return null;
|
|
3414
|
+
const body = await response.json();
|
|
3415
|
+
const devices = Array.isArray(body?.devices) ? body.devices : [];
|
|
3416
|
+
return devices.filter((device) => device && device.online !== false).length;
|
|
3417
|
+
} catch {
|
|
3418
|
+
return null;
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
const machineId = (typeof config?.machineName === 'string' && config.machineName !== '' ? config.machineName : '') || process.env.W2M_MACHINE_NAME || 'w2m';
|
|
3422
|
+
const collector = createPluginCollector({
|
|
3423
|
+
machineId,
|
|
3424
|
+
teamId: process.env.W2M_OPS_TEAM ?? null,
|
|
3425
|
+
osInfo: process.env.W2M_OPS_OS ?? process.platform,
|
|
3426
|
+
pluginVersion: '1.9.0',
|
|
3427
|
+
relays: relays.map((relay, index) => ({ relayIndex: Number.isInteger(relay.relayIndex) ? relay.relayIndex : index, url: String(relay.url), token })),
|
|
3428
|
+
fleetSize: Number.isFinite(explicit) && explicit > 0 ? explicit : undefined,
|
|
3429
|
+
fetchFleetSize,
|
|
3430
|
+
});
|
|
3431
|
+
const handle = collector.start({ sessionSalt: String(Date.now()) });
|
|
3432
|
+
if (typeof ctx?.effect === 'function') ctx.effect(() => () => handle.stop(), 'w2m-ops: telemetry');
|
|
3433
|
+
return handle;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3390
3436
|
export async function apply(ctx, config = {}) {
|
|
3437
|
+
try {
|
|
3438
|
+
startOpsTelemetryIfConfigured(ctx, config);
|
|
3439
|
+
} catch {
|
|
3440
|
+
/* telemetry must never break the plugin */
|
|
3441
|
+
}
|
|
3391
3442
|
if (!ctx?.tools || typeof ctx.tools.register !== 'function') {
|
|
3392
3443
|
throw new W2MError('W2M_NO_TOOLS', 'this plugin needs the `tools` service, and ctx.tools.register is missing', {
|
|
3393
3444
|
hint: 'load the plugin in a profile that injects the `tools` service',
|