viveworker 0.8.0 → 0.8.2
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +141 -0
- package/package.json +7 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
- package/scripts/a2a-executor.mjs +261 -7
- package/scripts/a2a-handler.mjs +88 -0
- package/scripts/a2a-relay-client.mjs +6 -0
- package/scripts/lib/markdown-render.mjs +128 -1
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +1504 -128
- package/scripts/viveworker.mjs +262 -1
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +162 -0
- package/web/app.js +1164 -101
- package/web/build-id.js +1 -0
- package/web/i18n.js +123 -15
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +2 -2
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +16 -6
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
|
@@ -84,6 +84,10 @@ const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
|
84
84
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
85
85
|
const MAX_PRE_CONNECT_BACKOFF_MS = 4_000;
|
|
86
86
|
const RELAY_RESET_RECONNECT_MS = 250;
|
|
87
|
+
const DEFAULT_FAILURE_WINDOW_MS = 60_000;
|
|
88
|
+
const DEFAULT_FAILURE_THRESHOLD = 4;
|
|
89
|
+
const DEFAULT_CIRCUIT_BREAKER_MS = 60_000;
|
|
90
|
+
const DEFAULT_MAX_CIRCUIT_BREAKER_MS = 10 * 60_000;
|
|
87
91
|
const DEFAULT_PROLOGUE = new TextEncoder().encode("viveworker/remote-pairing/v1");
|
|
88
92
|
|
|
89
93
|
// CloseEvent codes we emit. 1000 is normal; 4xxx is application-defined.
|
|
@@ -113,6 +117,10 @@ const CLOSE_RELAY_RESET_SESSION = 4004;
|
|
|
113
117
|
* @property {number} [pingIntervalMs]
|
|
114
118
|
* @property {number[]} [backoffMs]
|
|
115
119
|
* @property {number} [handshakeTimeoutMs]
|
|
120
|
+
* @property {number} [failureWindowMs]
|
|
121
|
+
* @property {number} [failureThreshold]
|
|
122
|
+
* @property {number} [circuitBreakerMs]
|
|
123
|
+
* @property {number} [maxCircuitBreakerMs]
|
|
116
124
|
* @property {typeof WebSocket} [WebSocketImpl] injectable for Node-side tests
|
|
117
125
|
* @property {{debug?: Function, warn?: Function}} [logger]
|
|
118
126
|
*/
|
|
@@ -154,6 +162,10 @@ export class RemotePairingTransport {
|
|
|
154
162
|
this._backoffMs = (opts.backoffMs ?? DEFAULT_BACKOFF_MS).slice();
|
|
155
163
|
if (this._backoffMs.length === 0) this._backoffMs = [1_000];
|
|
156
164
|
this._handshakeTimeoutMs = opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
165
|
+
this._failureWindowMs = opts.failureWindowMs ?? DEFAULT_FAILURE_WINDOW_MS;
|
|
166
|
+
this._failureThreshold = opts.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
167
|
+
this._circuitBreakerMs = opts.circuitBreakerMs ?? DEFAULT_CIRCUIT_BREAKER_MS;
|
|
168
|
+
this._maxCircuitBreakerMs = opts.maxCircuitBreakerMs ?? DEFAULT_MAX_CIRCUIT_BREAKER_MS;
|
|
157
169
|
|
|
158
170
|
this._WebSocketImpl = opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
159
171
|
if (typeof this._WebSocketImpl !== "function") {
|
|
@@ -188,6 +200,12 @@ export class RemotePairingTransport {
|
|
|
188
200
|
this._pingTimer = null;
|
|
189
201
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
190
202
|
this._handshakeTimer = null;
|
|
203
|
+
/** @type {number[]} recent reconnect-triggering failures. */
|
|
204
|
+
this._recentFailureAtMs = [];
|
|
205
|
+
/** If > Date.now(), reconnect attempts are intentionally delayed. */
|
|
206
|
+
this._circuitOpenUntilMs = 0;
|
|
207
|
+
/** Consecutive circuit openings, used to lengthen persistent outages. */
|
|
208
|
+
this._circuitOpenCount = 0;
|
|
191
209
|
|
|
192
210
|
// ---- crypto state (persists across reconnects until RESUME_FAIL) ----
|
|
193
211
|
/** @type {import("../remote-pairing.bundle.js").NoiseSession | null} */
|
|
@@ -320,6 +338,12 @@ export class RemotePairingTransport {
|
|
|
320
338
|
|
|
321
339
|
_open() {
|
|
322
340
|
this._cancelReconnectTimer();
|
|
341
|
+
const circuitDelay = this._circuitDelayMs();
|
|
342
|
+
if (circuitDelay > 0) {
|
|
343
|
+
this._log.warn?.(`relay reconnect circuit open for ${circuitDelay}ms`);
|
|
344
|
+
this._scheduleReconnect();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
323
347
|
this._setState(STATE.OPENING);
|
|
324
348
|
|
|
325
349
|
const url =
|
|
@@ -484,6 +508,12 @@ export class RemotePairingTransport {
|
|
|
484
508
|
|
|
485
509
|
this._log.debug?.(`ws closed code=${evt?.code} reason=${evt?.reason}`);
|
|
486
510
|
this._setState(STATE.DISCONNECTED, { code: evt?.code, reason: evt?.reason });
|
|
511
|
+
if (Number(evt?.code) !== CLOSE_NORMAL) {
|
|
512
|
+
this._recordReconnectFailure({
|
|
513
|
+
code: Number(evt?.code) || 0,
|
|
514
|
+
reason: String(evt?.reason || ""),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
487
517
|
if (isRelayReset) {
|
|
488
518
|
this._scheduleRelayResetReconnect();
|
|
489
519
|
} else {
|
|
@@ -619,9 +649,10 @@ export class RemotePairingTransport {
|
|
|
619
649
|
const idx = Math.min(this._reconnectAttempt, this._backoffMs.length - 1);
|
|
620
650
|
const waitingForFirstConnect = this._connectPromise != null && this._session == null;
|
|
621
651
|
const rawDelay = this._backoffMs[idx];
|
|
622
|
-
const
|
|
652
|
+
const backoffDelay = waitingForFirstConnect
|
|
623
653
|
? Math.min(rawDelay, MAX_PRE_CONNECT_BACKOFF_MS)
|
|
624
654
|
: rawDelay;
|
|
655
|
+
const delay = Math.max(backoffDelay, this._circuitDelayMs());
|
|
625
656
|
this._reconnectAttempt += 1;
|
|
626
657
|
this._log.debug?.(`reconnect in ${delay}ms (attempt ${this._reconnectAttempt})`);
|
|
627
658
|
this._reconnectTimer = setTimeout(() => {
|
|
@@ -636,7 +667,7 @@ export class RemotePairingTransport {
|
|
|
636
667
|
// rendezvous again. Treat it as a protocol reset, not a network failure,
|
|
637
668
|
// so app boot does not sit behind the exponential backoff ladder.
|
|
638
669
|
this._reconnectAttempt = 0;
|
|
639
|
-
const delay = RELAY_RESET_RECONNECT_MS;
|
|
670
|
+
const delay = Math.max(RELAY_RESET_RECONNECT_MS, this._circuitDelayMs());
|
|
640
671
|
this._log.debug?.(`reconnect in ${delay}ms (relay reset)`);
|
|
641
672
|
this._reconnectTimer = setTimeout(() => {
|
|
642
673
|
this._reconnectTimer = null;
|
|
@@ -713,6 +744,9 @@ export class RemotePairingTransport {
|
|
|
713
744
|
this._state = newState;
|
|
714
745
|
if (newState === STATE.CONNECTED) {
|
|
715
746
|
this._reconnectAttempt = 0;
|
|
747
|
+
this._recentFailureAtMs = [];
|
|
748
|
+
this._circuitOpenUntilMs = 0;
|
|
749
|
+
this._circuitOpenCount = 0;
|
|
716
750
|
}
|
|
717
751
|
try {
|
|
718
752
|
this._onStateChange(newState, prev, info);
|
|
@@ -729,6 +763,10 @@ export class RemotePairingTransport {
|
|
|
729
763
|
}
|
|
730
764
|
|
|
731
765
|
_fail(err) {
|
|
766
|
+
this._recordReconnectFailure({
|
|
767
|
+
code: CLOSE_FATAL,
|
|
768
|
+
reason: err?.message || "fatal-error",
|
|
769
|
+
});
|
|
732
770
|
this._setState(STATE.FAILED, { error: err });
|
|
733
771
|
this._stopPing();
|
|
734
772
|
this._stopHandshakeTimer();
|
|
@@ -748,6 +786,33 @@ export class RemotePairingTransport {
|
|
|
748
786
|
this._session = null;
|
|
749
787
|
this._handshake = null;
|
|
750
788
|
}
|
|
789
|
+
|
|
790
|
+
_recordReconnectFailure(info = {}) {
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const windowMs = Math.max(1_000, Number(this._failureWindowMs) || DEFAULT_FAILURE_WINDOW_MS);
|
|
793
|
+
this._recentFailureAtMs = this._recentFailureAtMs.filter((at) => now - at <= windowMs);
|
|
794
|
+
this._recentFailureAtMs.push(now);
|
|
795
|
+
const threshold = Math.max(2, Math.floor(Number(this._failureThreshold) || DEFAULT_FAILURE_THRESHOLD));
|
|
796
|
+
if (this._recentFailureAtMs.length < threshold) return;
|
|
797
|
+
|
|
798
|
+
const baseCooldownMs = Math.max(1_000, Number(this._circuitBreakerMs) || DEFAULT_CIRCUIT_BREAKER_MS);
|
|
799
|
+
const maxCooldownMs = Math.max(baseCooldownMs, Number(this._maxCircuitBreakerMs) || DEFAULT_MAX_CIRCUIT_BREAKER_MS);
|
|
800
|
+
const multiplier = Math.min(16, 2 ** this._circuitOpenCount);
|
|
801
|
+
const cooldownMs = Math.min(maxCooldownMs, baseCooldownMs * multiplier);
|
|
802
|
+
this._circuitOpenCount += 1;
|
|
803
|
+
this._circuitOpenUntilMs = Math.max(this._circuitOpenUntilMs, now + cooldownMs);
|
|
804
|
+
this._recentFailureAtMs = [];
|
|
805
|
+
this._dropNoiseState();
|
|
806
|
+
this._log.warn?.(
|
|
807
|
+
`relay reconnect circuit opened for ${cooldownMs}ms` +
|
|
808
|
+
`${info?.code ? ` after close=${info.code}` : ""}` +
|
|
809
|
+
`${info?.reason ? ` (${String(info.reason).slice(0, 64)})` : ""}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
_circuitDelayMs() {
|
|
814
|
+
return Math.max(0, this._circuitOpenUntilMs - Date.now());
|
|
815
|
+
}
|
|
751
816
|
}
|
|
752
817
|
|
|
753
818
|
// ---------------------------------------------------------------------------
|
package/web/sw.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
|
|
2
|
+
const CACHE_NAME = `viveworker-${APP_BUILD_ID}`;
|
|
3
3
|
const APP_SCRIPT_URL = `/app.js?v=${APP_BUILD_ID}`;
|
|
4
|
+
const APP_STYLE_URL = `/app.css?v=${APP_BUILD_ID}`;
|
|
4
5
|
const API_ROUTER_URL = `/remote-pairing/api-router.js?v=${APP_BUILD_ID}`;
|
|
5
6
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
6
7
|
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
@@ -14,7 +15,7 @@ const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
|
14
15
|
const APP_ASSETS = [
|
|
15
16
|
"/app",
|
|
16
17
|
APP_SCRIPT_URL,
|
|
17
|
-
|
|
18
|
+
APP_STYLE_URL,
|
|
18
19
|
"/app.js",
|
|
19
20
|
"/i18n.js",
|
|
20
21
|
"/icons/viveworker-v-pulse.svg",
|
|
@@ -31,10 +32,12 @@ const APP_ROUTES = new Set(["/", "/app", "/app/"]);
|
|
|
31
32
|
const CACHED_PATHS = new Set(APP_ASSETS.map((asset) => new URL(asset, self.location.origin).pathname));
|
|
32
33
|
const VERSIONED_CACHE_PATHS = new Set([
|
|
33
34
|
"/app.js",
|
|
35
|
+
"/app.css",
|
|
34
36
|
"/remote-pairing/api-router.js",
|
|
35
37
|
]);
|
|
36
38
|
const NETWORK_FIRST_PATHS = new Set([
|
|
37
39
|
"/app.js",
|
|
40
|
+
"/app.css",
|
|
38
41
|
"/remote-pairing/api-router.js",
|
|
39
42
|
]);
|
|
40
43
|
const APP_NAVIGATION_NETWORK_TIMEOUT_MS = 1800;
|
|
@@ -49,7 +52,7 @@ const APP_SHELL_FALLBACK_HTML = `<!doctype html>
|
|
|
49
52
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
50
53
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
51
54
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
52
|
-
<link rel="stylesheet" href="
|
|
55
|
+
<link rel="stylesheet" href="${APP_STYLE_URL}">
|
|
53
56
|
<style>
|
|
54
57
|
html, body { min-height: 100%; margin: 0; background: #081015; color: #f5fbff; }
|
|
55
58
|
.boot-splash { position: fixed; inset: 0; display: grid; place-items: center; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; background: radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%), linear-gradient(180deg, #081015 0%, #091015 100%); }
|
|
@@ -150,14 +153,14 @@ self.addEventListener("push", (event) => {
|
|
|
150
153
|
} catch {
|
|
151
154
|
payload = {
|
|
152
155
|
title: "viveworker",
|
|
153
|
-
body: event.data ? event.data.text() :
|
|
156
|
+
body: event.data ? event.data.text() : fallbackNotificationBody(),
|
|
154
157
|
data: { url: "/app" },
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
const title = payload.title || "viveworker";
|
|
159
162
|
const options = {
|
|
160
|
-
body: payload.body ||
|
|
163
|
+
body: payload.body || fallbackNotificationBody(),
|
|
161
164
|
tag: payload.tag || "",
|
|
162
165
|
data: payload.data || { url: "/app" },
|
|
163
166
|
};
|
|
@@ -175,6 +178,13 @@ self.addEventListener("push", (event) => {
|
|
|
175
178
|
})());
|
|
176
179
|
});
|
|
177
180
|
|
|
181
|
+
function fallbackNotificationBody() {
|
|
182
|
+
const language = String(self.navigator?.language || "").toLowerCase();
|
|
183
|
+
return language.startsWith("ja")
|
|
184
|
+
? "viveworker を開いて確認してください。"
|
|
185
|
+
: "Open viveworker for details.";
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
self.addEventListener("notificationclick", (event) => {
|
|
179
189
|
event.preventDefault?.();
|
|
180
190
|
event.notification.close();
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<radialGradient id="bg" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(524 120) rotate(90) scale(920 834)">
|
|
4
|
-
<stop stop-color="#132432"/>
|
|
5
|
-
<stop offset="0.45" stop-color="#0C151C"/>
|
|
6
|
-
<stop offset="1" stop-color="#091016"/>
|
|
7
|
-
</radialGradient>
|
|
8
|
-
<radialGradient id="signal" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 244) rotate(90) scale(182 244)">
|
|
9
|
-
<stop stop-color="#84CBFF" stop-opacity="0.3"/>
|
|
10
|
-
<stop offset="1" stop-color="#84CBFF" stop-opacity="0"/>
|
|
11
|
-
</radialGradient>
|
|
12
|
-
</defs>
|
|
13
|
-
<rect width="1024" height="1024" rx="224" fill="url(#bg)"/>
|
|
14
|
-
<ellipse cx="512" cy="246" rx="184" ry="132" fill="url(#signal)"/>
|
|
15
|
-
<path d="M322 370L477 718C493 754 531 754 547 718L702 370" stroke="#F4FAFF" stroke-width="96" stroke-linecap="round" stroke-linejoin="round"/>
|
|
16
|
-
<circle cx="512" cy="228" r="28" fill="#8CCBFF"/>
|
|
17
|
-
<path d="M438 216C438 175 471 142 512 142C553 142 586 175 586 216" stroke="#8CCBFF" stroke-width="30" stroke-linecap="round"/>
|
|
18
|
-
<path d="M384 220C384 149 441 92 512 92C583 92 640 149 640 220" stroke="#8CCBFF" stroke-width="20" stroke-linecap="round" opacity="0.95"/>
|
|
19
|
-
</svg>
|
|
Binary file
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="188" y1="122" x2="841" y2="931" gradientUnits="userSpaceOnUse">
|
|
4
|
-
<stop stop-color="#16222B"/>
|
|
5
|
-
<stop offset="0.48" stop-color="#0D141A"/>
|
|
6
|
-
<stop offset="1" stop-color="#0A1116"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
<radialGradient id="accent" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(780 252) rotate(128.783) scale(251.649)">
|
|
9
|
-
<stop stop-color="#8BE9C1" stop-opacity="0.34"/>
|
|
10
|
-
<stop offset="1" stop-color="#8BE9C1" stop-opacity="0"/>
|
|
11
|
-
</radialGradient>
|
|
12
|
-
</defs>
|
|
13
|
-
<rect width="1024" height="1024" rx="224" fill="url(#bg)"/>
|
|
14
|
-
<circle cx="781" cy="254" r="120" fill="url(#accent)"/>
|
|
15
|
-
<path d="M286 300L468 730C486 772 538 772 556 730L738 300" stroke="#F8FBFE" stroke-width="108" stroke-linecap="round" stroke-linejoin="round"/>
|
|
16
|
-
<circle cx="760" cy="276" r="116" fill="#8BE9C1"/>
|
|
17
|
-
<circle cx="760" cy="276" r="94" fill="#132028" fill-opacity="0.2"/>
|
|
18
|
-
<path d="M707 277L741 312L815 238" stroke="#0C161C" stroke-width="42" stroke-linecap="round" stroke-linejoin="round"/>
|
|
19
|
-
</svg>
|