zas-agent 0.6.1 → 0.6.3
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 +52 -1
- package/dist/cli.js +95 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.3] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The selected candidate pair is read from the transport, which names its
|
|
15
|
+
own pair on every engine. A transfer that connected and carried its whole
|
|
16
|
+
file could report `pair=none`, because the browser at the other end does
|
|
17
|
+
not mark a pair `nominated` on the controlled side. The empty field was
|
|
18
|
+
read as a missing pair and sent two rounds of diagnosis after a
|
|
19
|
+
connection that had been there all along.
|
|
20
|
+
- A peer connection state of `failed` has to hold before the engine acts on
|
|
21
|
+
it. Chrome reports `failed` while it is still gathering and returns to
|
|
22
|
+
`connecting` in the same millisecond; acting on the first one killed a
|
|
23
|
+
receiver at 0.88 s with pairs still waiting to be tried. The settle window
|
|
24
|
+
is shorter than the ICE grace, so a connection that really is dead still
|
|
25
|
+
gives up sooner than it did before.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- The trace counts candidate pairs by check state just before the verdict:
|
|
30
|
+
`checks failed=24 in-progress=4`, or `checks none`. No pair selected could
|
|
31
|
+
not tell a run that formed no pairs from one that formed pairs and lost
|
|
32
|
+
every check, and those are different faults.
|
|
33
|
+
- A candidate the peer refuses is traced as `ice apply rejected <type>
|
|
34
|
+
<ErrorName>`. The rejection used to be swallowed, so a refused candidate
|
|
35
|
+
looked exactly like one the network lost.
|
|
36
|
+
|
|
37
|
+
## [0.6.2] - 2026-09-04
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- The trace records the peer connection state, not only the ICE state.
|
|
42
|
+
ICE `connected` means a candidate pair answered; the peer connection
|
|
43
|
+
reaches `connected` only once DTLS completes. A session that holds an ICE
|
|
44
|
+
path and sends nothing over it used to look the same as one that never
|
|
45
|
+
found a path at all.
|
|
46
|
+
- A failure that had a working ICE pair is now `channel_never_opened`, not
|
|
47
|
+
`connect_timeout`. Same clock, different fault: one is the candidate hunt,
|
|
48
|
+
the other is the handshake above it.
|
|
49
|
+
- The verdict line carries the peer connection state and the data channel's
|
|
50
|
+
own state, so a stalled DTLS handshake and a stalled SCTP association can
|
|
51
|
+
be told apart from one line.
|
|
52
|
+
- The selected candidate pair is read when ICE connects, not only when the
|
|
53
|
+
data channel opens. A run that connected and then went quiet reported
|
|
54
|
+
`pair=none` and looked as if it had never selected one.
|
|
55
|
+
- `onicecandidateerror` is traced, by URL shape and error code only. A
|
|
56
|
+
gather that ends with no relay candidate now says whether the allocation
|
|
57
|
+
was refused or never answered.
|
|
58
|
+
|
|
10
59
|
## [0.6.1] - 2026-09-04
|
|
11
60
|
|
|
12
61
|
### Added
|
|
@@ -183,7 +232,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
232
|
two separate identities that cannot read each other's keys.
|
|
184
233
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
185
234
|
|
|
186
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.6.
|
|
235
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.6.3...HEAD
|
|
236
|
+
[0.6.3]: https://github.com/soke1556/zas-agent/compare/v0.6.2...v0.6.3
|
|
237
|
+
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
|
187
238
|
[0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
|
|
188
239
|
[0.6.0]: https://github.com/soke1556/zas-agent/compare/v0.5.1...v0.6.0
|
|
189
240
|
[0.5.1]: https://github.com/soke1556/zas-agent/compare/v0.5.0...v0.5.1
|
package/dist/cli.js
CHANGED
|
@@ -854,6 +854,8 @@ function directFallbackCipherSize(size) {
|
|
|
854
854
|
}
|
|
855
855
|
var DIRECT_CONNECT_TIMEOUT_MS = 30 * 1e3;
|
|
856
856
|
var DIRECT_STALL_GRACE_MS = 5 * 1e3;
|
|
857
|
+
var DIRECT_CENSUS_WAIT_MS = 250;
|
|
858
|
+
var DIRECT_CONN_FAILED_SETTLE_MS = 1500;
|
|
857
859
|
var DIRECT_SIGNAL_WAIT_MS = 90 * 1e3;
|
|
858
860
|
|
|
859
861
|
// src/shared/direct-engine.ts
|
|
@@ -969,14 +971,17 @@ function traceAdd(diag, line) {
|
|
|
969
971
|
const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
|
|
970
972
|
diag.trace.push(`${at.toFixed(2)} ${line}`);
|
|
971
973
|
}
|
|
974
|
+
function urlShape(url) {
|
|
975
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
976
|
+
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
977
|
+
return transport ? `${scheme}/${transport}` : scheme;
|
|
978
|
+
}
|
|
972
979
|
function iceShapes(servers) {
|
|
973
980
|
const seen = /* @__PURE__ */ new Map();
|
|
974
981
|
for (const server of servers ?? []) {
|
|
975
982
|
const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
|
|
976
983
|
for (const url of urls ?? []) {
|
|
977
|
-
const
|
|
978
|
-
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
979
|
-
const shape = transport ? `${scheme}/${transport}` : scheme;
|
|
984
|
+
const shape = urlShape(url);
|
|
980
985
|
seen.set(shape, (seen.get(shape) ?? 0) + 1);
|
|
981
986
|
}
|
|
982
987
|
}
|
|
@@ -1019,17 +1024,36 @@ function countTurnUrls(servers) {
|
|
|
1019
1024
|
}
|
|
1020
1025
|
function pairFromStats(stats) {
|
|
1021
1026
|
let pair;
|
|
1027
|
+
let selectedId;
|
|
1022
1028
|
stats.forEach((s) => {
|
|
1029
|
+
if (s.type === "transport" && typeof s.selectedCandidatePairId === "string") {
|
|
1030
|
+
selectedId = s.selectedCandidatePairId;
|
|
1031
|
+
}
|
|
1023
1032
|
if (s.type === "candidate-pair" && s.state === "succeeded" && (s.nominated || s.selected)) {
|
|
1024
1033
|
pair = s;
|
|
1025
1034
|
}
|
|
1026
1035
|
});
|
|
1036
|
+
if (selectedId) {
|
|
1037
|
+
const named = stats.get(selectedId);
|
|
1038
|
+
if (named?.localCandidateId && named.remoteCandidateId) pair = named;
|
|
1039
|
+
}
|
|
1027
1040
|
if (!pair?.localCandidateId || !pair.remoteCandidateId) return void 0;
|
|
1028
1041
|
const local = stats.get(pair.localCandidateId);
|
|
1029
1042
|
const remote = stats.get(pair.remoteCandidateId);
|
|
1030
1043
|
if (!local?.candidateType || !remote?.candidateType) return void 0;
|
|
1031
1044
|
return { local: local.candidateType, remote: remote.candidateType };
|
|
1032
1045
|
}
|
|
1046
|
+
function checkCensus(stats) {
|
|
1047
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1048
|
+
stats.forEach((s) => {
|
|
1049
|
+
if (s.type !== "candidate-pair") return;
|
|
1050
|
+
const state = String(s.state ?? "?");
|
|
1051
|
+
counts.set(state, (counts.get(state) ?? 0) + 1);
|
|
1052
|
+
});
|
|
1053
|
+
if (counts.size === 0) return "checks none";
|
|
1054
|
+
const parts = [...counts].map(([state, n]) => `${state}=${n}`);
|
|
1055
|
+
return `checks ${parts.join(" ")}`;
|
|
1056
|
+
}
|
|
1033
1057
|
function pathOf(pair) {
|
|
1034
1058
|
if (pair.local === "relay" || pair.remote === "relay") return "relay";
|
|
1035
1059
|
return pair.local === "host" && pair.remote === "host" ? "lan" : "wan";
|
|
@@ -1056,6 +1080,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1056
1080
|
let phase = "connecting";
|
|
1057
1081
|
let disconnectTimer;
|
|
1058
1082
|
let restartTimer;
|
|
1083
|
+
let connFailTimer;
|
|
1059
1084
|
let stallState = "idle";
|
|
1060
1085
|
const startedAt = Date.now();
|
|
1061
1086
|
const diag = {
|
|
@@ -1081,14 +1106,39 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1081
1106
|
pc.onicegatheringstatechange = () => {
|
|
1082
1107
|
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1083
1108
|
};
|
|
1109
|
+
const connState = () => String(pc.connectionState ?? "");
|
|
1110
|
+
pc.onconnectionstatechange = () => {
|
|
1111
|
+
const state = connState();
|
|
1112
|
+
traceAdd(diag, `conn ${state}`);
|
|
1113
|
+
if (state !== "failed") {
|
|
1114
|
+
clearTimeout(connFailTimer);
|
|
1115
|
+
connFailTimer = void 0;
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (connFailTimer !== void 0) return;
|
|
1119
|
+
connFailTimer = setTimeout(() => {
|
|
1120
|
+
connFailTimer = void 0;
|
|
1121
|
+
if (connState() !== "failed") return;
|
|
1122
|
+
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1123
|
+
else failWithCensus("ice_failed");
|
|
1124
|
+
}, DIRECT_CONN_FAILED_SETTLE_MS);
|
|
1125
|
+
};
|
|
1126
|
+
let channel;
|
|
1127
|
+
pc.onicecandidateerror = (e) => {
|
|
1128
|
+
const err = e;
|
|
1129
|
+
traceAdd(diag, `ice error ${err.url ? urlShape(err.url) : "?"} code=${err.errorCode ?? "?"}`);
|
|
1130
|
+
};
|
|
1084
1131
|
const cleanup = () => {
|
|
1085
1132
|
clearTimeout(waitTimer);
|
|
1133
|
+
clearTimeout(connFailTimer);
|
|
1086
1134
|
clearTimeout(connectTimer);
|
|
1087
1135
|
clearTimeout(disconnectTimer);
|
|
1088
1136
|
clearTimeout(restartTimer);
|
|
1089
1137
|
pc.onicecandidate = null;
|
|
1090
1138
|
pc.oniceconnectionstatechange = null;
|
|
1091
1139
|
pc.onicegatheringstatechange = null;
|
|
1140
|
+
pc.onicecandidateerror = null;
|
|
1141
|
+
pc.onconnectionstatechange = null;
|
|
1092
1142
|
pc.close();
|
|
1093
1143
|
};
|
|
1094
1144
|
const setPhase = (p) => {
|
|
@@ -1100,7 +1150,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1100
1150
|
diag.gatherState = String(pc.iceGatheringState ?? "");
|
|
1101
1151
|
diag.hadRemoteDesc = hasRemoteDescription(pc);
|
|
1102
1152
|
diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
|
|
1103
|
-
traceAdd(
|
|
1153
|
+
traceAdd(
|
|
1154
|
+
diag,
|
|
1155
|
+
`${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState} conn=${connState()} chan=${channel === void 0 ? "-" : String(channel.readyState ?? "")}`
|
|
1156
|
+
);
|
|
1104
1157
|
onDiag?.(diag);
|
|
1105
1158
|
onPhase(p);
|
|
1106
1159
|
cleanup();
|
|
@@ -1117,6 +1170,20 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1117
1170
|
}
|
|
1118
1171
|
setPhase("failed");
|
|
1119
1172
|
};
|
|
1173
|
+
const failWithCensus = (reason) => {
|
|
1174
|
+
if (phase === "done" || phase === "failed") return;
|
|
1175
|
+
let reported = false;
|
|
1176
|
+
const report2 = () => {
|
|
1177
|
+
if (reported) return;
|
|
1178
|
+
reported = true;
|
|
1179
|
+
fail(reason);
|
|
1180
|
+
};
|
|
1181
|
+
const guard = setTimeout(report2, DIRECT_CENSUS_WAIT_MS);
|
|
1182
|
+
void pc.getStats().then((stats) => traceAdd(diag, checkCensus(stats))).catch(() => void 0).then(() => {
|
|
1183
|
+
clearTimeout(guard);
|
|
1184
|
+
report2();
|
|
1185
|
+
});
|
|
1186
|
+
};
|
|
1120
1187
|
const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
|
|
1121
1188
|
let connectTimer;
|
|
1122
1189
|
let heard = false;
|
|
@@ -1124,7 +1191,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1124
1191
|
if (heard) return;
|
|
1125
1192
|
heard = true;
|
|
1126
1193
|
clearTimeout(waitTimer);
|
|
1127
|
-
connectTimer = setTimeout(() =>
|
|
1194
|
+
connectTimer = setTimeout(() => {
|
|
1195
|
+
const iceUp = pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed";
|
|
1196
|
+
failWithCensus(iceUp ? "channel_never_opened" : "connect_timeout");
|
|
1197
|
+
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1128
1198
|
};
|
|
1129
1199
|
let onStall;
|
|
1130
1200
|
let stallWindowMs = DIRECT_CONNECT_TIMEOUT_MS;
|
|
@@ -1133,7 +1203,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1133
1203
|
clearTimeout(disconnectTimer);
|
|
1134
1204
|
if (stallState === "restarting") return;
|
|
1135
1205
|
if (diag.msConnect === void 0 || !onStall) {
|
|
1136
|
-
|
|
1206
|
+
failWithCensus(diag.msConnect === void 0 ? "ice_failed" : "disconnected");
|
|
1137
1207
|
return;
|
|
1138
1208
|
}
|
|
1139
1209
|
stallState = "restarting";
|
|
@@ -1148,7 +1218,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1148
1218
|
traceAdd(diag, `ice ${String(s ?? "")}`);
|
|
1149
1219
|
if (s === "failed") {
|
|
1150
1220
|
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1151
|
-
else
|
|
1221
|
+
else failWithCensus("ice_failed");
|
|
1152
1222
|
}
|
|
1153
1223
|
if (s === "disconnected" && stallState === "idle") {
|
|
1154
1224
|
stallState = "grace";
|
|
@@ -1158,6 +1228,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1158
1228
|
clearTimeout(disconnectTimer);
|
|
1159
1229
|
clearTimeout(restartTimer);
|
|
1160
1230
|
stallState = "idle";
|
|
1231
|
+
void pc.getStats().then((stats) => {
|
|
1232
|
+
const pair = pairFromStats(stats);
|
|
1233
|
+
if (!pair || diag.pairLocal) return;
|
|
1234
|
+
diag.pairLocal = pair.local;
|
|
1235
|
+
diag.pairRemote = pair.remote;
|
|
1236
|
+
traceAdd(diag, `pair ${pair.local}/${pair.remote}`);
|
|
1237
|
+
}).catch(() => void 0);
|
|
1161
1238
|
}
|
|
1162
1239
|
};
|
|
1163
1240
|
return {
|
|
@@ -1176,6 +1253,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1176
1253
|
}
|
|
1177
1254
|
},
|
|
1178
1255
|
isTerminal: () => phase === "done" || phase === "failed",
|
|
1256
|
+
setChannel: (dc) => void (channel = dc),
|
|
1179
1257
|
setStall: (fn, windowMs) => {
|
|
1180
1258
|
onStall = fn;
|
|
1181
1259
|
stallWindowMs = windowMs;
|
|
@@ -1203,8 +1281,14 @@ function signalPlumbing(pc, send, onSendError, diag) {
|
|
|
1203
1281
|
generation: localGeneration
|
|
1204
1282
|
}).catch(onSendError);
|
|
1205
1283
|
};
|
|
1284
|
+
let applyRejects = 0;
|
|
1206
1285
|
const apply = (candidate) => {
|
|
1207
|
-
void pc.addIceCandidate(candidate ?? void 0).catch(() =>
|
|
1286
|
+
void pc.addIceCandidate(candidate ?? void 0).catch((e) => {
|
|
1287
|
+
if (applyRejects >= 3) return;
|
|
1288
|
+
applyRejects++;
|
|
1289
|
+
const name = e?.name || "Error";
|
|
1290
|
+
traceAdd(diag, `ice apply rejected ${candidate ? candType(candidate) : "end"} ${name}`);
|
|
1291
|
+
});
|
|
1208
1292
|
};
|
|
1209
1293
|
const applyIce = (candidate, generation) => {
|
|
1210
1294
|
countCand(diag, "remote", candidate);
|
|
@@ -1267,6 +1351,7 @@ function startSender(opts) {
|
|
|
1267
1351
|
})().catch((e) => s.fail("signaling", e));
|
|
1268
1352
|
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1269
1353
|
const dc = pc.createDataChannel("zas-direct", { ordered: true });
|
|
1354
|
+
s.setChannel(dc);
|
|
1270
1355
|
dc.binaryType = "arraybuffer";
|
|
1271
1356
|
dc.bufferedAmountLowThreshold = DIRECT_BUFFERED_HIGH / 8;
|
|
1272
1357
|
const waitLow = () => new Promise((resolve2) => {
|
|
@@ -1415,6 +1500,7 @@ function startReceiver(opts) {
|
|
|
1415
1500
|
let finished = false;
|
|
1416
1501
|
pc.ondatachannel = (e) => {
|
|
1417
1502
|
dc = e.channel;
|
|
1503
|
+
s.setChannel(dc);
|
|
1418
1504
|
dc.binaryType = "arraybuffer";
|
|
1419
1505
|
dc.onopen = () => {
|
|
1420
1506
|
s.connected();
|
|
@@ -3898,7 +3984,7 @@ async function report(client, properties) {
|
|
|
3898
3984
|
|
|
3899
3985
|
// src/server.ts
|
|
3900
3986
|
function agentVersion() {
|
|
3901
|
-
return true ? "0.6.
|
|
3987
|
+
return true ? "0.6.3" : "0.0.0-dev";
|
|
3902
3988
|
}
|
|
3903
3989
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3904
3990
|
var PAIR_ANNOUNCE_MS = 15e3;
|
package/package.json
CHANGED