Relationships
#999 [@adam/cfgmgmt] SSH helper can crash swamp serve via unhandled finally() rejection
Opened by evrardjp · 7/6/2026
Summary
@adam/cfgmgmt can crash a long-running swamp serve process when SSH connection establishment fails, instead of reporting the workflow/model method failure cleanly.
The immediate trigger was a @adam/cfgmgmt/systemd check against an unreachable lab VM:
error: Uncaught (in promise) Error: SSH ControlMaster failed for 192.168.164.10:22:admin after 10 attempts: ssh: connect to host 192.168.164.10 port 22: No route to host
throw new Error(`SSH ControlMaster failed for ${key} after ${maxRetries + 1} attempts: ${stderr}`);
^
at _establishConnection (.../.swamp/bundles/ae74233e/adam/cfgmgmt/systemd.js?fp=839f7f2bf0ada6fdb517eff39ee80e2eb8eb07834bfd9e67a9d553ed76f32eff:110:13)A workflow failure due to an unreachable SSH target is expected; the server process should survive and mark the method/workflow failed.
Suspected root cause
In models/adam/cfgmgmt/_lib/ssh.ts, getConnection() records an in-flight connection promise and uses finally() for cleanup:
const promise = _establishConnection(opts, key, port);
inflight().set(key, promise);
promise.finally(() => inflight().delete(key));
return promise;Promise.prototype.finally() returns a new promise. When _establishConnection() rejects, the returned promise from finally() also rejects. That returned promise is detached and has no rejection handler, so Deno reports it as an uncaught promise rejection. In a long-running host such as swamp serve, this can terminate the server even though the original promise is awaited/handled by the model method path.
Proposed fix
Attach a rejection handler to the cleanup promise, or use an equivalent cleanup pattern that does not create a detached rejecting promise:
const promise = _establishConnection(opts, key, port);
inflight().set(key, promise);
// Avoid a detached rejecting promise from finally(). The caller handles
// `promise`; this cleanup promise must not surface as an unhandled rejection.
promise.finally(() => inflight().delete(key)).catch(() => {});
return promise;Alternative:
const promise = _establishConnection(opts, key, port);
inflight().set(key, promise);
promise.then(
() => inflight().delete(key),
() => inflight().delete(key),
);
return promise;Reproduction shape
- Run
swamp serve. - Trigger any
@adam/cfgmgmtmethod using the shared SSH helper against an unreachable host, e.g. a paused VM or unroutable address. - After the SSH retries fail, the method should fail; instead the server can emit
Uncaught (in promise)and exit.
Environment notes
- Extension:
@adam/cfgmgmtversion2026.03.30.1 - Swamp CLI observed:
20260706.220403.0-sha.72b7ed98 - Failing model type:
@adam/cfgmgmt/systemd - Failing target:
192.168.164.10:22asadmin
Routing note
I first attempted to file this with swamp issue bug --extension @adam/cfgmgmt, but Swamp returned status: refused because the extension does not declare a repository URL. Filing here so the report is not lost; please route to the @adam/cfgmgmt publisher if possible.
Closed
No activity in this phase yet.
evrardjp commented 7/6/2026, 11:31:34 PM
You can most likely close this, it is not swamp core extension. It is badly filed by my agent, my bad.
stack72 commented 7/7/2026, 12:03:25 AM
Thanks for the detailed report @evrardjp — the root cause analysis and proposed fix are spot-on.
However, the affected code (_lib/ssh.ts) lives in the @adam/cfgmgmt extension, which is published by @adam and is not part of this repository. The @swamp/ssh extension in this repo uses a different SSH architecture that does not share this pattern.
Issues against third-party extensions should be filed against that extension's own repository. It looks like @adam/cfgmgmt doesn't currently declare a repository URL (which is why swamp issue bug --extension @adam/cfgmgmt refused the report) — that's something the publisher should fix so future reports can be routed correctly.
Closing this here. If the @adam/cfgmgmt publisher needs a pointer, the fix is exactly as described in the issue: either .finally(...).catch(() => {}) or the .then(cleanup, cleanup) pattern to avoid a detached rejecting promise.
evrardjp commented 7/7/2026, 8:41:06 AM
100%. This is why I said on the discord: My bad for submitting it here (my agent did), and I pinged adam with that link.
Sign in to post a ripple.