Repository navigation
Description
Summary
Two installs of the same published version produce different code signatures, so the second one replaces a running daemon and the open session is lost. The trees are byte-identical; only their mtimes differ, because npm stamps fresh mtimes on every install.
I am not suggesting size:mtime is the wrong stamp. code-signature.ts documents it as a deliberate stand-in for contents, and code-signature-cache.ts depends on replaying from statSync alone. The question is narrower: whether the signature should be consulted at all for an installed tree, where the version is already authoritative.
Repro
With one device attached:
npm install --prefix /tmp/a [email protected]
npm install --prefix /tmp/b [email protected]
A=/tmp/a/node_modules/.bin/agent-device
B=/tmp/b/node_modules/.bin/agent-device
"$A" open com.android.settings --relaunch
"$A" is visible 'text="Search Settings"' # Passed
"$B" is visible 'text="Search Settings"' # same versionObserved:
Replacing daemon (pid 75790, v0.20.10) in /Users/me/.agent-device: code-signature mismatch
Error (SESSION_NOT_FOUND): No active session. Run open first.
Returning to $A replaces the daemon a second time. The session is unrecoverable.
The two trees are identical in content. All 319 files hash equal:
(cd /tmp/a/node_modules/agent-device && find . -type f | sort | xargs shasum) > /tmp/a.sums
(cd /tmp/b/node_modules/agent-device && find . -type f | sort | xargs shasum) > /tmp/b.sums
diff /tmp/a.sums /tmp/b.sums # no differencesThe only difference is the timestamp, two seconds apart because the installs ran back to back:
stat -f '%m' /tmp/a/node_modules/agent-device/package.json # 1789044620
stat -f '%m' /tmp/b/node_modules/agent-device/package.json # 1789044622Repeating a command from the same install never prints Replacing daemon, and the session survives.
Why an installed tree seems different from a source tree
resolveDaemonTakeoverReason in src/daemon-client/daemon-client-lifecycle.ts checks the version first, then the signature:
if (info.version !== readVersion()) return `version mismatch (client v${readVersion()})`;
if (info.codeSignature !== (await resolveLocalDaemonCodeSignature())) {
return 'code-signature mismatch';
}For a source checkout the second check is doing real work: the version does not change when you rebuild, and CONTRIBUTING points at pnpm rebuild:cli precisely because a running daemon must not keep serving a stale build.
For an installed package the version is immutable, so the first check already answers "is this the same code". The second check can then only distinguish two installs of the same bytes, and the cost of that distinction is a destroyed session.
Who runs into it
Anyone provisioning their own pinned install rather than resolving through npx per call. We wrap the CLI for an Android monorepo, where agents invoke it once per UI step and npx --yes cost about 850ms every time against 35ms for a direct exec. A version-keyed cache directory makes that cheap, but it also means the tree can legitimately be created more than once: clearing the cache and repopulating it, or two checkouts provisioning in parallel, is enough to lose a session.
Proposed change
Skip the signature comparison when both sides run from an installed package and the versions match, leaving source checkouts exactly as they are:
if (info.version !== readVersion()) return `version mismatch (client v${readVersion()})`;
-if (info.codeSignature !== (await resolveLocalDaemonCodeSignature())) {
+// A published version is immutable, so the version check above already decides for an
+// installed tree; two installs of identical bytes differ only in mtime.
+if (!isInstalledPackage && info.codeSignature !== (await resolveLocalDaemonCodeSignature())) {
return 'code-signature mismatch';
}This leaves code-signature.ts and its cache untouched.
Two alternatives, in case either fits your intent better:
- Keep the check everywhere but refuse instead of replacing when a session is live, so the failure names itself rather than surfacing as
SESSION_NOT_FOUNDon the next command. - Compare resolved package identity (version plus integrity hash) for installed trees, which would also make equivalent installs compare equal.
Happy to open a PR for whichever cut you prefer, with a case added to daemon-client-lifecycle.test.ts. I did not want to guess, since the stamp choice is documented as deliberate.
Environment
- agent-device 0.20.10; signature scheme re-checked at v0.21.0 in
packages/host-kit/src/code-signature.ts - macOS 15, node v24.20.0, npm v11.19.0
- Android target, Pixel 9 Pro emulator
Activity
fix(daemon): scope the fingerprint bypass to two installed trees
fix(daemon): let an installed version stand as its whole code identity (