zas-agent 0.6.2 → 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 CHANGED
@@ -7,6 +7,33 @@ 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
+
10
37
  ## [0.6.2] - 2026-09-04
11
38
 
12
39
  ### Added
@@ -205,7 +232,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
205
232
  two separate identities that cannot read each other's keys.
206
233
  - Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
207
234
 
208
- [Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.6.2...HEAD
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
209
237
  [0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
210
238
  [0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
211
239
  [0.6.0]: https://github.com/soke1556/zas-agent/compare/v0.5.1...v0.6.0
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
@@ -1022,17 +1024,36 @@ function countTurnUrls(servers) {
1022
1024
  }
1023
1025
  function pairFromStats(stats) {
1024
1026
  let pair;
1027
+ let selectedId;
1025
1028
  stats.forEach((s) => {
1029
+ if (s.type === "transport" && typeof s.selectedCandidatePairId === "string") {
1030
+ selectedId = s.selectedCandidatePairId;
1031
+ }
1026
1032
  if (s.type === "candidate-pair" && s.state === "succeeded" && (s.nominated || s.selected)) {
1027
1033
  pair = s;
1028
1034
  }
1029
1035
  });
1036
+ if (selectedId) {
1037
+ const named = stats.get(selectedId);
1038
+ if (named?.localCandidateId && named.remoteCandidateId) pair = named;
1039
+ }
1030
1040
  if (!pair?.localCandidateId || !pair.remoteCandidateId) return void 0;
1031
1041
  const local = stats.get(pair.localCandidateId);
1032
1042
  const remote = stats.get(pair.remoteCandidateId);
1033
1043
  if (!local?.candidateType || !remote?.candidateType) return void 0;
1034
1044
  return { local: local.candidateType, remote: remote.candidateType };
1035
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
+ }
1036
1057
  function pathOf(pair) {
1037
1058
  if (pair.local === "relay" || pair.remote === "relay") return "relay";
1038
1059
  return pair.local === "host" && pair.remote === "host" ? "lan" : "wan";
@@ -1059,6 +1080,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1059
1080
  let phase = "connecting";
1060
1081
  let disconnectTimer;
1061
1082
  let restartTimer;
1083
+ let connFailTimer;
1062
1084
  let stallState = "idle";
1063
1085
  const startedAt = Date.now();
1064
1086
  const diag = {
@@ -1086,7 +1108,20 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1086
1108
  };
1087
1109
  const connState = () => String(pc.connectionState ?? "");
1088
1110
  pc.onconnectionstatechange = () => {
1089
- traceAdd(diag, `conn ${connState()}`);
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);
1090
1125
  };
1091
1126
  let channel;
1092
1127
  pc.onicecandidateerror = (e) => {
@@ -1095,6 +1130,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1095
1130
  };
1096
1131
  const cleanup = () => {
1097
1132
  clearTimeout(waitTimer);
1133
+ clearTimeout(connFailTimer);
1098
1134
  clearTimeout(connectTimer);
1099
1135
  clearTimeout(disconnectTimer);
1100
1136
  clearTimeout(restartTimer);
@@ -1134,6 +1170,20 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1134
1170
  }
1135
1171
  setPhase("failed");
1136
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
+ };
1137
1187
  const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
1138
1188
  let connectTimer;
1139
1189
  let heard = false;
@@ -1143,7 +1193,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1143
1193
  clearTimeout(waitTimer);
1144
1194
  connectTimer = setTimeout(() => {
1145
1195
  const iceUp = pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed";
1146
- fail(iceUp ? "channel_never_opened" : "connect_timeout");
1196
+ failWithCensus(iceUp ? "channel_never_opened" : "connect_timeout");
1147
1197
  }, DIRECT_CONNECT_TIMEOUT_MS);
1148
1198
  };
1149
1199
  let onStall;
@@ -1153,7 +1203,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1153
1203
  clearTimeout(disconnectTimer);
1154
1204
  if (stallState === "restarting") return;
1155
1205
  if (diag.msConnect === void 0 || !onStall) {
1156
- fail("disconnected");
1206
+ failWithCensus(diag.msConnect === void 0 ? "ice_failed" : "disconnected");
1157
1207
  return;
1158
1208
  }
1159
1209
  stallState = "restarting";
@@ -1168,7 +1218,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
1168
1218
  traceAdd(diag, `ice ${String(s ?? "")}`);
1169
1219
  if (s === "failed") {
1170
1220
  if (diag.msConnect !== void 0 && onStall) stalled();
1171
- else fail("ice_failed");
1221
+ else failWithCensus("ice_failed");
1172
1222
  }
1173
1223
  if (s === "disconnected" && stallState === "idle") {
1174
1224
  stallState = "grace";
@@ -1231,8 +1281,14 @@ function signalPlumbing(pc, send, onSendError, diag) {
1231
1281
  generation: localGeneration
1232
1282
  }).catch(onSendError);
1233
1283
  };
1284
+ let applyRejects = 0;
1234
1285
  const apply = (candidate) => {
1235
- void pc.addIceCandidate(candidate ?? void 0).catch(() => void 0);
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
+ });
1236
1292
  };
1237
1293
  const applyIce = (candidate, generation) => {
1238
1294
  countCand(diag, "remote", candidate);
@@ -3928,7 +3984,7 @@ async function report(client, properties) {
3928
3984
 
3929
3985
  // src/server.ts
3930
3986
  function agentVersion() {
3931
- return true ? "0.6.2" : "0.0.0-dev";
3987
+ return true ? "0.6.3" : "0.0.0-dev";
3932
3988
  }
3933
3989
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
3934
3990
  var PAIR_ANNOUNCE_MS = 15e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zas-agent",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "MCP server that lets a coding agent send files and notes into your Zas channels, and send or receive files live through Directo.",
5
5
  "type": "module",
6
6
  "license": "MIT",