ai-agentmanager 1.3.0 → 1.3.1
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.
- package/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli.js +31 -12
- package/src/client.js +46 -6
- package/src/manager.js +13 -12
- package/src/server.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.1 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Restart comes back:** `agentmgr restart` waits for the old daemon to exit (it closes every agent first, which takes a while with many), instead of giving up after 20 seconds and reporting success with no daemon left. `serve` started while one is shutting down waits for it instead of exiting, and `shutdown` returns once the daemon has stopped. `/api/health` says `closing` while it shuts down.
|
|
6
|
+
- **Pull request status is current:** checking an agent's pull requests while a check is already running (after an update, a stop or a push) waits for it and checks again, instead of returning what that earlier check read, so a pull request ready to merge could still show as needing fixes.
|
|
7
|
+
|
|
3
8
|
## 1.3.0 (2026-10-31)
|
|
4
9
|
|
|
5
10
|
- **Review changes:** each agent's changes in one screen: changed files, the diff file by file (new files included), line comments sent to the agent as one message, **Commit all**, and **Push & open PR** with `gh`. `agentmgr changes`, `comment`, `commit`, `pr`.
|
package/README.md
CHANGED
|
@@ -83,13 +83,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
83
83
|
|
|
84
84
|
```sh
|
|
85
85
|
npm install -g ai-agentmanager # the latest release
|
|
86
|
-
npm install -g ai-agentmanager@1.3.
|
|
86
|
+
npm install -g ai-agentmanager@1.3.1 # or a specific one
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Or from the repository, at a release tag:
|
|
90
90
|
|
|
91
91
|
```sh
|
|
92
|
-
git clone --branch v1.3.
|
|
92
|
+
git clone --branch v1.3.1 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
93
93
|
cd ai-agentmanager-core
|
|
94
94
|
npm install
|
|
95
95
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -109,7 +109,7 @@ npm link
|
|
|
109
109
|
| Installed with | Update |
|
|
110
110
|
| --- | --- |
|
|
111
111
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
112
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.3.
|
|
112
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.3.1 && npm install` (the tag you want) |
|
|
113
113
|
| A clone of `main` | `git pull && npm install` |
|
|
114
114
|
|
|
115
115
|
**Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { Command, Option } from 'commander';
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import { HOST, PORT } from './config.js';
|
|
6
|
-
import { api, BASE, ensureDaemon, isRunning } from './client.js';
|
|
6
|
+
import { api, BASE, ensureDaemon, health, isRunning, waitForExit } from './client.js';
|
|
7
7
|
import { about, latestRelease } from './about.js';
|
|
8
8
|
|
|
9
9
|
const tty = process.stdout.isTTY;
|
|
@@ -271,10 +271,16 @@ export async function main(argv) {
|
|
|
271
271
|
.command('serve')
|
|
272
272
|
.description('Run the agent daemon (API + web UI) in the foreground')
|
|
273
273
|
.action(async () => {
|
|
274
|
-
|
|
274
|
+
const h = await health();
|
|
275
|
+
if (h && !h.closing) {
|
|
275
276
|
// Clean exit, so a login service doesn't keep retrying while another daemon owns the port.
|
|
276
277
|
return console.log(`[agentmgr] a daemon is already running at ${BASE}; not starting another`);
|
|
277
278
|
}
|
|
279
|
+
if (h?.closing) {
|
|
280
|
+
// One that is shutting down closes its agents first; start once it has exited.
|
|
281
|
+
console.log(`[agentmgr] waiting for the previous daemon (pid ${h.pid}) to finish shutting down`);
|
|
282
|
+
if (!(await waitForExit(h.pid))) return console.log(`[agentmgr] the previous daemon (pid ${h.pid}) is still shutting down; not starting`);
|
|
283
|
+
}
|
|
278
284
|
const { startServer } = await import('./server.js');
|
|
279
285
|
const { url } = await startServer();
|
|
280
286
|
console.log(`[agentmgr] listening on ${url}`);
|
|
@@ -1789,10 +1795,11 @@ export async function main(argv) {
|
|
|
1789
1795
|
.description('Register the daemon with launchd (macOS) or systemd --user (Linux) and start it')
|
|
1790
1796
|
.action(async () => {
|
|
1791
1797
|
const svc = await import('./service.js');
|
|
1792
|
-
|
|
1798
|
+
const h = await health();
|
|
1799
|
+
if (h) {
|
|
1793
1800
|
// Hand over to the service-managed daemon; running agents are resumed by it.
|
|
1794
|
-
await api('POST', '/shutdown');
|
|
1795
|
-
|
|
1801
|
+
if (!h.closing) await api('POST', '/shutdown');
|
|
1802
|
+
await waitForExit(h.pid, { onWait: (s) => console.log(dim(`closing agents… (${s}s)`)) });
|
|
1796
1803
|
}
|
|
1797
1804
|
const res = await svc.install();
|
|
1798
1805
|
for (let i = 0; i < 100 && !(await isRunning()); i++) await new Promise((r) => setTimeout(r, 200));
|
|
@@ -2001,7 +2008,8 @@ export async function main(argv) {
|
|
|
2001
2008
|
.description('Flush, stop the daemon, and start it again; running agents resume their sessions')
|
|
2002
2009
|
.action(async () => {
|
|
2003
2010
|
const svc = await import('./service.js');
|
|
2004
|
-
|
|
2011
|
+
const before = await health();
|
|
2012
|
+
if (before && !before.closing) {
|
|
2005
2013
|
try {
|
|
2006
2014
|
const r = await api('POST', '/flush');
|
|
2007
2015
|
console.log(dim(`saved ${r.agents} agents, scrollback for ${r.scrollback}`));
|
|
@@ -2011,13 +2019,21 @@ export async function main(argv) {
|
|
|
2011
2019
|
console.log(dim('running daemon is an older version (no flush); restarting it to upgrade'));
|
|
2012
2020
|
}
|
|
2013
2021
|
await api('POST', '/shutdown');
|
|
2014
|
-
|
|
2022
|
+
}
|
|
2023
|
+
// Wait for the old process itself to exit: it answers until every agent is closed.
|
|
2024
|
+
if (before?.pid) {
|
|
2025
|
+
const gone = await waitForExit(before.pid, { onWait: (s) => console.log(dim(`closing agents… (${s}s)`)) });
|
|
2026
|
+
if (!gone) throw new Error(`The daemon (pid ${before.pid}) is still shutting down after 5 minutes; check the log`);
|
|
2015
2027
|
}
|
|
2016
2028
|
const st = await svc.status();
|
|
2017
2029
|
if (st.loaded) await svc.kickstart();
|
|
2018
2030
|
else await ensureDaemon({ quiet: true });
|
|
2019
|
-
|
|
2020
|
-
|
|
2031
|
+
const back = async () => {
|
|
2032
|
+
const h = await health();
|
|
2033
|
+
return Boolean(h && !h.closing && h.pid !== before?.pid);
|
|
2034
|
+
};
|
|
2035
|
+
for (let i = 0; i < 150 && !(await back()); i++) await new Promise((res) => setTimeout(res, 200));
|
|
2036
|
+
if (!(await back())) throw new Error('Daemon did not come back; check the log');
|
|
2021
2037
|
const agents = await api('GET', '/agents');
|
|
2022
2038
|
console.log(`${green('✓')} restarted${st.loaded ? ' (login service)' : ''}; ${agents.filter((a) => a.desired === 'running').length} agents set to resume`);
|
|
2023
2039
|
});
|
|
@@ -2026,9 +2042,12 @@ export async function main(argv) {
|
|
|
2026
2042
|
.command('shutdown')
|
|
2027
2043
|
.description('Stop the daemon (running agents are closed; sessions are kept)')
|
|
2028
2044
|
.action(async () => {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2045
|
+
const h = await health();
|
|
2046
|
+
if (!h) return console.log('Daemon is not running');
|
|
2047
|
+
if (!h.closing) await api('POST', '/shutdown');
|
|
2048
|
+
// It closes every agent before it exits; say stopped only once it has.
|
|
2049
|
+
const gone = await waitForExit(h.pid, { onWait: (s) => console.log(dim(`closing agents… (${s}s)`)) });
|
|
2050
|
+
console.log(gone ? 'Daemon stopped' : yellow(`The daemon (pid ${h.pid}) is still shutting down; check the log`));
|
|
2032
2051
|
});
|
|
2033
2052
|
|
|
2034
2053
|
try {
|
package/src/client.js
CHANGED
|
@@ -19,18 +19,58 @@ export async function api(method, route, body) {
|
|
|
19
19
|
return data;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** The daemon's /health answer ({ ok, pid, closing, … }), or null when nothing answers. */
|
|
23
|
+
export async function health() {
|
|
24
|
+
try {
|
|
25
|
+
return await api('GET', '/health');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A daemon that answers and isn't shutting down. */
|
|
22
32
|
export async function isRunning() {
|
|
33
|
+
const h = await health();
|
|
34
|
+
return Boolean(h && !h.closing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const alive = (pid) => {
|
|
23
38
|
try {
|
|
24
|
-
|
|
39
|
+
process.kill(pid, 0);
|
|
25
40
|
return true;
|
|
26
|
-
} catch {
|
|
27
|
-
return
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return e.code === 'EPERM';
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wait for a daemon process to exit. Shutting down closes every agent first (to save their
|
|
48
|
+
* sessions), which takes a while with many agents, and it answers /health until then.
|
|
49
|
+
* Returns true once it is gone, false after `timeoutMs`.
|
|
50
|
+
*/
|
|
51
|
+
export async function waitForExit(pid, { timeoutMs = 300_000, onWait = null } = {}) {
|
|
52
|
+
if (!pid) return true;
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
let told = 0;
|
|
55
|
+
while (alive(pid)) {
|
|
56
|
+
if (Date.now() - start > timeoutMs) return false;
|
|
57
|
+
if (onWait && Date.now() - told > 10_000) {
|
|
58
|
+
told = Date.now();
|
|
59
|
+
onWait(Math.round((Date.now() - start) / 1000));
|
|
60
|
+
}
|
|
61
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
28
62
|
}
|
|
63
|
+
return true;
|
|
29
64
|
}
|
|
30
65
|
|
|
31
|
-
/** Start the daemon in the background if it is not already listening. */
|
|
66
|
+
/** Start the daemon in the background if it is not already listening (waiting out one that is shutting down). */
|
|
32
67
|
export async function ensureDaemon({ quiet = false } = {}) {
|
|
33
|
-
|
|
68
|
+
const h = await health();
|
|
69
|
+
if (h && !h.closing) return false;
|
|
70
|
+
if (h?.closing) {
|
|
71
|
+
if (!quiet) process.stderr.write(`agentmgr: waiting for the daemon (pid ${h.pid}) to finish shutting down…\n`);
|
|
72
|
+
if (!(await waitForExit(h.pid))) throw new Error(`The daemon (pid ${h.pid}) is still shutting down; try again in a minute`);
|
|
73
|
+
}
|
|
34
74
|
ensureHome();
|
|
35
75
|
const log = fs.openSync(paths.log, 'a');
|
|
36
76
|
const child = spawn(process.execPath, [BIN, 'serve'], {
|
|
@@ -39,7 +79,7 @@ export async function ensureDaemon({ quiet = false } = {}) {
|
|
|
39
79
|
env: process.env,
|
|
40
80
|
});
|
|
41
81
|
child.unref();
|
|
42
|
-
for (let i = 0; i <
|
|
82
|
+
for (let i = 0; i < 150; i++) {
|
|
43
83
|
await new Promise((r) => setTimeout(r, 100));
|
|
44
84
|
if (await isRunning()) {
|
|
45
85
|
if (!quiet) process.stderr.write(`agentmgr: started daemon at ${BASE} (log: ${paths.log})\n`);
|
package/src/manager.js
CHANGED
|
@@ -1991,18 +1991,19 @@ export class AgentManager extends EventEmitter {
|
|
|
1991
1991
|
const id = this.mustGet(ref).id;
|
|
1992
1992
|
const running = this.refreshing.get(id);
|
|
1993
1993
|
if (running) {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
1994
|
+
if (!withPrs) return running.promise.then(() => this.store.get(id) && this.view(this.store.get(id)));
|
|
1995
|
+
// A pull request check runs again after the one in progress, which may have read PRs from
|
|
1996
|
+
// before this request; callers arriving meanwhile share that next run.
|
|
1997
|
+
running.next ??= running.promise.catch(() => {}).then(() => this.refresh(id, { withPrs }));
|
|
1998
|
+
return running.next;
|
|
1999
|
+
}
|
|
2000
|
+
// Cleared before anyone waiting on it continues, so the next run starts afresh.
|
|
2001
|
+
const entry = { withPrs };
|
|
2002
|
+
entry.promise = this.doRefresh(id, { withPrs }).finally(() => {
|
|
2003
|
+
if (this.refreshing.get(id) === entry) this.refreshing.delete(id);
|
|
2004
|
+
});
|
|
2005
|
+
this.refreshing.set(id, entry);
|
|
2006
|
+
return entry.promise;
|
|
2006
2007
|
}
|
|
2007
2008
|
|
|
2008
2009
|
async doRefresh(id, { withPrs }) {
|
package/src/server.js
CHANGED
|
@@ -193,7 +193,8 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
193
193
|
};
|
|
194
194
|
const fail = (status, message) => Object.assign(new Error(message), { status });
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
// `closing` while shutting down: it still answers until every agent is closed, then exits.
|
|
197
|
+
api.get('/health', wrap(() => ({ ok: true, pid: process.pid, home: paths.home, claudeBin: config.claudeBin, closing })));
|
|
197
198
|
api.get('/agents', wrap(() => manager.list()));
|
|
198
199
|
api.get('/engines', wrap(() => listEngines(config)));
|
|
199
200
|
api.post(
|