a2acalling 0.6.42 → 0.6.43

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/bin/cli.js CHANGED
@@ -2037,60 +2037,173 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2037
2037
  }
2038
2038
  }
2039
2039
 
2040
- // Kill server by PID from config (detached process started by quickstart)
2041
- function killServerPid() {
2042
- try {
2043
- const { A2AConfig } = require('../src/lib/config');
2044
- const config = new A2AConfig();
2045
- const onboarding = config.getOnboarding();
2046
- const pid = onboarding.server_pid;
2047
- if (!pid) return { ok: true, skipped: true };
2040
+ // Check if a TCP port is currently occupied (async, fast)
2041
+ function isPortOccupied(port) {
2042
+ if (!port) return false;
2043
+ const net = require('net');
2044
+ return new Promise((resolve) => {
2045
+ const socket = net.connect({ host: '127.0.0.1', port });
2046
+ let settled = false;
2047
+ const finish = (result) => {
2048
+ if (settled) return;
2049
+ settled = true;
2050
+ try { socket.destroy(); } catch (e) {}
2051
+ resolve(result);
2052
+ };
2053
+ socket.setTimeout(500, () => finish(false));
2054
+ socket.once('connect', () => finish(true));
2055
+ socket.once('error', () => finish(false));
2056
+ });
2057
+ }
2048
2058
 
2049
- // Check if process is alive
2050
- try {
2051
- process.kill(pid, 0); // signal 0 = existence check
2052
- } catch (e) {
2053
- // Process doesn't exist — already dead
2054
- return { ok: true, skipped: true };
2059
+ // Find PID listening on a given port by parsing /proc/net/tcp6 (Linux)
2060
+ function findPidOnPort(port) {
2061
+ try {
2062
+ // Try fuser first (most reliable)
2063
+ const fuserResult = spawnSync('fuser', [`${port}/tcp`], {
2064
+ encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
2065
+ });
2066
+ if (fuserResult.status === 0 && fuserResult.stdout) {
2067
+ const pids = fuserResult.stdout.trim().split(/\s+/).map(Number).filter(p => p > 0);
2068
+ if (pids.length > 0) return pids;
2055
2069
  }
2070
+ } catch (e) { /* fuser not available, fall through */ }
2056
2071
 
2057
- // Kill it
2058
- process.kill(pid, 'SIGTERM');
2059
-
2060
- // Wait briefly and verify it's gone
2061
- const start = Date.now();
2062
- while (Date.now() - start < 3000) {
2063
- try {
2064
- process.kill(pid, 0);
2065
- spawnSync('sleep', ['0.1'], { timeout: 500 });
2066
- } catch (e) {
2067
- // Process is gone
2068
- return { ok: true, pid };
2072
+ try {
2073
+ // Fallback: parse /proc/net/tcp and /proc/net/tcp6
2074
+ const portHex = port.toString(16).toUpperCase().padStart(4, '0');
2075
+ const pids = [];
2076
+ for (const proto of ['/proc/net/tcp', '/proc/net/tcp6']) {
2077
+ let content;
2078
+ try { content = fs.readFileSync(proto, 'utf8'); } catch (e) { continue; }
2079
+ const lines = content.split('\n');
2080
+ for (const line of lines) {
2081
+ const parts = line.trim().split(/\s+/);
2082
+ if (parts.length < 10) continue;
2083
+ const localAddr = parts[1]; // e.g., 00000000:1F90
2084
+ const localPort = localAddr.split(':')[1];
2085
+ if (localPort && localPort.toUpperCase() === portHex) {
2086
+ const inode = parts[9];
2087
+ // Search /proc/*/fd/* for socket inodes
2088
+ try {
2089
+ const procDirs = fs.readdirSync('/proc').filter(d => /^\d+$/.test(d));
2090
+ for (const pid of procDirs) {
2091
+ try {
2092
+ const fdDir = `/proc/${pid}/fd`;
2093
+ const fds = fs.readdirSync(fdDir);
2094
+ for (const fd of fds) {
2095
+ try {
2096
+ const link = fs.readlinkSync(`${fdDir}/${fd}`);
2097
+ if (link === `socket:[${inode}]`) {
2098
+ pids.push(Number(pid));
2099
+ }
2100
+ } catch (e) {}
2101
+ }
2102
+ } catch (e) {}
2103
+ }
2104
+ } catch (e) {}
2105
+ }
2069
2106
  }
2070
2107
  }
2108
+ if (pids.length > 0) return [...new Set(pids)];
2109
+ } catch (e) { /* /proc parsing failed */ }
2071
2110
 
2072
- // Still alive after 3s — force kill
2111
+ return [];
2112
+ }
2113
+
2114
+ // Kill a specific PID with SIGTERM, wait, then SIGKILL if needed
2115
+ function killPidSync(pid) {
2116
+ try {
2117
+ process.kill(pid, 0); // existence check
2118
+ } catch (e) {
2119
+ return { ok: true, skipped: true };
2120
+ }
2121
+
2122
+ process.kill(pid, 'SIGTERM');
2123
+ const start = Date.now();
2124
+ while (Date.now() - start < 3000) {
2073
2125
  try {
2074
- process.kill(pid, 'SIGKILL');
2075
- return { ok: true, pid, forced: true };
2126
+ process.kill(pid, 0);
2127
+ spawnSync('sleep', ['0.1'], { timeout: 500 });
2076
2128
  } catch (e) {
2077
2129
  return { ok: true, pid };
2078
2130
  }
2131
+ }
2132
+
2133
+ // Still alive — force kill
2134
+ try {
2135
+ process.kill(pid, 'SIGKILL');
2136
+ // Brief wait for SIGKILL to take effect
2137
+ spawnSync('sleep', ['0.2'], { timeout: 500 });
2138
+ try {
2139
+ process.kill(pid, 0);
2140
+ return { ok: false, pid, error: `PID ${pid} survived SIGKILL` };
2141
+ } catch (e) {
2142
+ return { ok: true, pid, forced: true };
2143
+ }
2144
+ } catch (e) {
2145
+ return { ok: true, pid };
2146
+ }
2147
+ }
2148
+
2149
+ // Kill server by PID from config (detached process started by quickstart)
2150
+ // Then verify the port is actually freed; if not, find and kill whatever holds it.
2151
+ async function killServerPid() {
2152
+ let pid, serverPort;
2153
+ try {
2154
+ const { A2AConfig } = require('../src/lib/config');
2155
+ const cfg = new A2AConfig();
2156
+ const onboarding = cfg.getOnboarding();
2157
+ pid = onboarding.server_pid;
2158
+ serverPort = onboarding.server_port;
2079
2159
  } catch (err) {
2080
2160
  // Config read failed — not fatal, continue with pm2 path
2081
2161
  return { ok: true, skipped: true };
2082
2162
  }
2163
+
2164
+ // Step 1: Try to kill the PID from config
2165
+ if (pid) {
2166
+ killPidSync(pid);
2167
+ }
2168
+
2169
+ // Step 2: Verify the port is freed
2170
+ if (serverPort) {
2171
+ const stillOccupied = await isPortOccupied(serverPort);
2172
+ if (stillOccupied) {
2173
+ // Port is still held — find and kill whatever is on it
2174
+ const pids = findPidOnPort(serverPort);
2175
+ let killedAny = false;
2176
+ for (const p of pids) {
2177
+ const result = killPidSync(p);
2178
+ if (result.ok && !result.skipped) killedAny = true;
2179
+ }
2180
+
2181
+ // Final check
2182
+ const stillUp = await isPortOccupied(serverPort);
2183
+ if (stillUp) {
2184
+ return { ok: false, pid, port: serverPort, error: `Port ${serverPort} is still occupied after kill attempts` };
2185
+ }
2186
+ return { ok: true, pid, port: serverPort, portKill: killedAny };
2187
+ }
2188
+ }
2189
+
2190
+ return { ok: true, pid, port: serverPort, skipped: !pid };
2083
2191
  }
2084
2192
 
2085
2193
  process.stdout.write('Stopping server... ');
2086
- const pidResult = killServerPid();
2194
+ const pidResult = await killServerPid();
2087
2195
  const stopped = pm2StopAndDelete('a2a');
2088
2196
  if (!pidResult.ok && !stopped.ok) {
2089
2197
  console.log('❌');
2090
- console.error(` ${stopped.error}`);
2198
+ console.error(` ${pidResult.error || stopped.error}`);
2091
2199
  process.exit(1);
2092
2200
  }
2093
- console.log('✅');
2201
+ if (!pidResult.ok) {
2202
+ console.log('⚠️');
2203
+ console.error(` Warning: ${pidResult.error}`);
2204
+ } else {
2205
+ console.log('✅');
2206
+ }
2094
2207
 
2095
2208
  let configOk = true;
2096
2209
  let dbOk = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.42",
3
+ "version": "0.6.43",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -33,6 +33,11 @@ const TERMINATION_PATTERNS = [
33
33
  /\[DISCONNECT/i,
34
34
  /\[END.?CALL\]/i,
35
35
  /\[CLOSING\]/i,
36
+ /\bEND.?CALL\b/i,
37
+ /\bcall\s+closed\b/i,
38
+ /\bwrapping\s+up\b/i,
39
+ /\[No\s+further\b/i,
40
+ /\bno\s+further\b/i,
36
41
  /\bREFUSING\s+(TO\s+)?(CONTINU|RESPOND|ENGAG)/i,
37
42
  /\bcall\s+complet(ed|e)\b/i,
38
43
  /\bconversation\s+(is\s+)?(over|ended|closed|complet)/i,
@@ -43,6 +48,9 @@ const TERMINATION_PATTERNS = [
43
48
 
44
49
  function detectRemoteTermination(text) {
45
50
  if (!text || typeof text !== 'string') return false;
51
+ // Very short responses (single dot, empty-ish) indicate dead conversation
52
+ const trimmed = text.trim();
53
+ if (trimmed.length <= 1) return true;
46
54
  return TERMINATION_PATTERNS.some(pattern => pattern.test(text));
47
55
  }
48
56
 
@@ -82,6 +90,12 @@ function inferStateProgression(collabState, remoteText, turn) {
82
90
  // Confidence increases over turns
83
91
  patch.confidence = Math.min(0.9, 0.25 + turn * 0.08);
84
92
 
93
+ // In converging phase, signal close — conversation has run its natural course
94
+ const effectivePhase = patch.phase || collabState.phase;
95
+ if (effectivePhase === 'converging') {
96
+ patch.closeSignal = true;
97
+ }
98
+
85
99
  return patch;
86
100
  }
87
101
 
@@ -207,6 +221,7 @@ Be concise but specific. No filler.`;
207
221
  conversationId = `conv_${Date.now()}_local`;
208
222
 
209
223
  let nextMessage = openingMessage;
224
+ const overlapHistory = [];
210
225
 
211
226
  for (let turn = 0; turn < this.maxTurns; turn++) {
212
227
  // 1. Send message to remote
@@ -385,6 +400,21 @@ Be concise but specific. No filler.`;
385
400
  if (inferred.phase) collabState.phase = inferred.phase;
386
401
  if (inferred.overlapScore != null) collabState.overlapScore = inferred.overlapScore;
387
402
  if (inferred.confidence != null) collabState.confidence = inferred.confidence;
403
+ if (inferred.closeSignal != null) collabState.closeSignal = inferred.closeSignal;
404
+ }
405
+
406
+ // 6b. Overlap flatline detection — if overlap hasn't changed significantly
407
+ // for 3+ consecutive turns while in converging phase, the conversation is dead
408
+ overlapHistory.push(collabState.overlapScore);
409
+ if (collabState.phase === 'converging' && overlapHistory.length >= 3) {
410
+ const recent = overlapHistory.slice(-3);
411
+ const maxDelta = Math.max(
412
+ Math.abs(recent[1] - recent[0]),
413
+ Math.abs(recent[2] - recent[1])
414
+ );
415
+ if (maxDelta < 0.02) {
416
+ collabState.closeSignal = true;
417
+ }
388
418
  }
389
419
 
390
420
  // 7. Persist collab state to DB