agent-dealer 1.2.4 → 1.2.6

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 (72) hide show
  1. package/bundle/server/dist/adapters/agent-deck-bind.js +33 -3
  2. package/bundle/server/dist/adapters/agent-deck-bind.test.js +60 -2
  3. package/bundle/server/dist/adapters/agent-health.js +28 -6
  4. package/bundle/server/dist/adapters/agent-health.test.js +66 -11
  5. package/bundle/server/dist/adapters/git-worktree.js +187 -18
  6. package/bundle/server/dist/adapters/git-worktree.test.js +293 -0
  7. package/bundle/server/dist/adapters/github.js +110 -12
  8. package/bundle/server/dist/adapters/github.test.js +274 -3
  9. package/bundle/server/dist/adapters/muse-capability.js +356 -0
  10. package/bundle/server/dist/adapters/muse-capability.test.js +464 -0
  11. package/bundle/server/dist/capacity/claude-local-cache.js +266 -81
  12. package/bundle/server/dist/capacity/claude-local-cache.test.js +335 -36
  13. package/bundle/server/dist/capacity/muse-host.js +92 -32
  14. package/bundle/server/dist/capacity/muse-host.test.js +130 -4
  15. package/bundle/server/dist/coordinator/admission.test.js +85 -0
  16. package/bundle/server/dist/coordinator/args.js +4 -3
  17. package/bundle/server/dist/coordinator/checkpoint.js +5 -3
  18. package/bundle/server/dist/coordinator/checkpoint.test.js +23 -0
  19. package/bundle/server/dist/coordinator/commands.js +100 -12
  20. package/bundle/server/dist/coordinator/developer-effect.js +128 -16
  21. package/bundle/server/dist/coordinator/developer-effect.test.js +291 -12
  22. package/bundle/server/dist/coordinator/human-resolution.js +33 -1
  23. package/bundle/server/dist/coordinator/muse-developer.integration.test.js +291 -55
  24. package/bundle/server/dist/coordinator/muse-spawn.js +72 -164
  25. package/bundle/server/dist/coordinator/projection.js +1 -0
  26. package/bundle/server/dist/coordinator/prompts.js +25 -6
  27. package/bundle/server/dist/coordinator/prompts.test.js +36 -8
  28. package/bundle/server/dist/coordinator/routing.js +1 -0
  29. package/bundle/server/dist/coordinator/routing.test.js +20 -0
  30. package/bundle/server/dist/coordinator/spawn.js +9 -1
  31. package/bundle/server/dist/coordinator/worktree-cwd-guard.js +48 -0
  32. package/bundle/server/dist/coordinator/worktree-cwd-guard.test.js +83 -0
  33. package/bundle/server/dist/coordinator/worktree-owner-liveness.js +6 -1
  34. package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +2 -1
  35. package/bundle/server/dist/repository/human-actions.js +13 -0
  36. package/bundle/server/dist/routes/human-actions.js +10 -1
  37. package/bundle/server/dist/routes/human-actions.test.js +117 -1
  38. package/bundle/server/dist/routes/index.js +12 -5
  39. package/bundle/server/dist/routes/runtime-capacity.test.js +3 -2
  40. package/bundle/server/dist/routes/version.js +30 -0
  41. package/bundle/server/dist/routes/version.test.js +21 -0
  42. package/bundle/server/dist/runners/muse-config-core.js +63 -16
  43. package/bundle/server/dist/runners/muse-config.js +1 -1
  44. package/bundle/server/dist/runners/muse-config.test.js +135 -29
  45. package/bundle/server/dist/runners/spawn-cli.js +17 -3
  46. package/bundle/server/dist/runners/spawn-cli.test.js +59 -0
  47. package/bundle/server/package.json +2 -2
  48. package/bundle/server/static-ui/assets/{index-CYqRh_-S.css → index-BII-LgB8.css} +1 -1
  49. package/bundle/server/static-ui/assets/index-CoDzZcsz.js +60 -0
  50. package/bundle/server/static-ui/index.html +2 -2
  51. package/bundle/shared/dist/agents.d.ts +15 -15
  52. package/bundle/shared/dist/agents.js +6 -0
  53. package/bundle/shared/dist/index.d.ts +7 -7
  54. package/bundle/shared/package.json +1 -1
  55. package/dist/doctor.d.ts +7 -3
  56. package/dist/doctor.js +21 -13
  57. package/dist/doctor.test.js +9 -8
  58. package/dist/managed/index.d.ts +1 -1
  59. package/dist/managed/index.js +1 -1
  60. package/dist/managed/updater.d.ts +16 -6
  61. package/dist/managed/updater.js +28 -11
  62. package/dist/managed-update-restart.test.d.ts +1 -0
  63. package/dist/managed-update-restart.test.js +227 -0
  64. package/dist/ports.d.ts +6 -0
  65. package/dist/ports.js +10 -0
  66. package/dist/runtime-state.d.ts +10 -0
  67. package/dist/runtime-state.js +26 -0
  68. package/dist/start.js +67 -2
  69. package/dist/status.js +30 -2
  70. package/dist/update-check.js +3 -6
  71. package/package.json +1 -1
  72. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +0 -60
@@ -0,0 +1,227 @@
1
+ // NOT-279: a managed update downloaded while Dealer runs stays pending until a safe
2
+ // start/restart, and status reports the version the backend itself is running.
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { spawn } from "node:child_process";
6
+ import fs from "node:fs";
7
+ import net from "node:net";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { readCurrentManagedVersion, readUpdateState, writeUpdateState } from "./managed/index.js";
11
+ import { activateVersion } from "./managed/activate.js";
12
+ import { cliEntryInVersionDir, versionDir } from "./managed/paths.js";
13
+ import { runStart } from "./start.js";
14
+ import { runStatus } from "./status.js";
15
+ import { maybeCheckForUpdateOnRun } from "./update-check.js";
16
+ import { getVersion } from "./version.js";
17
+ const RUNNING = "1.0.0";
18
+ const PENDING = "1.1.0";
19
+ /** Stands in for an agent-dealer backend; `version` null mimics a build that predates /api/version. */
20
+ async function startFakeBackend(version) {
21
+ const script = `
22
+ const http = require("node:http");
23
+ const version = ${JSON.stringify(version)};
24
+ const server = http.createServer((req, res) => {
25
+ res.setHeader("content-type", "application/json");
26
+ if (req.url === "/api/version" && version === null) { res.statusCode = 404; res.end("{}"); return; }
27
+ res.end(JSON.stringify(req.url === "/api/version" ? { version } : { ok: true }));
28
+ });
29
+ server.listen(0, "127.0.0.1", () => console.log(server.address().port));
30
+ `;
31
+ const child = spawn(process.execPath, ["-e", script], { stdio: ["ignore", "pipe", "inherit"] });
32
+ const port = await new Promise((resolve, reject) => {
33
+ child.once("error", reject);
34
+ child.stdout.once("data", (chunk) => resolve(Number(String(chunk).trim())));
35
+ });
36
+ return { child, port };
37
+ }
38
+ async function freePort() {
39
+ return new Promise((resolve) => {
40
+ const server = net.createServer();
41
+ server.listen(0, "127.0.0.1", () => {
42
+ const { port } = server.address();
43
+ server.close(() => resolve(port));
44
+ });
45
+ });
46
+ }
47
+ function isAlive(pid) {
48
+ try {
49
+ process.kill(pid, 0);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ /**
57
+ * A managed home with RUNNING active and PENDING downloaded. The pending CLI is a stub that
58
+ * records its argv, standing in for the new version's `agent-dealer start`.
59
+ */
60
+ function seedManagedHome() {
61
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "agent-dealer-managed-restart-"));
62
+ const reexecLog = path.join(home, "reexec.json");
63
+ process.env.AGENT_DEALER_HOME = home;
64
+ process.env.AGENT_DEALER_LOCAL_BIN = path.join(home, "local-bin");
65
+ for (const version of [RUNNING, PENDING]) {
66
+ const entry = cliEntryInVersionDir(versionDir(version));
67
+ fs.mkdirSync(path.dirname(entry), { recursive: true });
68
+ fs.writeFileSync(entry, `require("node:fs").writeFileSync(${JSON.stringify(reexecLog)}, JSON.stringify({ version: ${JSON.stringify(version)}, argv: process.argv.slice(2) }));\n`);
69
+ }
70
+ activateVersion(RUNNING);
71
+ // Fresh checkedAt: the entry hook's background check must not reach the registry.
72
+ writeUpdateState({ checkedAt: new Date().toISOString(), latest: PENDING, pendingVersion: PENDING });
73
+ return { home, reexecLog };
74
+ }
75
+ async function withManagedHome(env, fn) {
76
+ const keys = ["AGENT_DEALER_HOME", "AGENT_DEALER_LOCAL_BIN", "AGENT_DEALER_DISABLE_AUTOUPDATER", "AGENT_DEALER_SUPERVISOR", "PORT", "WEB_PORT", ...Object.keys(env)];
77
+ const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
78
+ delete process.env.AGENT_DEALER_DISABLE_AUTOUPDATER;
79
+ delete process.env.AGENT_DEALER_SUPERVISOR;
80
+ delete process.env.WEB_PORT;
81
+ for (const [key, value] of Object.entries(env)) {
82
+ if (value === undefined)
83
+ delete process.env[key];
84
+ else
85
+ process.env[key] = value;
86
+ }
87
+ const managed = seedManagedHome();
88
+ try {
89
+ await fn(managed);
90
+ }
91
+ finally {
92
+ for (const [key, value] of Object.entries(saved)) {
93
+ if (value === undefined)
94
+ delete process.env[key];
95
+ else
96
+ process.env[key] = value;
97
+ }
98
+ fs.rmSync(managed.home, { recursive: true, force: true });
99
+ }
100
+ }
101
+ async function captureOutput(fn) {
102
+ const original = { log: console.log, warn: console.warn, error: console.error };
103
+ const lines = [];
104
+ console.log = console.warn = console.error = (message) => {
105
+ lines.push(String(message));
106
+ };
107
+ try {
108
+ const code = await fn();
109
+ return { code, text: lines.join("\n") };
110
+ }
111
+ finally {
112
+ Object.assign(console, original);
113
+ }
114
+ }
115
+ function assertStillPending(managed) {
116
+ assert.equal(readCurrentManagedVersion(), RUNNING, "`current` must not switch under a live backend");
117
+ assert.equal(readUpdateState()?.pendingVersion, PENDING);
118
+ assert.equal(fs.existsSync(managed.reexecLog), false, "the pending version must not be launched");
119
+ }
120
+ test("pending version stays pending while a backend runs; status shows running A and pending B", async () => {
121
+ const { child, port } = await startFakeBackend(RUNNING);
122
+ try {
123
+ await withManagedHome({ PORT: String(port) }, async (managed) => {
124
+ assert.deepEqual(await maybeCheckForUpdateOnRun(), { upgraded: false });
125
+ const start = await captureOutput(() => runStart({ port }));
126
+ assert.equal(start.code, 0);
127
+ assert.match(start.text, /Already running/);
128
+ assert.match(start.text, new RegExp(`${PENDING} is downloaded but not active`));
129
+ assertStillPending(managed);
130
+ const status = await captureOutput(() => runStatus());
131
+ assert.equal(status.code, 0);
132
+ assert.match(status.text, new RegExp(`Agent Dealer version ${getVersion()}`), "NOT-224 installed line is kept");
133
+ assert.match(status.text, new RegExp(`running version ${RUNNING.replace(/\./g, "\\.")}`));
134
+ assert.match(status.text, new RegExp(`Pending +managed version ${PENDING.replace(/\./g, "\\.")}`));
135
+ assert.match(status.text, /Version mismatch: backend is running 1\.0\.0, installed CLI is/);
136
+ assert.match(status.text, /restart after current execution finishes/);
137
+ assertStillPending(managed);
138
+ });
139
+ assert.equal(isAlive(child.pid), true, "observing an update must not stop the backend");
140
+ }
141
+ finally {
142
+ child.kill("SIGKILL");
143
+ }
144
+ });
145
+ test("status says running version unknown for a backend without /api/version", async () => {
146
+ const { child, port } = await startFakeBackend(null);
147
+ try {
148
+ await withManagedHome({ PORT: String(port) }, async () => {
149
+ const status = await captureOutput(() => runStatus());
150
+ assert.equal(status.code, 0);
151
+ assert.match(status.text, /running version unknown/);
152
+ assert.doesNotMatch(status.text, new RegExp(`running version ${getVersion().replace(/\./g, "\\.")}`));
153
+ assert.doesNotMatch(status.text, /Version mismatch/);
154
+ });
155
+ }
156
+ finally {
157
+ child.kill("SIGKILL");
158
+ }
159
+ });
160
+ test("stopped backend: start activates the pending version before spawning and hands off to it", async () => {
161
+ const port = await freePort();
162
+ await withManagedHome({ PORT: String(port) }, async (managed) => {
163
+ const result = await captureOutput(() => runStart({ port }));
164
+ assert.equal(result.code, 0);
165
+ assert.match(result.text, new RegExp(`Activated managed version ${PENDING.replace(/\./g, "\\.")}`));
166
+ assert.equal(readCurrentManagedVersion(), PENDING);
167
+ assert.equal(readUpdateState()?.pendingVersion, null);
168
+ const handoff = JSON.parse(fs.readFileSync(managed.reexecLog, "utf8"));
169
+ assert.equal(handoff.version, PENDING);
170
+ assert.deepEqual(handoff.argv, ["start", "--port", String(port)]);
171
+ });
172
+ });
173
+ test("stopped backend: status reports the pending version for the next start", async () => {
174
+ const port = await freePort();
175
+ await withManagedHome({ PORT: String(port) }, async () => {
176
+ const status = await captureOutput(() => runStatus());
177
+ assert.equal(status.code, 1);
178
+ assert.match(status.text, /Status: not running/);
179
+ assert.match(status.text, /Update pending: 1\.1\.0 activates on the next `agent-dealer start`/);
180
+ });
181
+ });
182
+ test("start refuses to activate while the server's own pid marker is live, even if /health is down", async () => {
183
+ const port = await freePort();
184
+ await withManagedHome({ PORT: String(port) }, async (managed) => {
185
+ fs.writeFileSync(path.join(managed.home, "server.pid"), JSON.stringify({ pid: process.pid, port }));
186
+ const result = await captureOutput(() => runStart({ port }));
187
+ assert.equal(result.code, 1);
188
+ assert.match(result.text, /not activating/);
189
+ assertStillPending(managed);
190
+ });
191
+ });
192
+ for (const daemon of [false, true]) {
193
+ const label = daemon ? "start --daemon --force" : "start --force";
194
+ test(`${label}: failed stop leaves the pending version inactive and starts nothing`, async () => {
195
+ const { child, port } = await startFakeBackend(RUNNING);
196
+ try {
197
+ // No run.json in an isolated home = no proof of ownership, so stop leaves the listener up.
198
+ await withManagedHome({ PORT: String(port) }, async (managed) => {
199
+ const result = await captureOutput(() => runStart({ force: true, daemon, port }));
200
+ assert.equal(result.code, 1);
201
+ assertStillPending(managed);
202
+ assert.equal(fs.existsSync(path.join(managed.home, "run.json")), false);
203
+ });
204
+ assert.equal(isAlive(child.pid), true);
205
+ }
206
+ finally {
207
+ child.kill("SIGKILL");
208
+ }
209
+ });
210
+ test(`${label}: successful stop activates and starts the pending version`, async () => {
211
+ const { child, port } = await startFakeBackend(RUNNING);
212
+ try {
213
+ await withManagedHome({ PORT: String(port) }, async (managed) => {
214
+ fs.writeFileSync(path.join(managed.home, "run.json"), JSON.stringify({ host: "127.0.0.1", port, serverPid: child.pid, cliPid: 2 ** 22, startedAt: new Date().toISOString() }));
215
+ const result = await captureOutput(() => runStart({ force: true, daemon, port }));
216
+ assert.equal(result.code, 0, result.text);
217
+ assert.equal(readCurrentManagedVersion(), PENDING);
218
+ const handoff = JSON.parse(fs.readFileSync(managed.reexecLog, "utf8"));
219
+ assert.equal(handoff.version, PENDING);
220
+ assert.deepEqual(handoff.argv, ["start", "--force", "--port", String(port), ...(daemon ? ["--daemon"] : [])]);
221
+ });
222
+ }
223
+ finally {
224
+ child.kill("SIGKILL");
225
+ }
226
+ });
227
+ }
package/dist/ports.d.ts CHANGED
@@ -6,3 +6,9 @@ export declare function isTcpPortOpen(host: string, port: number): Promise<boole
6
6
  export declare function probeAgentDealer(host: string, port: number): Promise<AgentDealerProbe>;
7
7
  export declare function listListeningPids(port: number): number[];
8
8
  export declare function formatPortConflict(port: number, label: string, host: string, isAgentDealer: boolean): string;
9
+ /**
10
+ * NOT-279: the version the live backend reports about itself, or null when it is down, the
11
+ * request fails, or it predates `/api/version`. Callers must show "unknown" for null —
12
+ * never substitute the installed CLI version, which may be ahead of the running server.
13
+ */
14
+ export declare function fetchRunningBackendVersion(host: string, port: number): Promise<string | null>;
package/dist/ports.js CHANGED
@@ -70,3 +70,13 @@ export function formatPortConflict(port, label, host, isAgentDealer) {
70
70
  return (`Port ${port} (${label}) is in use by another program on ${host}.${pidHint}\n` +
71
71
  ` • Free the port, or start on a different port: agent-dealer start --port <port>`);
72
72
  }
73
+ /**
74
+ * NOT-279: the version the live backend reports about itself, or null when it is down, the
75
+ * request fails, or it predates `/api/version`. Callers must show "unknown" for null —
76
+ * never substitute the installed CLI version, which may be ahead of the running server.
77
+ */
78
+ export async function fetchRunningBackendVersion(host, port) {
79
+ const body = await fetchJson(`http://${host}:${port}/api/version`);
80
+ const version = body?.version;
81
+ return typeof version === "string" && version.trim() ? version : null;
82
+ }
@@ -10,3 +10,13 @@ export declare function readRunState(): RunState | null;
10
10
  export declare function writeRunState(state: RunState): void;
11
11
  export declare function clearRunState(): void;
12
12
  export declare function isProcessAlive(pid: number): boolean;
13
+ /** The server's own liveness marker (packages/server/src/server-liveness.ts), written by
14
+ * every launch mode — not only by this CLI's supervisor like run.json. */
15
+ export declare function serverPidMarkerPath(): string;
16
+ /**
17
+ * NOT-279: whether a backend may still be serving this home — a healthy agent-dealer on the
18
+ * port, or a live owner of the server's pid marker (a backend still booting or draining
19
+ * holds the marker before/after it answers /health). Gate for switching the managed
20
+ * `current` version: activation must only happen when this is false.
21
+ */
22
+ export declare function isBackendLive(host: string, port: number): Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { prodHomeDir } from "./env.js";
4
+ import { probeAgentDealer } from "./ports.js";
4
5
  export function runStatePath() {
5
6
  return path.join(prodHomeDir(), "run.json");
6
7
  }
@@ -37,3 +38,28 @@ export function isProcessAlive(pid) {
37
38
  return false;
38
39
  }
39
40
  }
41
+ /** The server's own liveness marker (packages/server/src/server-liveness.ts), written by
42
+ * every launch mode — not only by this CLI's supervisor like run.json. */
43
+ export function serverPidMarkerPath() {
44
+ return path.join(prodHomeDir(), "server.pid");
45
+ }
46
+ function isServerPidMarkerLive() {
47
+ try {
48
+ const raw = JSON.parse(fs.readFileSync(serverPidMarkerPath(), "utf8"));
49
+ return typeof raw.pid === "number" && isProcessAlive(raw.pid);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * NOT-279: whether a backend may still be serving this home — a healthy agent-dealer on the
57
+ * port, or a live owner of the server's pid marker (a backend still booting or draining
58
+ * holds the marker before/after it answers /health). Gate for switching the managed
59
+ * `current` version: activation must only happen when this is false.
60
+ */
61
+ export async function isBackendLive(host, port) {
62
+ if ((await probeAgentDealer(host, port)).up)
63
+ return true;
64
+ return isServerPidMarkerLive();
65
+ }
package/dist/start.js CHANGED
@@ -3,7 +3,8 @@ import { loadProdEnvFile, prodEnvFilePath, prodHomeDir, resolveBundledListenPort
3
3
  import { appendDaemonLogLine, openDaemonLogFd, resolveCliEntry, resolveDaemonLogPath, resolveDaemonLogsDir, } from "./daemon-logs.js";
4
4
  import { formatPortConflict, isTcpPortOpen, probeAgentDealer } from "./ports.js";
5
5
  import { resolveServerEntry, resolveUiDist } from "./paths.js";
6
- import { clearRunState, writeRunState } from "./runtime-state.js";
6
+ import { clearRunState, isBackendLive, writeRunState } from "./runtime-state.js";
7
+ import { activatePendingVersion, cliEntryInVersionDir, readPendingManagedVersion, versionDir, } from "./managed/index.js";
7
8
  import { runStop } from "./stop.js";
8
9
  import { getVersion } from "./version.js";
9
10
  const children = [];
@@ -80,7 +81,10 @@ function printRunningEndpoints(base, uiDist) {
80
81
  console.log("");
81
82
  }
82
83
  function buildSupervisorArgs(options) {
83
- const args = ["start", "--_supervisor"];
84
+ return ["start", "--_supervisor", ...buildStartFlags(options)];
85
+ }
86
+ function buildStartFlags(options) {
87
+ const args = [];
84
88
  if (options.open) {
85
89
  args.push("--open");
86
90
  }
@@ -117,6 +121,57 @@ async function runDaemonLauncher(options) {
117
121
  console.log("");
118
122
  return 0;
119
123
  }
124
+ function printPendingWhileRunning() {
125
+ const pending = readPendingManagedVersion();
126
+ if (!pending)
127
+ return;
128
+ console.log(`Managed version ${pending} is downloaded but not active while this backend runs. ` +
129
+ "Restart after current execution finishes: agent-dealer start --force");
130
+ }
131
+ /** Runs `start` from the freshly activated version's CLI so its own server bundle is spawned. */
132
+ function reexecStart(cliEntry, args) {
133
+ return new Promise((resolve) => {
134
+ const child = spawn(process.execPath, [cliEntry, ...args], { stdio: "inherit", env: process.env });
135
+ const forward = (signal) => () => {
136
+ if (!child.killed)
137
+ child.kill(signal);
138
+ };
139
+ process.on("SIGINT", forward("SIGINT"));
140
+ process.on("SIGTERM", forward("SIGTERM"));
141
+ child.on("error", (err) => {
142
+ console.error(`[agent-dealer] Could not launch ${cliEntry}: ${err.message}`);
143
+ resolve(1);
144
+ });
145
+ child.on("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
146
+ });
147
+ }
148
+ /**
149
+ * NOT-279: a managed update downloaded while Dealer ran stays pending until here — after the
150
+ * already-running check and any `--force` stop succeeded. Re-checks liveness (health probe +
151
+ * the server's own pid marker) so `current` never switches under a live backend, then hands
152
+ * off to the new version's CLI. Returns null when there is nothing to activate (continue in
153
+ * this process), otherwise the exit code to return.
154
+ */
155
+ async function activatePendingBeforeSpawn(host, port, options) {
156
+ if (isSupervisorMode(options))
157
+ return null;
158
+ const pending = readPendingManagedVersion();
159
+ if (!pending)
160
+ return null;
161
+ if (await isBackendLive(host, port)) {
162
+ console.error(`[agent-dealer] Managed version ${pending} is pending, but a backend is still live for this home — not activating.`);
163
+ console.error("[agent-dealer] Stop it first (agent-dealer stop) or restart with: agent-dealer start --force");
164
+ return 1;
165
+ }
166
+ const { activated } = activatePendingVersion();
167
+ if (!activated)
168
+ return null;
169
+ console.log(`[agent-dealer] Activated managed version ${activated}`);
170
+ const args = ["start", ...buildStartFlags(options)];
171
+ if (options.daemon)
172
+ args.push("--daemon");
173
+ return reexecStart(cliEntryInVersionDir(versionDir(activated)), args);
174
+ }
120
175
  async function ensurePortAvailable(host, port, probe) {
121
176
  const portBusy = await isTcpPortOpen(host, port);
122
177
  if (portBusy && !probe.up) {
@@ -172,6 +227,7 @@ export async function runStart(options = {}) {
172
227
  else {
173
228
  printRunningEndpoints(base, uiDist);
174
229
  console.log("Already running. Use `agent-dealer stop` or `agent-dealer start --daemon --force` to restart.");
230
+ printPendingWhileRunning();
175
231
  return 0;
176
232
  }
177
233
  }
@@ -179,6 +235,10 @@ export async function runStart(options = {}) {
179
235
  if (portError !== null) {
180
236
  return portError;
181
237
  }
238
+ const handoff = await activatePendingBeforeSpawn(host, port, options);
239
+ if (handoff !== null) {
240
+ return handoff;
241
+ }
182
242
  return runDaemonLauncher(options);
183
243
  }
184
244
  const ioMode = supervisor ? "file" : "inherit";
@@ -193,6 +253,7 @@ export async function runStart(options = {}) {
193
253
  else {
194
254
  printRunningEndpoints(base, uiDist);
195
255
  console.log("Already running. Use `agent-dealer stop` or `agent-dealer start --force` to restart.");
256
+ printPendingWhileRunning();
196
257
  return 0;
197
258
  }
198
259
  }
@@ -200,6 +261,10 @@ export async function runStart(options = {}) {
200
261
  if (portError !== null) {
201
262
  return portError;
202
263
  }
264
+ const handoff = await activatePendingBeforeSpawn(host, port, options);
265
+ if (handoff !== null) {
266
+ return handoff;
267
+ }
203
268
  if (!uiDist) {
204
269
  const warnUi = "[agent-dealer] Dashboard UI bundle not found. API will still start.";
205
270
  const warnDist = "[agent-dealer] Set AGENT_DEALER_UI_DIST or run from a published npm package build.";
package/dist/status.js CHANGED
@@ -1,24 +1,52 @@
1
- import { formatPortConflict, isTcpPortOpen, probeAgentDealer } from "./ports.js";
1
+ import { fetchRunningBackendVersion, formatPortConflict, isTcpPortOpen, probeAgentDealer } from "./ports.js";
2
2
  import { isProcessAlive, readRunState } from "./runtime-state.js";
3
3
  import { getVersion } from "./version.js";
4
4
  import { loadProdEnvFile, resolveBundledListenPort } from "./env.js";
5
+ import { readPendingManagedVersion } from "./managed/index.js";
6
+ const RESTART_ACTION = "restart after current execution finishes: agent-dealer start --force";
5
7
  export async function runStatus() {
6
8
  loadProdEnvFile();
7
9
  const host = "127.0.0.1";
8
10
  const state = readRunState();
9
11
  const port = state?.port ?? resolveBundledListenPort();
10
12
  const probe = await probeAgentDealer(host, port);
11
- console.log(`Agent Dealer version ${getVersion()}`);
13
+ const installed = getVersion();
14
+ console.log(`Agent Dealer version ${installed}`);
12
15
  console.log(`Configured host ${host} port :${port}`);
13
16
  console.log("");
17
+ // NOT-279: the running version comes only from the backend itself. The installed CLI (and the
18
+ // managed `current` link) can be ahead of a server that was started before an update.
19
+ const running = probe.up ? await fetchRunningBackendVersion(host, port) : null;
20
+ const pending = readPendingManagedVersion();
14
21
  if (probe.up) {
15
22
  console.log("Status: running");
16
23
  console.log(` Dashboard ${probe.url}`);
17
24
  console.log(` API health ${probe.url}/health`);
25
+ console.log(running
26
+ ? ` Backend running version ${running}`
27
+ : " Backend running version unknown (backend did not report it — installed CLI version is not assumed)");
18
28
  }
19
29
  else {
20
30
  console.log("Status: not running");
21
31
  }
32
+ console.log(` Installed CLI version ${installed}`);
33
+ if (pending) {
34
+ console.log(` Pending managed version ${pending} (downloaded, not active)`);
35
+ }
36
+ if (probe.up) {
37
+ if (running && running !== installed) {
38
+ console.log("");
39
+ console.log(`Version mismatch: backend is running ${running}, installed CLI is ${installed} — ${RESTART_ACTION}`);
40
+ }
41
+ if (pending) {
42
+ console.log("");
43
+ console.log(`Update pending: ${pending} activates only on restart — ${RESTART_ACTION}`);
44
+ }
45
+ }
46
+ else if (pending) {
47
+ console.log("");
48
+ console.log(`Update pending: ${pending} activates on the next \`agent-dealer start\`.`);
49
+ }
22
50
  if (state) {
23
51
  console.log("");
24
52
  console.log("Last run.json:");
@@ -44,12 +44,9 @@ function writeCache(next) {
44
44
  /** npm-global / unknown path — keep TTY prompt behavior. Managed uses hooks instead. */
45
45
  export async function maybeCheckForUpdateOnRun() {
46
46
  if (detectInstallKind() === "managed") {
47
- const { activated } = runManagedCliEntryHooks({ allowActivate: true });
48
- if (activated) {
49
- console.log(`[agent-dealer] Activated managed version ${activated}`);
50
- console.log("Re-run your command to use the new version.");
51
- return { upgraded: true };
52
- }
47
+ // NOT-279: download only. A pending version activates in `agent-dealer start`, once no
48
+ // backend is live — never from an arbitrary command while Dealer may be serving work.
49
+ runManagedCliEntryHooks();
53
50
  return { upgraded: false };
54
51
  }
55
52
  if (process.env.AGENT_DEALER_DISABLE_UPGRADE_CHECK === "1") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dealer",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Human control plane for agent execution — queue, plan approval, audit",
5
5
  "license": "MIT",
6
6
  "type": "module",