@cabane/companion 0.6.126 → 0.6.128

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.
Files changed (3) hide show
  1. package/dist/cli.js +119 -40
  2. package/dist/runtime.js +119 -40
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1037,6 +1037,11 @@ function leaseRefusal(err) {
1037
1037
  function isWriteFenceRefusal(err) {
1038
1038
  return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
1039
1039
  }
1040
+ function isTurnEndedRefusal(err) {
1041
+ if (!(err instanceof ApiError) || err.status !== 401) return false;
1042
+ const body = err.body;
1043
+ return typeof body === "object" && body !== null && body.reason === "turn_ended";
1044
+ }
1040
1045
  function describeErrorBody(body) {
1041
1046
  if (body === void 0 || body === null) return void 0;
1042
1047
  let text;
@@ -1286,6 +1291,15 @@ var init_api = __esm({
1286
1291
  progressed = true;
1287
1292
  } catch (err) {
1288
1293
  if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1294
+ if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
1295
+ this.opts.log?.debug(
1296
+ { kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
1297
+ "companion: queued settle refused; the turn had already ended"
1298
+ );
1299
+ outbox.remove(entry.turnId, entry.seq);
1300
+ progressed = true;
1301
+ continue;
1302
+ }
1289
1303
  this.opts.log?.warn(
1290
1304
  { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
1291
1305
  "Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
@@ -8998,10 +9012,17 @@ var init_turn_execution = __esm({
8998
9012
  body
8999
9013
  );
9000
9014
  } catch (err) {
9001
- turnLog.warn(
9002
- { ...apiErrorLogFields(err), turnId },
9003
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9004
- );
9015
+ if (isTurnEndedRefusal(err)) {
9016
+ turnLog.debug(
9017
+ { ...apiErrorLogFields(err), turnId },
9018
+ "companion: settle refused; the turn had already ended"
9019
+ );
9020
+ } else {
9021
+ turnLog.warn(
9022
+ { ...apiErrorLogFields(err), turnId },
9023
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9024
+ );
9025
+ }
9005
9026
  }
9006
9027
  const durationMs = Date.now() - startedAt;
9007
9028
  this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
@@ -9809,10 +9830,17 @@ var init_turn_execution = __esm({
9809
9830
  body
9810
9831
  );
9811
9832
  } catch (err) {
9812
- turnLog.warn(
9813
- { ...apiErrorLogFields(err) },
9814
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9815
- );
9833
+ if (isTurnEndedRefusal(err)) {
9834
+ turnLog.debug(
9835
+ { ...apiErrorLogFields(err) },
9836
+ "companion: settle refused; the turn had already ended"
9837
+ );
9838
+ } else {
9839
+ turnLog.warn(
9840
+ { ...apiErrorLogFields(err) },
9841
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9842
+ );
9843
+ }
9816
9844
  }
9817
9845
  this.supervisor.releaseAbort(turnId, abortController);
9818
9846
  }
@@ -13810,6 +13838,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13810
13838
  function containerName(attempt) {
13811
13839
  return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
13812
13840
  }
13841
+ var networkName = containerName;
13842
+ function networkCreateError(stderr) {
13843
+ const said = stderr.trim();
13844
+ if (/address pool|fully subnetted/i.test(said))
13845
+ return new Error(
13846
+ `docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
13847
+ );
13848
+ return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
13849
+ }
13813
13850
  function sessionArchivePath(deviceDir, attemptId) {
13814
13851
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
13815
13852
  return join17(deviceDir, "attempts", attemptId, "session.tar");
@@ -13886,7 +13923,7 @@ function createDockerProvider(opts) {
13886
13923
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
13887
13924
  const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
13888
13925
  const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
13889
- const network = `cabane-runners-${opts.deviceKey}`;
13926
+ const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
13890
13927
  const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
13891
13928
  const proxy = `cabane-egress-${opts.deviceKey}`;
13892
13929
  const held = /* @__PURE__ */ new Map();
@@ -13919,32 +13956,14 @@ function createDockerProvider(opts) {
13919
13956
  }
13920
13957
  throw new Error("docker: the egress proxy never started listening");
13921
13958
  };
13922
- let networkReady = null;
13959
+ let proxyReady = null;
13923
13960
  const proxyKey = async () => {
13924
13961
  const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
13925
13962
  return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
13926
13963
  };
13927
- const ensureNetwork = () => {
13928
- networkReady ??= (async () => {
13964
+ const ensureProxy = () => {
13965
+ proxyReady ??= (async () => {
13929
13966
  const allowKey = await proxyKey();
13930
- if ((await docker.run(["network", "inspect", network])).code !== 0) {
13931
- await must(
13932
- [
13933
- "network",
13934
- "create",
13935
- // No route off the host, and no address of the host's on the
13936
- // bridge: the host's own ports are out of reach too. The proxy is
13937
- // the only other thing on the network.
13938
- "--internal",
13939
- "-o",
13940
- "com.docker.network.bridge.inhibit_ipv4=true",
13941
- "--label",
13942
- `cabane.device=${opts.deviceKey}`,
13943
- network
13944
- ],
13945
- "creating the runners network"
13946
- );
13947
- }
13948
13967
  const seen = await docker.run([
13949
13968
  "inspect",
13950
13969
  "-f",
@@ -13953,6 +13972,7 @@ function createDockerProvider(opts) {
13953
13972
  ]);
13954
13973
  const [key, running] = seen.stdout.trim().split(" ");
13955
13974
  if (seen.code === 0 && key === allowKey) {
13975
+ await retireSharedNetwork();
13956
13976
  if (running !== "true") {
13957
13977
  await must(["start", proxy], "starting the egress proxy");
13958
13978
  await proxyListening();
@@ -13987,17 +14007,61 @@ function createDockerProvider(opts) {
13987
14007
  ],
13988
14008
  "starting the egress proxy"
13989
14009
  );
13990
- await must(
13991
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
13992
- "connecting the proxy"
13993
- );
14010
+ await retireSharedNetwork();
13994
14011
  await proxyListening();
13995
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
14012
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
13996
14013
  })().catch((err) => {
13997
- networkReady = null;
14014
+ proxyReady = null;
13998
14015
  throw err;
13999
14016
  });
14000
- return networkReady;
14017
+ return proxyReady;
14018
+ };
14019
+ const retireSharedNetwork = async () => {
14020
+ if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
14021
+ await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
14022
+ const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
14023
+ if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
14024
+ };
14025
+ const networksOf = async (container) => (await docker.run([
14026
+ "inspect",
14027
+ "-f",
14028
+ "{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
14029
+ container
14030
+ ])).stdout.split(/\s+/).filter(Boolean);
14031
+ const ensurePairNetwork = async (net, attempt) => {
14032
+ if ((await docker.run(["network", "inspect", net])).code !== 0) {
14033
+ const made = await docker.run([
14034
+ "network",
14035
+ "create",
14036
+ // No route off the host, and no address of the host's on the bridge:
14037
+ // the host's own ports are out of reach too. The proxy is the only
14038
+ // other thing on the network.
14039
+ "--internal",
14040
+ "-o",
14041
+ "com.docker.network.bridge.inhibit_ipv4=true",
14042
+ "--label",
14043
+ `cabane.device=${opts.deviceKey}`,
14044
+ "--label",
14045
+ `cabane.conversation=${attempt.conversationId}`,
14046
+ "--label",
14047
+ `cabane.agent=${attempt.agentId}`,
14048
+ net
14049
+ ]);
14050
+ if (made.code !== 0) throw networkCreateError(made.stderr);
14051
+ }
14052
+ if (!(await networksOf(proxy)).includes(net))
14053
+ await must(
14054
+ ["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
14055
+ "connecting the proxy"
14056
+ );
14057
+ };
14058
+ const settleNetworks = async (name, net) => {
14059
+ const on = await networksOf(name);
14060
+ if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
14061
+ const others = on.filter((n) => n !== net);
14062
+ for (const other of others)
14063
+ await must(["network", "disconnect", "-f", other, name], "moving the container");
14064
+ if (others.includes(sharedNetwork)) await retireSharedNetwork();
14001
14065
  };
14002
14066
  const socketRoot = opts.socketRoot ?? join17(
14003
14067
  process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
@@ -14029,7 +14093,7 @@ function createDockerProvider(opts) {
14029
14093
  "--label",
14030
14094
  `cabane.companion=${companionLabel}`,
14031
14095
  "--network",
14032
- network,
14096
+ networkName(attempt),
14033
14097
  "--mount",
14034
14098
  `type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
14035
14099
  "--mount",
@@ -14055,6 +14119,17 @@ function createDockerProvider(opts) {
14055
14119
  await docker.run(["rm", "-f", "-v", name]);
14056
14120
  await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
14057
14121
  rmSync9(socketDir(name), { recursive: true, force: true });
14122
+ await dropNetwork(name);
14123
+ };
14124
+ const dropNetwork = async (name) => {
14125
+ if ((await docker.run(["network", "inspect", name])).code !== 0) return;
14126
+ await docker.run(["network", "disconnect", "-f", name, proxy]);
14127
+ const gone = await docker.run(["network", "rm", name]);
14128
+ if (gone.code !== 0)
14129
+ log.warn(
14130
+ { network: name, err: gone.stderr.trim() },
14131
+ "runners: couldn't remove a pair's network"
14132
+ );
14058
14133
  };
14059
14134
  const containerEnv = () => {
14060
14135
  const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
@@ -14221,7 +14296,7 @@ function createDockerProvider(opts) {
14221
14296
  });
14222
14297
  await before;
14223
14298
  try {
14224
- await ensureNetwork();
14299
+ await ensureProxy();
14225
14300
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
14226
14301
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
14227
14302
  let seen = await docker.run([
@@ -14251,9 +14326,12 @@ function createDockerProvider(opts) {
14251
14326
  );
14252
14327
  seen = { code: 1, stdout: "", stderr: "" };
14253
14328
  }
14329
+ await ensurePairNetwork(networkName(attempt), attempt);
14254
14330
  if (seen.code !== 0) {
14255
14331
  await create(name, attempt);
14256
14332
  log.debug({ container: name }, "runners: container created");
14333
+ } else {
14334
+ await settleNetworks(name, networkName(attempt));
14257
14335
  }
14258
14336
  if (seen.stdout.trim().split(" ")[0] !== "true")
14259
14337
  await must(["start", name], "starting the container");
@@ -14267,7 +14345,8 @@ function createDockerProvider(opts) {
14267
14345
  return { provider: "docker", id: name };
14268
14346
  } catch (err) {
14269
14347
  const keep = opts.keepContainers || readPending(name).length > 0;
14270
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
14348
+ const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
14349
+ await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
14271
14350
  });
14272
14351
  release();
14273
14352
  throw err;
package/dist/runtime.js CHANGED
@@ -2279,6 +2279,11 @@ function leaseRefusal(err) {
2279
2279
  function isWriteFenceRefusal(err) {
2280
2280
  return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
2281
2281
  }
2282
+ function isTurnEndedRefusal(err) {
2283
+ if (!(err instanceof ApiError) || err.status !== 401) return false;
2284
+ const body = err.body;
2285
+ return typeof body === "object" && body !== null && body.reason === "turn_ended";
2286
+ }
2282
2287
  var ERROR_BODY_LOG_CAP = 2e3;
2283
2288
  function describeErrorBody(body) {
2284
2289
  if (body === void 0 || body === null) return void 0;
@@ -2469,6 +2474,15 @@ var CabaneApi = class {
2469
2474
  progressed = true;
2470
2475
  } catch (err) {
2471
2476
  if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
2477
+ if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
2478
+ this.opts.log?.debug(
2479
+ { kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
2480
+ "companion: queued settle refused; the turn had already ended"
2481
+ );
2482
+ outbox.remove(entry.turnId, entry.seq);
2483
+ progressed = true;
2484
+ continue;
2485
+ }
2472
2486
  this.opts.log?.warn(
2473
2487
  { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
2474
2488
  "Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
@@ -9984,10 +9998,17 @@ var TurnExecution = class {
9984
9998
  body
9985
9999
  );
9986
10000
  } catch (err) {
9987
- turnLog.warn(
9988
- { ...apiErrorLogFields(err), turnId },
9989
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9990
- );
10001
+ if (isTurnEndedRefusal(err)) {
10002
+ turnLog.debug(
10003
+ { ...apiErrorLogFields(err), turnId },
10004
+ "companion: settle refused; the turn had already ended"
10005
+ );
10006
+ } else {
10007
+ turnLog.warn(
10008
+ { ...apiErrorLogFields(err), turnId },
10009
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
10010
+ );
10011
+ }
9991
10012
  }
9992
10013
  const durationMs = Date.now() - startedAt;
9993
10014
  this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
@@ -10795,10 +10816,17 @@ var TurnExecution = class {
10795
10816
  body
10796
10817
  );
10797
10818
  } catch (err) {
10798
- turnLog.warn(
10799
- { ...apiErrorLogFields(err) },
10800
- "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
10801
- );
10819
+ if (isTurnEndedRefusal(err)) {
10820
+ turnLog.debug(
10821
+ { ...apiErrorLogFields(err) },
10822
+ "companion: settle refused; the turn had already ended"
10823
+ );
10824
+ } else {
10825
+ turnLog.warn(
10826
+ { ...apiErrorLogFields(err) },
10827
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
10828
+ );
10829
+ }
10802
10830
  }
10803
10831
  this.supervisor.releaseAbort(turnId, abortController);
10804
10832
  }
@@ -11527,6 +11555,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11527
11555
  function containerName(attempt) {
11528
11556
  return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
11529
11557
  }
11558
+ var networkName = containerName;
11559
+ function networkCreateError(stderr) {
11560
+ const said = stderr.trim();
11561
+ if (/address pool|fully subnetted/i.test(said))
11562
+ return new Error(
11563
+ `docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
11564
+ );
11565
+ return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
11566
+ }
11530
11567
  function sessionArchivePath(deviceDir, attemptId) {
11531
11568
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
11532
11569
  return join17(deviceDir, "attempts", attemptId, "session.tar");
@@ -11603,7 +11640,7 @@ function createDockerProvider(opts) {
11603
11640
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
11604
11641
  const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
11605
11642
  const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
11606
- const network = `cabane-runners-${opts.deviceKey}`;
11643
+ const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
11607
11644
  const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
11608
11645
  const proxy = `cabane-egress-${opts.deviceKey}`;
11609
11646
  const held = /* @__PURE__ */ new Map();
@@ -11636,32 +11673,14 @@ function createDockerProvider(opts) {
11636
11673
  }
11637
11674
  throw new Error("docker: the egress proxy never started listening");
11638
11675
  };
11639
- let networkReady = null;
11676
+ let proxyReady = null;
11640
11677
  const proxyKey = async () => {
11641
11678
  const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
11642
11679
  return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
11643
11680
  };
11644
- const ensureNetwork = () => {
11645
- networkReady ??= (async () => {
11681
+ const ensureProxy = () => {
11682
+ proxyReady ??= (async () => {
11646
11683
  const allowKey = await proxyKey();
11647
- if ((await docker.run(["network", "inspect", network])).code !== 0) {
11648
- await must(
11649
- [
11650
- "network",
11651
- "create",
11652
- // No route off the host, and no address of the host's on the
11653
- // bridge: the host's own ports are out of reach too. The proxy is
11654
- // the only other thing on the network.
11655
- "--internal",
11656
- "-o",
11657
- "com.docker.network.bridge.inhibit_ipv4=true",
11658
- "--label",
11659
- `cabane.device=${opts.deviceKey}`,
11660
- network
11661
- ],
11662
- "creating the runners network"
11663
- );
11664
- }
11665
11684
  const seen = await docker.run([
11666
11685
  "inspect",
11667
11686
  "-f",
@@ -11670,6 +11689,7 @@ function createDockerProvider(opts) {
11670
11689
  ]);
11671
11690
  const [key, running] = seen.stdout.trim().split(" ");
11672
11691
  if (seen.code === 0 && key === allowKey) {
11692
+ await retireSharedNetwork();
11673
11693
  if (running !== "true") {
11674
11694
  await must(["start", proxy], "starting the egress proxy");
11675
11695
  await proxyListening();
@@ -11704,17 +11724,61 @@ function createDockerProvider(opts) {
11704
11724
  ],
11705
11725
  "starting the egress proxy"
11706
11726
  );
11707
- await must(
11708
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
11709
- "connecting the proxy"
11710
- );
11727
+ await retireSharedNetwork();
11711
11728
  await proxyListening();
11712
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11729
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11713
11730
  })().catch((err) => {
11714
- networkReady = null;
11731
+ proxyReady = null;
11715
11732
  throw err;
11716
11733
  });
11717
- return networkReady;
11734
+ return proxyReady;
11735
+ };
11736
+ const retireSharedNetwork = async () => {
11737
+ if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
11738
+ await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
11739
+ const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
11740
+ if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
11741
+ };
11742
+ const networksOf = async (container) => (await docker.run([
11743
+ "inspect",
11744
+ "-f",
11745
+ "{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
11746
+ container
11747
+ ])).stdout.split(/\s+/).filter(Boolean);
11748
+ const ensurePairNetwork = async (net, attempt) => {
11749
+ if ((await docker.run(["network", "inspect", net])).code !== 0) {
11750
+ const made = await docker.run([
11751
+ "network",
11752
+ "create",
11753
+ // No route off the host, and no address of the host's on the bridge:
11754
+ // the host's own ports are out of reach too. The proxy is the only
11755
+ // other thing on the network.
11756
+ "--internal",
11757
+ "-o",
11758
+ "com.docker.network.bridge.inhibit_ipv4=true",
11759
+ "--label",
11760
+ `cabane.device=${opts.deviceKey}`,
11761
+ "--label",
11762
+ `cabane.conversation=${attempt.conversationId}`,
11763
+ "--label",
11764
+ `cabane.agent=${attempt.agentId}`,
11765
+ net
11766
+ ]);
11767
+ if (made.code !== 0) throw networkCreateError(made.stderr);
11768
+ }
11769
+ if (!(await networksOf(proxy)).includes(net))
11770
+ await must(
11771
+ ["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
11772
+ "connecting the proxy"
11773
+ );
11774
+ };
11775
+ const settleNetworks = async (name, net) => {
11776
+ const on = await networksOf(name);
11777
+ if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
11778
+ const others = on.filter((n) => n !== net);
11779
+ for (const other of others)
11780
+ await must(["network", "disconnect", "-f", other, name], "moving the container");
11781
+ if (others.includes(sharedNetwork)) await retireSharedNetwork();
11718
11782
  };
11719
11783
  const socketRoot = opts.socketRoot ?? join17(
11720
11784
  process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
@@ -11746,7 +11810,7 @@ function createDockerProvider(opts) {
11746
11810
  "--label",
11747
11811
  `cabane.companion=${companionLabel}`,
11748
11812
  "--network",
11749
- network,
11813
+ networkName(attempt),
11750
11814
  "--mount",
11751
11815
  `type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
11752
11816
  "--mount",
@@ -11772,6 +11836,17 @@ function createDockerProvider(opts) {
11772
11836
  await docker.run(["rm", "-f", "-v", name]);
11773
11837
  await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
11774
11838
  rmSync9(socketDir(name), { recursive: true, force: true });
11839
+ await dropNetwork(name);
11840
+ };
11841
+ const dropNetwork = async (name) => {
11842
+ if ((await docker.run(["network", "inspect", name])).code !== 0) return;
11843
+ await docker.run(["network", "disconnect", "-f", name, proxy]);
11844
+ const gone = await docker.run(["network", "rm", name]);
11845
+ if (gone.code !== 0)
11846
+ log.warn(
11847
+ { network: name, err: gone.stderr.trim() },
11848
+ "runners: couldn't remove a pair's network"
11849
+ );
11775
11850
  };
11776
11851
  const containerEnv = () => {
11777
11852
  const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
@@ -11938,7 +12013,7 @@ function createDockerProvider(opts) {
11938
12013
  });
11939
12014
  await before;
11940
12015
  try {
11941
- await ensureNetwork();
12016
+ await ensureProxy();
11942
12017
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
11943
12018
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
11944
12019
  let seen = await docker.run([
@@ -11968,9 +12043,12 @@ function createDockerProvider(opts) {
11968
12043
  );
11969
12044
  seen = { code: 1, stdout: "", stderr: "" };
11970
12045
  }
12046
+ await ensurePairNetwork(networkName(attempt), attempt);
11971
12047
  if (seen.code !== 0) {
11972
12048
  await create(name, attempt);
11973
12049
  log.debug({ container: name }, "runners: container created");
12050
+ } else {
12051
+ await settleNetworks(name, networkName(attempt));
11974
12052
  }
11975
12053
  if (seen.stdout.trim().split(" ")[0] !== "true")
11976
12054
  await must(["start", name], "starting the container");
@@ -11984,7 +12062,8 @@ function createDockerProvider(opts) {
11984
12062
  return { provider: "docker", id: name };
11985
12063
  } catch (err) {
11986
12064
  const keep = opts.keepContainers || readPending(name).length > 0;
11987
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
12065
+ const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
12066
+ await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
11988
12067
  });
11989
12068
  release();
11990
12069
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.126",
3
+ "version": "0.6.128",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",