Skip to main content
← Back to list
01Issue
BugClosedExtensionsTeam
Assigneesstack72

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

  1. Run swamp serve.
  2. Trigger any @adam/cfgmgmt method using the shared SSH helper against an unreachable host, e.g. a paused VM or unroutable address.
  3. After the SSH retries fail, the method should fail; instead the server can emit Uncaught (in promise) and exit.

Environment notes

  • Extension: @adam/cfgmgmt version 2026.03.30.1
  • Swamp CLI observed: 20260706.220403.0-sha.72b7ed98
  • Failing model type: @adam/cfgmgmt/systemd
  • Failing target: 192.168.164.10:22 as admin

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.

02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED+ 1 MOREASSIGNEDCLASSIFICATION

Closed

7/7/2026, 12:03:26 AM

No activity in this phase yet.

03Sludge Pulse
stack72 assigned stack727/6/2026, 11:55:00 PM
Editable. Press Enter to edit.

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.