@decentnetwork/beagle 0.1.89 → 0.1.91
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/dist/cli.js +18 -1
- package/dist/desktop/app.js +9 -5
- package/dist/relaunch.d.ts +8 -0
- package/dist/relaunch.js +85 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +32 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { dirname, join, resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { startBeagleServer } from "./server.js";
|
|
11
|
+
import { startBeagleServer, installMode } from "./server.js";
|
|
12
|
+
import { isSupervised, relaunchSelf, waitForPredecessor } from "./relaunch.js";
|
|
12
13
|
import { openPeerHost, defaultConfigDir, decentlanCarrierDir, makeDaemonHost } from "./peer-host.js";
|
|
13
14
|
import { SwitchablePeerHost } from "./lan-handoff.js";
|
|
14
15
|
import { loadNodeConfig, saveAutoAccept } from "./node-config.js";
|
|
@@ -97,6 +98,9 @@ const resolveVer = (pkg) => {
|
|
|
97
98
|
};
|
|
98
99
|
async function main() {
|
|
99
100
|
const args = parseArgs(process.argv.slice(2));
|
|
101
|
+
// Spawned by a predecessor restarting itself after an update: let it finish
|
|
102
|
+
// exiting before touching the identity lock or the port. No-op otherwise.
|
|
103
|
+
await waitForPredecessor();
|
|
100
104
|
// Bootstrap list: prefer the user's decentlan config so both stacks agree on
|
|
101
105
|
// the fleet, else fall back to the shipped defaults. A wrong list here is the
|
|
102
106
|
// difference between "joins the network" and "silently alone".
|
|
@@ -189,6 +193,19 @@ async function main() {
|
|
|
189
193
|
},
|
|
190
194
|
listenHost: args.host,
|
|
191
195
|
listenPort: args.port,
|
|
196
|
+
// After an update: stop the peer cleanly, then either let the supervisor
|
|
197
|
+
// relaunch us or, with none, spawn our own successor. The successor waits
|
|
198
|
+
// for this pid to be gone (waitForPredecessor above), so the identity
|
|
199
|
+
// lock and the port are free by the time it needs them.
|
|
200
|
+
onRestart: async () => {
|
|
201
|
+
await Promise.race([
|
|
202
|
+
peerHost.stop().catch(() => undefined),
|
|
203
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
204
|
+
]);
|
|
205
|
+
if (!isSupervised())
|
|
206
|
+
relaunchSelf(installMode());
|
|
207
|
+
process.exit(0);
|
|
208
|
+
},
|
|
192
209
|
});
|
|
193
210
|
const shutdown = async () => {
|
|
194
211
|
await peerHost.stop().catch(() => undefined);
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.91";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -380,11 +380,13 @@ window.__DK_UI_VERSION="0.1.89";
|
|
|
380
380
|
const t = setInterval(refresh, 2500);
|
|
381
381
|
return () => clearInterval(t);
|
|
382
382
|
}, [refresh]);
|
|
383
|
+
const inFlight = React.useRef(/* @__PURE__ */ new Set());
|
|
383
384
|
const loadThread = React.useCallback(async (peerId) => {
|
|
384
|
-
if (!peerId)
|
|
385
|
+
if (!peerId || inFlight.current.has(peerId))
|
|
385
386
|
return;
|
|
387
|
+
inFlight.current.add(peerId);
|
|
386
388
|
try {
|
|
387
|
-
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId));
|
|
389
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=200");
|
|
388
390
|
const arr = d.chats && d.chats[peerId] || [];
|
|
389
391
|
await prefetchThreadFiles(arr);
|
|
390
392
|
const msgs = arr.map((m) => ({
|
|
@@ -417,6 +419,8 @@ window.__DK_UI_VERSION="0.1.89";
|
|
|
417
419
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
418
420
|
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
419
421
|
} catch (e) {
|
|
422
|
+
} finally {
|
|
423
|
+
inFlight.current.delete(peerId);
|
|
420
424
|
}
|
|
421
425
|
}, []);
|
|
422
426
|
return Object.assign({}, snap, { threads, refresh, loadThread });
|
|
@@ -4751,7 +4755,7 @@ ${peer.address}`
|
|
|
4751
4755
|
updRestart: "Restart",
|
|
4752
4756
|
updBusy: "Updating\u2026 (about a minute)",
|
|
4753
4757
|
updDone: "Updated \u2014 restart to apply.",
|
|
4754
|
-
updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If
|
|
4758
|
+
updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If it has not come back after a minute, run beagle again.)",
|
|
4755
4759
|
updNpx: "Running via npx \u2014 restarting picks up the latest version automatically.",
|
|
4756
4760
|
updDev: "Running from a source checkout \u2014 update with:",
|
|
4757
4761
|
updPeerDaemon: "The peer version belongs to the running agentnet daemon, not to this app. Update it with:",
|
|
@@ -4933,7 +4937,7 @@ ${peer.address}`
|
|
|
4933
4937
|
updRestart: "\u91CD\u542F",
|
|
4934
4938
|
updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
|
|
4935
4939
|
updDone: "\u66F4\u65B0\u5B8C\u6210\u2014\u2014\u91CD\u542F\u540E\u751F\u6548\u3002",
|
|
4936
|
-
updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C
|
|
4940
|
+
updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C\u4E00\u5206\u949F\u540E\u8FD8\u6CA1\u56DE\u6765,\u8BF7\u91CD\u65B0\u8FD0\u884C\u4E00\u6B21 beagle\u3002)",
|
|
4937
4941
|
updNpx: "\u4F60\u662F\u7528 npx \u8FD0\u884C\u7684\u2014\u2014\u91CD\u542F\u5373\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7248\u3002",
|
|
4938
4942
|
updDev: "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C,\u8BF7\u624B\u52A8\u66F4\u65B0:",
|
|
4939
4943
|
updPeerDaemon: "peer \u7684\u7248\u672C\u5C5E\u4E8E\u6B63\u5728\u8FD0\u884C\u7684 agentnet daemon,\u4E0D\u5C5E\u4E8E\u672C\u5E94\u7528\u3002\u8BF7\u7528\u8FD9\u6761\u547D\u4EE4\u66F4\u65B0:",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Something outside this process will restart it when it exits. */
|
|
2
|
+
export declare function isSupervised(): boolean;
|
|
3
|
+
/** Start the successor. Caller exits right after. */
|
|
4
|
+
export declare function relaunchSelf(mode: "global" | "npx" | "dev" | "unknown"): void;
|
|
5
|
+
/** In the successor: block until the process that spawned us has exited.
|
|
6
|
+
* No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
|
|
7
|
+
* than hang forever behind a predecessor that will not die. */
|
|
8
|
+
export declare function waitForPredecessor(): Promise<void>;
|
package/dist/relaunch.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// relaunch.ts — bring Beagle back after a self-update when nobody else will.
|
|
2
|
+
//
|
|
3
|
+
// "Restart" after an update used to be process.exit(0) and a hope: pm2,
|
|
4
|
+
// launchd and systemd relaunch a process that exits, so under any of them
|
|
5
|
+
// that was a restart. A `beagle` started by hand in a terminal has no such
|
|
6
|
+
// minder — it exited and stayed exited, the page sat on "Restarting…"
|
|
7
|
+
// forever, and the person had to notice and run it again (air-wli,
|
|
8
|
+
// 2026-09-12). The dialog even admitted as much in parentheses.
|
|
9
|
+
//
|
|
10
|
+
// When no supervisor is detected, spawn our own successor before exiting:
|
|
11
|
+
// the same node, the same script path (which after `npm i -g` now holds the
|
|
12
|
+
// new version), the same arguments, detached so our exit does not take it
|
|
13
|
+
// down. The successor is told our pid and waits for us to be gone before it
|
|
14
|
+
// opens the peer or binds the port, so it never races us for the identity
|
|
15
|
+
// lock or gets EADDRINUSE from a parent still shutting down.
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
const WAIT_ENV = "BEAGLE_RELAUNCH_WAIT_PID";
|
|
18
|
+
/** Set on a successor we spawned, and inherited by its successors in turn.
|
|
19
|
+
* A detached child is reparented to pid 1, which looks exactly like a
|
|
20
|
+
* launchd job — without this mark the SECOND update would have exited
|
|
21
|
+
* and stayed exited, which is the bug this file exists to fix. */
|
|
22
|
+
const SELF_ENV = "BEAGLE_SELF_RELAUNCHED";
|
|
23
|
+
/** Something outside this process will restart it when it exits. */
|
|
24
|
+
export function isSupervised() {
|
|
25
|
+
const env = process.env;
|
|
26
|
+
if (env[SELF_ENV])
|
|
27
|
+
return false; // we are our own minder
|
|
28
|
+
if (env.pm_id !== undefined)
|
|
29
|
+
return true; // pm2
|
|
30
|
+
if (env.INVOCATION_ID || env.JOURNAL_STREAM)
|
|
31
|
+
return true; // systemd
|
|
32
|
+
if (process.pid === 1)
|
|
33
|
+
return true; // a container with a restart policy
|
|
34
|
+
// launchd is the parent of every job it runs. A hand-started process in a
|
|
35
|
+
// terminal has the shell as its parent; only when that shell is gone does
|
|
36
|
+
// it get reparented to 1 — and then nobody was restarting it anyway, but a
|
|
37
|
+
// process that outlived its terminal is not one a person is watching, so
|
|
38
|
+
// exiting quietly is the lesser surprise.
|
|
39
|
+
if (process.platform === "darwin" && process.ppid === 1)
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/** Start the successor. Caller exits right after. */
|
|
44
|
+
export function relaunchSelf(mode) {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
let cmd;
|
|
47
|
+
let cmdArgs;
|
|
48
|
+
if (mode === "npx") {
|
|
49
|
+
// argv[1] points into npx's cache of the OLD version. `@latest`
|
|
50
|
+
// re-resolves, which is the whole update for an npx user.
|
|
51
|
+
cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
52
|
+
cmdArgs = ["-y", "@decentnetwork/beagle@latest", ...args];
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cmd = process.execPath;
|
|
56
|
+
cmdArgs = [process.argv[1], ...args];
|
|
57
|
+
}
|
|
58
|
+
const child = spawn(cmd, cmdArgs, {
|
|
59
|
+
detached: true,
|
|
60
|
+
// Keep the terminal this was started from as the log, exactly as before.
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
env: { ...process.env, [WAIT_ENV]: String(process.pid), [SELF_ENV]: "1" },
|
|
63
|
+
shell: process.platform === "win32" && mode === "npx",
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
}
|
|
67
|
+
/** In the successor: block until the process that spawned us has exited.
|
|
68
|
+
* No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
|
|
69
|
+
* than hang forever behind a predecessor that will not die. */
|
|
70
|
+
export async function waitForPredecessor() {
|
|
71
|
+
const pid = Number(process.env[WAIT_ENV]);
|
|
72
|
+
delete process.env[WAIT_ENV];
|
|
73
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
74
|
+
return;
|
|
75
|
+
const deadline = Date.now() + 20_000;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return; // ESRCH: gone. (EPERM cannot happen — same user, our own parent.)
|
|
82
|
+
}
|
|
83
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { IpcRequest, IpcResponse } from "./ipc.js";
|
|
2
|
+
/** How this copy of beagle got onto disk — decides what "update" can do.
|
|
3
|
+
* npx cache paths contain _npx AND node_modules, so test _npx first. */
|
|
4
|
+
export declare function installMode(): "global" | "npx" | "dev" | "unknown";
|
|
2
5
|
export interface BeagleServerOptions {
|
|
3
6
|
/** Call the running daemon over IPC (injected so this module stays
|
|
4
7
|
* decoupled from the CLI's ipc client). */
|
|
@@ -35,6 +38,10 @@ export interface BeagleServerOptions {
|
|
|
35
38
|
}>;
|
|
36
39
|
cancelArm: () => Promise<string>;
|
|
37
40
|
};
|
|
41
|
+
/** Called instead of process.exit(0) on /api/update-restart, so the
|
|
42
|
+
* process that owns the peer can shut it down and relaunch itself when
|
|
43
|
+
* no supervisor will (see relaunch.ts). */
|
|
44
|
+
onRestart?: () => void | Promise<void>;
|
|
38
45
|
listenHost?: string;
|
|
39
46
|
listenPort?: number;
|
|
40
47
|
log?: (msg: string) => void;
|
package/dist/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import yaml from "js-yaml";
|
|
|
22
22
|
import { DEFAULT_EXITS } from "./exits.js";
|
|
23
23
|
import * as peerAddr from "@decentnetwork/peer";
|
|
24
24
|
import { bridgeRoster } from "./bridge-roster.js";
|
|
25
|
+
import { isSupervised } from "./relaunch.js";
|
|
25
26
|
// Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
|
|
26
27
|
// scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
|
|
27
28
|
const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
|
|
@@ -268,7 +269,7 @@ async function runningVersions(call) {
|
|
|
268
269
|
}
|
|
269
270
|
/** How this copy of beagle got onto disk — decides what "update" can do.
|
|
270
271
|
* npx cache paths contain _npx AND node_modules, so test _npx first. */
|
|
271
|
-
function installMode() {
|
|
272
|
+
export function installMode() {
|
|
272
273
|
if (BEAGLE_ROOT.includes("_npx"))
|
|
273
274
|
return "npx";
|
|
274
275
|
if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
|
|
@@ -1796,10 +1797,17 @@ export function startBeagleServer(opts) {
|
|
|
1796
1797
|
sendJson(res, 403, { ok: false, error: "restart only from this machine (localhost)" });
|
|
1797
1798
|
return;
|
|
1798
1799
|
}
|
|
1799
|
-
sendJson(res, 200, { ok: true });
|
|
1800
|
-
// Let the response flush, then
|
|
1801
|
-
//
|
|
1802
|
-
|
|
1800
|
+
sendJson(res, 200, { ok: true, supervised: isSupervised() });
|
|
1801
|
+
// Let the response flush, then hand over. Under pm2/launchd/systemd an
|
|
1802
|
+
// exit IS the restart; a bare `beagle` in a terminal relaunches itself
|
|
1803
|
+
// (cli.ts onRestart) — it used to just exit and stay exited.
|
|
1804
|
+
setTimeout(() => {
|
|
1805
|
+
if (!opts.onRestart) {
|
|
1806
|
+
process.exit(0);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
void Promise.resolve(opts.onRestart()).catch(() => process.exit(0));
|
|
1810
|
+
}, 400);
|
|
1803
1811
|
return;
|
|
1804
1812
|
}
|
|
1805
1813
|
// Public profile card for any userid (friend or directory row): what
|
|
@@ -2106,8 +2114,25 @@ export function startBeagleServer(opts) {
|
|
|
2106
2114
|
return;
|
|
2107
2115
|
}
|
|
2108
2116
|
if (req.method === "GET" && url === "/api/chat-history") {
|
|
2109
|
-
const
|
|
2110
|
-
const
|
|
2117
|
+
const q = new URL(req.url || "/", "http://x").searchParams;
|
|
2118
|
+
const peer = q.get("peer") || undefined;
|
|
2119
|
+
// A PAGE, never the whole thread. The UI re-polls the open
|
|
2120
|
+
// conversation every 2.5s, and this used to ask for everything each
|
|
2121
|
+
// time: a group with 2000 six-kilobyte bot reports is 12 MB of JSON
|
|
2122
|
+
// per poll per tab, which the daemon could not push over IPC inside
|
|
2123
|
+
// the 30s call timeout. While it tried, its socket stopped answering
|
|
2124
|
+
// the 400ms liveness probe, Beagle declared it gone and dropped the
|
|
2125
|
+
// backend, and the peer list came back empty — a blank chat panel
|
|
2126
|
+
// caused by one busy group. Default 200 is a full screen and then
|
|
2127
|
+
// some; `before` pages further back; the cap keeps a hand-written
|
|
2128
|
+
// URL from recreating the problem. Both `userid` and `peer` are sent:
|
|
2129
|
+
// the daemon reads the first, the embedded host the second, and with
|
|
2130
|
+
// only `userid` the embedded host returned every thread in the store.
|
|
2131
|
+
const lim = Number(q.get("limit") ?? 200);
|
|
2132
|
+
const limit = Number.isFinite(lim) && lim > 0 ? Math.min(lim, 1000) : 200;
|
|
2133
|
+
const bef = Number(q.get("before"));
|
|
2134
|
+
const before = Number.isFinite(bef) && bef > 0 ? bef : undefined;
|
|
2135
|
+
const r = await opts.call({ op: "chat-history", userid: peer, peer, before, limit });
|
|
2111
2136
|
sendJson(res, 200, r.ok ? (r.data ?? { chats: {} }) : { chats: {} });
|
|
2112
2137
|
return;
|
|
2113
2138
|
}
|
package/package.json
CHANGED