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 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.1...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
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 scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
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(diag, `${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState}`);
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(() => fail("connect_timeout"), DIRECT_CONNECT_TIMEOUT_MS);
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
- fail("disconnected");
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 fail("ice_failed");
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(() => 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
+ });
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.1" : "0.0.0-dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zas-agent",
3
- "version": "0.6.1",
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",