@alook/daemon 0.1.6 → 0.1.8
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/index.js +1035 -153
- package/dist/index.js +893 -549
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -98,7 +98,7 @@ var init_nanoid = () => {};
|
|
|
98
98
|
// src/cli/index.ts
|
|
99
99
|
import { Command, CommanderError } from "commander";
|
|
100
100
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
101
|
-
import { randomUUID as
|
|
101
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
102
102
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
103
103
|
|
|
104
104
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
@@ -16762,6 +16762,17 @@ function parseThreadTail(segment) {
|
|
|
16762
16762
|
return { threadRootSeq };
|
|
16763
16763
|
return { threadRootSeq, seq: parseSeq(tokens[1]) };
|
|
16764
16764
|
}
|
|
16765
|
+
function formatRef(p) {
|
|
16766
|
+
if (p.seq !== undefined && p.threadRootSeq === undefined) {
|
|
16767
|
+
throw new Error("formatRef: seq without threadRootSeq is not supported");
|
|
16768
|
+
}
|
|
16769
|
+
const base = `/${p.server}/${p.channel}`;
|
|
16770
|
+
if (p.threadRootSeq === undefined)
|
|
16771
|
+
return base;
|
|
16772
|
+
if (p.seq === undefined)
|
|
16773
|
+
return `${base}/#${p.threadRootSeq}`;
|
|
16774
|
+
return `${base}/#${p.threadRootSeq}#${p.seq}`;
|
|
16775
|
+
}
|
|
16765
16776
|
function parseSeq(s) {
|
|
16766
16777
|
const n = Number(s.startsWith("#") ? s.slice(1) : s);
|
|
16767
16778
|
if (!Number.isFinite(n))
|
|
@@ -16808,6 +16819,9 @@ var HostCommandSchema = exports_external.discriminatedUnion("type", [
|
|
|
16808
16819
|
launchId: exports_external.string().min(1)
|
|
16809
16820
|
}))
|
|
16810
16821
|
}),
|
|
16822
|
+
exports_external.strictObject({
|
|
16823
|
+
type: exports_external.literal("machine:update")
|
|
16824
|
+
}),
|
|
16811
16825
|
exports_external.object({
|
|
16812
16826
|
type: exports_external.literal("bot:added"),
|
|
16813
16827
|
botId: exports_external.string().min(1),
|
|
@@ -18616,6 +18630,32 @@ function parseInviteToken(input) {
|
|
|
18616
18630
|
return urlMatch[1];
|
|
18617
18631
|
return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
|
|
18618
18632
|
}
|
|
18633
|
+
// ../shared/src/semver.ts
|
|
18634
|
+
var RELEASE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
18635
|
+
function parseReleaseVersion(value) {
|
|
18636
|
+
const match = RELEASE_VERSION_PATTERN.exec(value);
|
|
18637
|
+
if (!match)
|
|
18638
|
+
return null;
|
|
18639
|
+
const major = Number(match[1]);
|
|
18640
|
+
const minor = Number(match[2]);
|
|
18641
|
+
const patch = Number(match[3]);
|
|
18642
|
+
if (![major, minor, patch].every(Number.isSafeInteger))
|
|
18643
|
+
return null;
|
|
18644
|
+
return [major, minor, patch];
|
|
18645
|
+
}
|
|
18646
|
+
function releaseVersionGte(a, b) {
|
|
18647
|
+
const parsedA = parseReleaseVersion(a);
|
|
18648
|
+
const parsedB = parseReleaseVersion(b);
|
|
18649
|
+
if (!parsedA || !parsedB)
|
|
18650
|
+
return false;
|
|
18651
|
+
for (let i = 0;i < parsedA.length; i++) {
|
|
18652
|
+
if (parsedA[i] > parsedB[i])
|
|
18653
|
+
return true;
|
|
18654
|
+
if (parsedA[i] < parsedB[i])
|
|
18655
|
+
return false;
|
|
18656
|
+
}
|
|
18657
|
+
return true;
|
|
18658
|
+
}
|
|
18619
18659
|
// src/cli/proxyServerApi.ts
|
|
18620
18660
|
function proxyServerApiFromEnv(prefix = "ALOOK", env = process.env) {
|
|
18621
18661
|
const proxyUrl = env[`${prefix}_PROXY_URL`];
|
|
@@ -18985,9 +19025,9 @@ function createProxyServerApi(config2) {
|
|
|
18985
19025
|
}
|
|
18986
19026
|
|
|
18987
19027
|
// src/cli/daemonStart.ts
|
|
18988
|
-
import * as
|
|
18989
|
-
import * as
|
|
18990
|
-
import * as
|
|
19028
|
+
import * as fs12 from "fs";
|
|
19029
|
+
import * as path13 from "path";
|
|
19030
|
+
import * as crypto5 from "crypto";
|
|
18991
19031
|
import * as os3 from "os";
|
|
18992
19032
|
import { homedir as homedir4 } from "os";
|
|
18993
19033
|
|
|
@@ -19033,7 +19073,7 @@ function cliCommandsSection() {
|
|
|
19033
19073
|
"### Messaging",
|
|
19034
19074
|
"",
|
|
19035
19075
|
`1. \`${CLI} inbox pull\` — fetch unread messages (advances your read waterline by default, ` + `so they won't re-pull; \`--no-ack\` to peek without advancing).`,
|
|
19036
|
-
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters)
|
|
19076
|
+
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
|
|
19037
19077
|
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
|
|
19038
19078
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
19039
19079
|
`5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
|
|
@@ -19182,7 +19222,13 @@ function utilsSection() {
|
|
|
19182
19222
|
"",
|
|
19183
19223
|
"### Join a new server",
|
|
19184
19224
|
"",
|
|
19185
|
-
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
19225
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it.",
|
|
19226
|
+
"",
|
|
19227
|
+
"### Follow up when a conversation goes quiet",
|
|
19228
|
+
"",
|
|
19229
|
+
`Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
|
|
19230
|
+
"",
|
|
19231
|
+
`Example: \`${CLI} message send --target /demo#1234/team --text "Please review the rollout plan" ` + `--remind-after 1m\`.`
|
|
19186
19232
|
].join(`
|
|
19187
19233
|
`);
|
|
19188
19234
|
}
|
|
@@ -20253,13 +20299,18 @@ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
|
|
|
20253
20299
|
// src/version.ts
|
|
20254
20300
|
import { createRequire as createRequire2 } from "module";
|
|
20255
20301
|
var requireFromHere = createRequire2(import.meta.url);
|
|
20302
|
+
var PACKAGE_JSON_CANDIDATES = ["../package.json", "../../package.json"];
|
|
20256
20303
|
function readDaemonVersion() {
|
|
20257
|
-
|
|
20258
|
-
|
|
20259
|
-
|
|
20260
|
-
|
|
20261
|
-
|
|
20304
|
+
for (const candidate of PACKAGE_JSON_CANDIDATES) {
|
|
20305
|
+
try {
|
|
20306
|
+
const pkg = requireFromHere(candidate);
|
|
20307
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0)
|
|
20308
|
+
return pkg.version;
|
|
20309
|
+
} catch {
|
|
20310
|
+
continue;
|
|
20311
|
+
}
|
|
20262
20312
|
}
|
|
20313
|
+
return "";
|
|
20263
20314
|
}
|
|
20264
20315
|
function getDaemonClientInfo() {
|
|
20265
20316
|
return { name: "alook-daemon", version: readDaemonVersion() };
|
|
@@ -21362,8 +21413,8 @@ function createLogger2(options = {}) {
|
|
|
21362
21413
|
}
|
|
21363
21414
|
|
|
21364
21415
|
// src/cli/daemonRunner.ts
|
|
21365
|
-
import * as
|
|
21366
|
-
import * as
|
|
21416
|
+
import * as fs11 from "node:fs";
|
|
21417
|
+
import * as path12 from "node:path";
|
|
21367
21418
|
import { WebSocket } from "ws";
|
|
21368
21419
|
|
|
21369
21420
|
// src/daemon/createDaemon.ts
|
|
@@ -21862,6 +21913,10 @@ import * as https from "https";
|
|
|
21862
21913
|
import * as os2 from "os";
|
|
21863
21914
|
import * as path9 from "path";
|
|
21864
21915
|
import { URL as URL2 } from "url";
|
|
21916
|
+
var LOCAL_MESSAGE_REMINDER_PATH = "/__alook/local/message-reminder";
|
|
21917
|
+
var LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES = 4 * 1024;
|
|
21918
|
+
var LOCAL_MESSAGE_REMINDER_MIN_MS = 60000;
|
|
21919
|
+
var LOCAL_MESSAGE_REMINDER_MAX_MS = 24 * 60 * 60000;
|
|
21865
21920
|
var DEFAULT_HEADER_NAMES = {
|
|
21866
21921
|
agentId: "X-Agent-Id",
|
|
21867
21922
|
client: "X-Client",
|
|
@@ -21977,6 +22032,99 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
|
|
|
21977
22032
|
return;
|
|
21978
22033
|
};
|
|
21979
22034
|
var DEFAULT_UPSTREAM_TIMEOUT_MS = 20000;
|
|
22035
|
+
function writeJson(res, status, body) {
|
|
22036
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
22037
|
+
res.end(JSON.stringify(body));
|
|
22038
|
+
}
|
|
22039
|
+
function isCanonicalChannelScope(channel2) {
|
|
22040
|
+
try {
|
|
22041
|
+
const parsed = parseRef(channel2);
|
|
22042
|
+
if (!parsed.channel || parsed.seq !== undefined)
|
|
22043
|
+
return false;
|
|
22044
|
+
if (parsed.threadRootSeq !== undefined && (!Number.isSafeInteger(parsed.threadRootSeq) || parsed.threadRootSeq < 1)) {
|
|
22045
|
+
return false;
|
|
22046
|
+
}
|
|
22047
|
+
const handle = parseNameAndTag(parsed.server === ".dm" ? parsed.channel : parsed.server);
|
|
22048
|
+
if (!handle || `${handle.name}#${handle.discriminator}` !== (parsed.server === ".dm" ? parsed.channel : parsed.server)) {
|
|
22049
|
+
return false;
|
|
22050
|
+
}
|
|
22051
|
+
return formatRef(parsed) === channel2;
|
|
22052
|
+
} catch {
|
|
22053
|
+
return false;
|
|
22054
|
+
}
|
|
22055
|
+
}
|
|
22056
|
+
function parseLocalMessageReminderBody(body, agentId) {
|
|
22057
|
+
let value;
|
|
22058
|
+
try {
|
|
22059
|
+
value = JSON.parse(body.toString("utf8"));
|
|
22060
|
+
} catch {
|
|
22061
|
+
return null;
|
|
22062
|
+
}
|
|
22063
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22064
|
+
return null;
|
|
22065
|
+
const record2 = value;
|
|
22066
|
+
if (Object.keys(record2).sort().join(",") !== "channel,remindAfterMs,sentSeq")
|
|
22067
|
+
return null;
|
|
22068
|
+
if (typeof record2.channel !== "string" || !isCanonicalChannelScope(record2.channel))
|
|
22069
|
+
return null;
|
|
22070
|
+
if (!Number.isSafeInteger(record2.sentSeq) || record2.sentSeq < 1)
|
|
22071
|
+
return null;
|
|
22072
|
+
if (!Number.isSafeInteger(record2.remindAfterMs) || record2.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record2.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
22073
|
+
return null;
|
|
22074
|
+
return {
|
|
22075
|
+
agentId,
|
|
22076
|
+
channel: record2.channel,
|
|
22077
|
+
sentSeq: record2.sentSeq,
|
|
22078
|
+
remindAfterMs: record2.remindAfterMs
|
|
22079
|
+
};
|
|
22080
|
+
}
|
|
22081
|
+
async function handleLocalMessageReminder(req, res, agentId, onArm) {
|
|
22082
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
22083
|
+
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
22084
|
+
writeJson(res, 415, { error: "content-type must be application/json", code: "unsupported_media_type" });
|
|
22085
|
+
req.resume();
|
|
22086
|
+
return;
|
|
22087
|
+
}
|
|
22088
|
+
const declaredLength = Number(req.headers["content-length"] ?? 0);
|
|
22089
|
+
if (!Number.isFinite(declaredLength) || declaredLength > LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES) {
|
|
22090
|
+
writeJson(res, 413, { error: "request body too large", code: "body_too_large" });
|
|
22091
|
+
req.resume();
|
|
22092
|
+
return;
|
|
22093
|
+
}
|
|
22094
|
+
const chunks = [];
|
|
22095
|
+
let bytes = 0;
|
|
22096
|
+
let tooLarge = false;
|
|
22097
|
+
await new Promise((resolve4) => {
|
|
22098
|
+
req.on("data", (chunk2) => {
|
|
22099
|
+
bytes += chunk2.byteLength;
|
|
22100
|
+
if (bytes > LOCAL_MESSAGE_REMINDER_BODY_MAX_BYTES) {
|
|
22101
|
+
tooLarge = true;
|
|
22102
|
+
} else {
|
|
22103
|
+
chunks.push(chunk2);
|
|
22104
|
+
}
|
|
22105
|
+
});
|
|
22106
|
+
req.on("end", resolve4);
|
|
22107
|
+
req.on("error", resolve4);
|
|
22108
|
+
});
|
|
22109
|
+
if (tooLarge) {
|
|
22110
|
+
writeJson(res, 413, { error: "request body too large", code: "body_too_large" });
|
|
22111
|
+
return;
|
|
22112
|
+
}
|
|
22113
|
+
const input = parseLocalMessageReminderBody(Buffer.concat(chunks), agentId);
|
|
22114
|
+
if (!input) {
|
|
22115
|
+
writeJson(res, 400, { error: "invalid local message reminder request", code: "invalid_request" });
|
|
22116
|
+
return;
|
|
22117
|
+
}
|
|
22118
|
+
if (!onArm) {
|
|
22119
|
+
writeJson(res, 503, { error: "local message reminder unavailable", code: "reminder_unavailable" });
|
|
22120
|
+
return;
|
|
22121
|
+
}
|
|
22122
|
+
try {
|
|
22123
|
+
writeJson(res, 200, await onArm(input));
|
|
22124
|
+
} catch {
|
|
22125
|
+
writeJson(res, 500, { error: "local message reminder failed", code: "reminder_failed" });
|
|
22126
|
+
}
|
|
22127
|
+
}
|
|
21980
22128
|
async function startCredentialProxy(broker, options = {}) {
|
|
21981
22129
|
const host = options.host ?? "127.0.0.1";
|
|
21982
22130
|
const resolveCap = options.capabilityResolver ?? DEFAULT_CAPABILITY_RESOLVER;
|
|
@@ -21986,6 +22134,26 @@ async function startCredentialProxy(broker, options = {}) {
|
|
|
21986
22134
|
const onProxyRequest = options.onProxyRequest;
|
|
21987
22135
|
const server = http.createServer((req, res) => {
|
|
21988
22136
|
const pathname = new URL2(req.url ?? "/", "http://placeholder").pathname;
|
|
22137
|
+
if (pathname.startsWith("/__alook/local/")) {
|
|
22138
|
+
if (req.url !== LOCAL_MESSAGE_REMINDER_PATH) {
|
|
22139
|
+
writeJson(res, 404, { error: "unknown local route", code: "not_found" });
|
|
22140
|
+
req.resume();
|
|
22141
|
+
return;
|
|
22142
|
+
}
|
|
22143
|
+
if (req.method !== "PUT") {
|
|
22144
|
+
writeJson(res, 405, { error: "method not allowed", code: "method_not_allowed" });
|
|
22145
|
+
req.resume();
|
|
22146
|
+
return;
|
|
22147
|
+
}
|
|
22148
|
+
const localVerdict = broker.check(req.headers["authorization"], "send");
|
|
22149
|
+
if (!localVerdict.ok) {
|
|
22150
|
+
writeJson(res, localVerdict.status, { error: localVerdict.error, code: localVerdict.code });
|
|
22151
|
+
req.resume();
|
|
22152
|
+
return;
|
|
22153
|
+
}
|
|
22154
|
+
handleLocalMessageReminder(req, res, localVerdict.reg.agentId, options.onMessageReminderArm);
|
|
22155
|
+
return;
|
|
22156
|
+
}
|
|
21989
22157
|
const requiredCap = resolveCap(req.method ?? "GET", pathname);
|
|
21990
22158
|
const verdict = broker.check(req.headers["authorization"], requiredCap);
|
|
21991
22159
|
if (!verdict.ok) {
|
|
@@ -24818,6 +24986,108 @@ function createDiagnosticsCommandListener(options) {
|
|
|
24818
24986
|
};
|
|
24819
24987
|
}
|
|
24820
24988
|
|
|
24989
|
+
// src/daemon/selfUpdateCommand.ts
|
|
24990
|
+
function createSelfUpdateCommandListener(handleSelfUpdate) {
|
|
24991
|
+
return (command) => {
|
|
24992
|
+
if (command.type !== "machine:update")
|
|
24993
|
+
return;
|
|
24994
|
+
if (handleSelfUpdate) {
|
|
24995
|
+
try {
|
|
24996
|
+
Promise.resolve(handleSelfUpdate()).catch(() => {});
|
|
24997
|
+
} catch {}
|
|
24998
|
+
}
|
|
24999
|
+
return WS_CONTROL_COMMAND_CONSUMED;
|
|
25000
|
+
};
|
|
25001
|
+
}
|
|
25002
|
+
|
|
25003
|
+
// src/daemon/messageReminderScheduler.ts
|
|
25004
|
+
function reminderKey(agentId, channel2) {
|
|
25005
|
+
return `${agentId}\x00${channel2}`;
|
|
25006
|
+
}
|
|
25007
|
+
function reminderPrompt(channel2, sentRef, startedAt) {
|
|
25008
|
+
return `Reminder: At ${localISOString(new Date(startedAt))}, after sending ${sentRef} in ${channel2}, you asked to be reminded if no newer message arrived. No newer message has arrived in that conversation.`;
|
|
25009
|
+
}
|
|
25010
|
+
|
|
25011
|
+
class MessageReminderScheduler {
|
|
25012
|
+
options;
|
|
25013
|
+
reminders = new Map;
|
|
25014
|
+
latestObservedSeq = new Map;
|
|
25015
|
+
now;
|
|
25016
|
+
setTimer;
|
|
25017
|
+
clearTimer;
|
|
25018
|
+
constructor(options) {
|
|
25019
|
+
this.options = options;
|
|
25020
|
+
this.now = options.now ?? Date.now;
|
|
25021
|
+
this.setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
25022
|
+
this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
25023
|
+
}
|
|
25024
|
+
arm(input) {
|
|
25025
|
+
const key = reminderKey(input.agentId, input.channel);
|
|
25026
|
+
const latest = this.latestObservedSeq.get(key);
|
|
25027
|
+
if (latest !== undefined && latest > input.sentSeq) {
|
|
25028
|
+
return { armed: false, reason: "newer_message_observed" };
|
|
25029
|
+
}
|
|
25030
|
+
this.clearReminder(key);
|
|
25031
|
+
const startedAt = this.now();
|
|
25032
|
+
const dueAt = startedAt + input.remindAfterMs;
|
|
25033
|
+
const sentRef = `${input.channel}#${input.sentSeq}`;
|
|
25034
|
+
const record2 = {
|
|
25035
|
+
...input,
|
|
25036
|
+
sentRef,
|
|
25037
|
+
startedAt,
|
|
25038
|
+
dueAt,
|
|
25039
|
+
timer: undefined
|
|
25040
|
+
};
|
|
25041
|
+
record2.timer = this.setTimer(() => {
|
|
25042
|
+
if (this.reminders.get(key) !== record2)
|
|
25043
|
+
return;
|
|
25044
|
+
this.reminders.delete(key);
|
|
25045
|
+
try {
|
|
25046
|
+
const delivery = this.options.deliver(input.agentId, {
|
|
25047
|
+
text: reminderPrompt(input.channel, sentRef, startedAt)
|
|
25048
|
+
});
|
|
25049
|
+
Promise.resolve(delivery).catch(() => {});
|
|
25050
|
+
} catch {}
|
|
25051
|
+
}, input.remindAfterMs);
|
|
25052
|
+
record2.timer.unref?.();
|
|
25053
|
+
this.reminders.set(key, record2);
|
|
25054
|
+
return { armed: true, dueAt };
|
|
25055
|
+
}
|
|
25056
|
+
observe(agentId, channel2, latestSeq) {
|
|
25057
|
+
const key = reminderKey(agentId, channel2);
|
|
25058
|
+
const previous = this.latestObservedSeq.get(key);
|
|
25059
|
+
if (previous === undefined || latestSeq > previous) {
|
|
25060
|
+
this.latestObservedSeq.set(key, latestSeq);
|
|
25061
|
+
}
|
|
25062
|
+
const reminder = this.reminders.get(key);
|
|
25063
|
+
if (reminder && latestSeq > reminder.sentSeq)
|
|
25064
|
+
this.clearReminder(key);
|
|
25065
|
+
}
|
|
25066
|
+
clearAgent(agentId) {
|
|
25067
|
+
const prefix = `${agentId}\x00`;
|
|
25068
|
+
for (const key of [...this.reminders.keys()]) {
|
|
25069
|
+
if (key.startsWith(prefix))
|
|
25070
|
+
this.clearReminder(key);
|
|
25071
|
+
}
|
|
25072
|
+
for (const key of [...this.latestObservedSeq.keys()]) {
|
|
25073
|
+
if (key.startsWith(prefix))
|
|
25074
|
+
this.latestObservedSeq.delete(key);
|
|
25075
|
+
}
|
|
25076
|
+
}
|
|
25077
|
+
clearAll() {
|
|
25078
|
+
for (const key of [...this.reminders.keys()])
|
|
25079
|
+
this.clearReminder(key);
|
|
25080
|
+
this.latestObservedSeq.clear();
|
|
25081
|
+
}
|
|
25082
|
+
clearReminder(key) {
|
|
25083
|
+
const reminder = this.reminders.get(key);
|
|
25084
|
+
if (!reminder)
|
|
25085
|
+
return;
|
|
25086
|
+
this.reminders.delete(key);
|
|
25087
|
+
this.clearTimer(reminder.timer);
|
|
25088
|
+
}
|
|
25089
|
+
}
|
|
25090
|
+
|
|
24821
25091
|
// src/daemon/createDaemon.ts
|
|
24822
25092
|
var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
|
|
24823
25093
|
var WARMUP_CEILING_MS = 30000;
|
|
@@ -24948,6 +25218,7 @@ async function createDaemon(opts) {
|
|
|
24948
25218
|
});
|
|
24949
25219
|
let channelRef = null;
|
|
24950
25220
|
let managerRef = null;
|
|
25221
|
+
let reminderSchedulerRef = null;
|
|
24951
25222
|
const emitBotAuditEvent = (agentId, event, context) => {
|
|
24952
25223
|
channelRef?.reportBotAuditEvent?.({
|
|
24953
25224
|
type: "bot_audit_event",
|
|
@@ -24961,6 +25232,7 @@ async function createDaemon(opts) {
|
|
|
24961
25232
|
const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
|
|
24962
25233
|
const proxy = await startCredentialProxy(broker, {
|
|
24963
25234
|
onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
|
|
25235
|
+
onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
|
|
24964
25236
|
onProxyRequest: (agentId, method, pathname) => {
|
|
24965
25237
|
const subcommand = deriveAuditLogSubcommand(pathname, method);
|
|
24966
25238
|
if (!subcommand)
|
|
@@ -25233,6 +25505,10 @@ async function createDaemon(opts) {
|
|
|
25233
25505
|
});
|
|
25234
25506
|
managerRef = manager;
|
|
25235
25507
|
manager.start();
|
|
25508
|
+
reminderSchedulerRef = new MessageReminderScheduler({
|
|
25509
|
+
deliver: (agentId, message2) => manager.deliver(agentId, message2),
|
|
25510
|
+
...opts.messageReminderClock
|
|
25511
|
+
});
|
|
25236
25512
|
let statusTimer = null;
|
|
25237
25513
|
if (opts.statusFilePath) {
|
|
25238
25514
|
const statusPath = opts.statusFilePath;
|
|
@@ -25278,10 +25554,26 @@ async function createDaemon(opts) {
|
|
|
25278
25554
|
},
|
|
25279
25555
|
formatUnreadNoticeText: (notice) => `You have unread messages in channel ${notice.channel}.`
|
|
25280
25556
|
});
|
|
25557
|
+
channel2.onCommand(createSelfUpdateCommandListener(opts.handleSelfUpdate));
|
|
25281
25558
|
channel2.onCommand(createDiagnosticsCommandListener({
|
|
25282
25559
|
handleDiagnosticCommand: opts.handleDiagnosticCommand,
|
|
25283
25560
|
reportDiagnosticFailure: opts.reportDiagnosticFailure
|
|
25284
25561
|
}));
|
|
25562
|
+
channel2.onCommand((cmd) => {
|
|
25563
|
+
switch (cmd.type) {
|
|
25564
|
+
case "agent:wake":
|
|
25565
|
+
reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
|
|
25566
|
+
break;
|
|
25567
|
+
case "agent:stop":
|
|
25568
|
+
reminderSchedulerRef?.clearAgent(cmd.agentId);
|
|
25569
|
+
break;
|
|
25570
|
+
case "bot:removed":
|
|
25571
|
+
reminderSchedulerRef?.clearAgent(cmd.botId);
|
|
25572
|
+
break;
|
|
25573
|
+
default:
|
|
25574
|
+
break;
|
|
25575
|
+
}
|
|
25576
|
+
});
|
|
25285
25577
|
channel2.onCommand((cmd) => {
|
|
25286
25578
|
handleBotFrame(cmd);
|
|
25287
25579
|
});
|
|
@@ -25300,6 +25592,7 @@ async function createDaemon(opts) {
|
|
|
25300
25592
|
},
|
|
25301
25593
|
proxyUrl: proxy.url,
|
|
25302
25594
|
stop: async () => {
|
|
25595
|
+
reminderSchedulerRef?.clearAll();
|
|
25303
25596
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
25304
25597
|
emitTypingStopsAndClear(agentId);
|
|
25305
25598
|
}
|
|
@@ -26476,6 +26769,318 @@ function createDiagnosticHttpTransport(args) {
|
|
|
26476
26769
|
}
|
|
26477
26770
|
};
|
|
26478
26771
|
}
|
|
26772
|
+
// src/cli/daemonUpdate.ts
|
|
26773
|
+
import * as crypto4 from "node:crypto";
|
|
26774
|
+
import * as fs10 from "node:fs";
|
|
26775
|
+
import * as path11 from "node:path";
|
|
26776
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
26777
|
+
var UPDATE_INTENT_SCHEMA_VERSION = 1;
|
|
26778
|
+
var UPDATE_LOG_MAX_BYTES = 512 * 1024;
|
|
26779
|
+
var UPDATE_LOG_KEEP_BYTES = 256 * 1024;
|
|
26780
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
26781
|
+
function intentPath(baseDir, machineId) {
|
|
26782
|
+
return path11.join(daemonDirById(baseDir, machineId), "update-intent.json");
|
|
26783
|
+
}
|
|
26784
|
+
function updateLogPath(baseDir, machineId) {
|
|
26785
|
+
return path11.join(daemonDirById(baseDir, machineId), "update.log");
|
|
26786
|
+
}
|
|
26787
|
+
function updatePackageMapPath(baseDir, machineId) {
|
|
26788
|
+
return path11.join(daemonDirById(baseDir, machineId), "update-package-map.json");
|
|
26789
|
+
}
|
|
26790
|
+
function ensurePrivateDir(dir) {
|
|
26791
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
26792
|
+
fs10.chmodSync(dir, 448);
|
|
26793
|
+
}
|
|
26794
|
+
function writePrivateJsonAtomic(filePath, value) {
|
|
26795
|
+
const dir = path11.dirname(filePath);
|
|
26796
|
+
ensurePrivateDir(dir);
|
|
26797
|
+
const tempPath = path11.join(dir, `.${path11.basename(filePath)}.${process.pid}.${crypto4.randomBytes(8).toString("hex")}.tmp`);
|
|
26798
|
+
let fd = fs10.openSync(tempPath, "wx", 384);
|
|
26799
|
+
try {
|
|
26800
|
+
fs10.writeFileSync(fd, JSON.stringify(value));
|
|
26801
|
+
fs10.fsyncSync(fd);
|
|
26802
|
+
fs10.closeSync(fd);
|
|
26803
|
+
fd = null;
|
|
26804
|
+
fs10.chmodSync(tempPath, 384);
|
|
26805
|
+
fs10.renameSync(tempPath, filePath);
|
|
26806
|
+
} finally {
|
|
26807
|
+
if (fd !== null) {
|
|
26808
|
+
try {
|
|
26809
|
+
fs10.closeSync(fd);
|
|
26810
|
+
} catch {}
|
|
26811
|
+
}
|
|
26812
|
+
try {
|
|
26813
|
+
fs10.unlinkSync(tempPath);
|
|
26814
|
+
} catch {}
|
|
26815
|
+
}
|
|
26816
|
+
}
|
|
26817
|
+
function scrub(value) {
|
|
26818
|
+
return (value instanceof Error ? value.message : String(value)).replace(/\b(?:cmk|cmt)_[A-Za-z0-9_-]+\b/g, "[redacted-token]").replace(/[\r\n]+/g, " ").slice(0, 1024);
|
|
26819
|
+
}
|
|
26820
|
+
function appendUpdateLog(baseDir, machineId, event, fields = {}) {
|
|
26821
|
+
const filePath = updateLogPath(baseDir, machineId);
|
|
26822
|
+
ensurePrivateDir(path11.dirname(filePath));
|
|
26823
|
+
try {
|
|
26824
|
+
if (fs10.existsSync(filePath) && fs10.statSync(filePath).size > UPDATE_LOG_MAX_BYTES) {
|
|
26825
|
+
const data = fs10.readFileSync(filePath);
|
|
26826
|
+
fs10.writeFileSync(filePath, data.subarray(Math.max(0, data.length - UPDATE_LOG_KEEP_BYTES)), { mode: 384 });
|
|
26827
|
+
}
|
|
26828
|
+
const safeFields = Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, scrub(value)]));
|
|
26829
|
+
fs10.appendFileSync(filePath, `${JSON.stringify({ time: new Date().toISOString(), event, ...safeFields })}
|
|
26830
|
+
`, { mode: 384 });
|
|
26831
|
+
fs10.chmodSync(filePath, 384);
|
|
26832
|
+
} catch {}
|
|
26833
|
+
}
|
|
26834
|
+
function pidTuple(value) {
|
|
26835
|
+
if (!value || typeof value.machineId !== "string" || typeof value.startedAt !== "string" || typeof value.ownerToken !== "string")
|
|
26836
|
+
return null;
|
|
26837
|
+
return {
|
|
26838
|
+
pid: value.pid,
|
|
26839
|
+
machineId: value.machineId,
|
|
26840
|
+
startedAt: value.startedAt,
|
|
26841
|
+
ownerToken: value.ownerToken
|
|
26842
|
+
};
|
|
26843
|
+
}
|
|
26844
|
+
function tuplesEqual(value, expected) {
|
|
26845
|
+
return value?.pid === expected.pid && value.machineId === expected.machineId && value.startedAt === expected.startedAt && value.ownerToken === expected.ownerToken;
|
|
26846
|
+
}
|
|
26847
|
+
function readIntent(baseDir, machineId) {
|
|
26848
|
+
const filePath = intentPath(baseDir, machineId);
|
|
26849
|
+
const stat = fs10.lstatSync(filePath);
|
|
26850
|
+
if (!stat.isFile())
|
|
26851
|
+
throw new Error("unsafe daemon update intent type");
|
|
26852
|
+
fs10.chmodSync(filePath, 384);
|
|
26853
|
+
const value = JSON.parse(fs10.readFileSync(filePath, "utf8"));
|
|
26854
|
+
if (value.schemaVersion !== UPDATE_INTENT_SCHEMA_VERSION || typeof value.requestId !== "string" || !REQUEST_ID_PATTERN.test(value.requestId) || !Number.isInteger(value.pid) || (value.pid ?? 0) <= 0 || typeof value.machineId !== "string" || typeof value.startedAt !== "string" || typeof value.ownerToken !== "string")
|
|
26855
|
+
throw new Error("invalid daemon update intent");
|
|
26856
|
+
return value;
|
|
26857
|
+
}
|
|
26858
|
+
function removeIntentIfMatches(baseDir, machineId, requestId) {
|
|
26859
|
+
try {
|
|
26860
|
+
const current = readIntent(baseDir, machineId);
|
|
26861
|
+
if (current.requestId === requestId)
|
|
26862
|
+
fs10.unlinkSync(intentPath(baseDir, machineId));
|
|
26863
|
+
} catch {}
|
|
26864
|
+
}
|
|
26865
|
+
function npmExecPath(explicit) {
|
|
26866
|
+
const value = explicit ?? process.env.npm_execpath;
|
|
26867
|
+
if (!value || !path11.isAbsolute(value) || !fs10.existsSync(value)) {
|
|
26868
|
+
throw new Error("npm launch context unavailable; daemon remains online");
|
|
26869
|
+
}
|
|
26870
|
+
return value;
|
|
26871
|
+
}
|
|
26872
|
+
function readTestPackageMap(baseDir, machineId) {
|
|
26873
|
+
if (true)
|
|
26874
|
+
return null;
|
|
26875
|
+
const filePath = updatePackageMapPath(baseDir, machineId);
|
|
26876
|
+
if (!fs10.existsSync(filePath))
|
|
26877
|
+
return null;
|
|
26878
|
+
const stat = fs10.lstatSync(filePath);
|
|
26879
|
+
if (!stat.isFile())
|
|
26880
|
+
throw new Error("unsafe daemon update package map type");
|
|
26881
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
|
|
26882
|
+
throw new Error("daemon update package map must be mode 0600");
|
|
26883
|
+
}
|
|
26884
|
+
const parsed = JSON.parse(fs10.readFileSync(filePath, "utf8"));
|
|
26885
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
26886
|
+
throw new Error("invalid daemon update package map");
|
|
26887
|
+
const result = {};
|
|
26888
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
26889
|
+
if (key !== "latest" && !parseReleaseVersion(key))
|
|
26890
|
+
throw new Error("invalid daemon update package map key");
|
|
26891
|
+
if (typeof value !== "string" || !path11.isAbsolute(value))
|
|
26892
|
+
throw new Error("invalid daemon update package map value");
|
|
26893
|
+
result[key] = value;
|
|
26894
|
+
}
|
|
26895
|
+
return result;
|
|
26896
|
+
}
|
|
26897
|
+
function packageSpec(baseDir, machineId, version3) {
|
|
26898
|
+
const mapped = readTestPackageMap(baseDir, machineId)?.[version3];
|
|
26899
|
+
if (mapped)
|
|
26900
|
+
return mapped;
|
|
26901
|
+
return `@alook/daemon@${version3}`;
|
|
26902
|
+
}
|
|
26903
|
+
function fixedNpmArgs(args) {
|
|
26904
|
+
return [
|
|
26905
|
+
args.npmPath,
|
|
26906
|
+
"exec",
|
|
26907
|
+
"--yes",
|
|
26908
|
+
`--package=${args.packageSpec}`,
|
|
26909
|
+
"--",
|
|
26910
|
+
"alook-daemon",
|
|
26911
|
+
"daemon",
|
|
26912
|
+
args.command,
|
|
26913
|
+
"--id",
|
|
26914
|
+
args.machineId,
|
|
26915
|
+
"--base-dir",
|
|
26916
|
+
args.baseDir,
|
|
26917
|
+
"--request-id",
|
|
26918
|
+
args.requestId
|
|
26919
|
+
];
|
|
26920
|
+
}
|
|
26921
|
+
function openUpdateLog(baseDir, machineId) {
|
|
26922
|
+
appendUpdateLog(baseDir, machineId, "helper_spawn_requested");
|
|
26923
|
+
const filePath = updateLogPath(baseDir, machineId);
|
|
26924
|
+
const fd = fs10.openSync(filePath, "a", 384);
|
|
26925
|
+
fs10.chmodSync(filePath, 384);
|
|
26926
|
+
return fd;
|
|
26927
|
+
}
|
|
26928
|
+
function createDaemonSelfUpdateHandler(context, deps = {}) {
|
|
26929
|
+
let updateInFlight = null;
|
|
26930
|
+
return () => {
|
|
26931
|
+
if (updateInFlight && updateInFlight.exitCode === null && updateInFlight.signalCode === null)
|
|
26932
|
+
return;
|
|
26933
|
+
let requestId = "";
|
|
26934
|
+
try {
|
|
26935
|
+
const current = pidTuple(readPidFile(pidfilePathById(context.baseDir, context.machineId)));
|
|
26936
|
+
if (!current || current.pid !== context.pid || current.machineId !== context.machineId || current.startedAt !== context.startedAt || current.ownerToken !== context.ownerToken)
|
|
26937
|
+
throw new Error("daemon ownership changed before update launch");
|
|
26938
|
+
const npmPath = npmExecPath(deps.npmExecPath);
|
|
26939
|
+
requestId = crypto4.randomUUID();
|
|
26940
|
+
const intent = {
|
|
26941
|
+
schemaVersion: UPDATE_INTENT_SCHEMA_VERSION,
|
|
26942
|
+
requestId,
|
|
26943
|
+
...current
|
|
26944
|
+
};
|
|
26945
|
+
writePrivateJsonAtomic(intentPath(context.baseDir, context.machineId), intent);
|
|
26946
|
+
const logFd = openUpdateLog(context.baseDir, context.machineId);
|
|
26947
|
+
try {
|
|
26948
|
+
const child = (deps.spawnProcess ?? spawn2)(process.execPath, fixedNpmArgs({
|
|
26949
|
+
npmPath,
|
|
26950
|
+
packageSpec: packageSpec(context.baseDir, context.machineId, "latest"),
|
|
26951
|
+
command: "replace",
|
|
26952
|
+
machineId: context.machineId,
|
|
26953
|
+
baseDir: context.baseDir,
|
|
26954
|
+
requestId
|
|
26955
|
+
}), {
|
|
26956
|
+
detached: true,
|
|
26957
|
+
shell: false,
|
|
26958
|
+
stdio: ["ignore", logFd, logFd]
|
|
26959
|
+
});
|
|
26960
|
+
updateInFlight = child;
|
|
26961
|
+
const clear = (event, fields) => {
|
|
26962
|
+
appendUpdateLog(context.baseDir, context.machineId, event, fields);
|
|
26963
|
+
if (updateInFlight === child)
|
|
26964
|
+
updateInFlight = null;
|
|
26965
|
+
removeIntentIfMatches(context.baseDir, context.machineId, requestId);
|
|
26966
|
+
};
|
|
26967
|
+
child.once("exit", (code, signal) => clear("helper_exited", { code, signal }));
|
|
26968
|
+
child.once("error", (error51) => clear("helper_process_error", { error: error51 }));
|
|
26969
|
+
child.unref();
|
|
26970
|
+
deps.logger?.info("daemon self-update helper launched", { machineId: context.machineId });
|
|
26971
|
+
} finally {
|
|
26972
|
+
fs10.closeSync(logFd);
|
|
26973
|
+
}
|
|
26974
|
+
} catch (error51) {
|
|
26975
|
+
if (requestId)
|
|
26976
|
+
removeIntentIfMatches(context.baseDir, context.machineId, requestId);
|
|
26977
|
+
appendUpdateLog(context.baseDir, context.machineId, "helper_spawn_failed", { error: error51 });
|
|
26978
|
+
deps.logger?.warn("daemon self-update helper launch failed", { machineId: context.machineId, error: scrub(error51) });
|
|
26979
|
+
}
|
|
26980
|
+
};
|
|
26981
|
+
}
|
|
26982
|
+
function currentPidTuple(baseDir, machineId) {
|
|
26983
|
+
return pidTuple(readPidFile(pidfilePathById(baseDir, machineId)));
|
|
26984
|
+
}
|
|
26985
|
+
function removeOldPidfileIfMatches(baseDir, intent) {
|
|
26986
|
+
const filePath = pidfilePathById(baseDir, intent.machineId);
|
|
26987
|
+
if (!tuplesEqual(currentPidTuple(baseDir, intent.machineId), intent))
|
|
26988
|
+
return;
|
|
26989
|
+
try {
|
|
26990
|
+
fs10.unlinkSync(filePath);
|
|
26991
|
+
} catch {}
|
|
26992
|
+
}
|
|
26993
|
+
async function runPinnedResume(args) {
|
|
26994
|
+
const fd = openUpdateLog(args.baseDir, args.machineId);
|
|
26995
|
+
try {
|
|
26996
|
+
const child = spawn2(process.execPath, fixedNpmArgs({
|
|
26997
|
+
npmPath: args.npmPath,
|
|
26998
|
+
packageSpec: packageSpec(args.baseDir, args.machineId, args.version),
|
|
26999
|
+
command: "resume",
|
|
27000
|
+
machineId: args.machineId,
|
|
27001
|
+
baseDir: args.baseDir,
|
|
27002
|
+
requestId: args.requestId
|
|
27003
|
+
}), {
|
|
27004
|
+
shell: false,
|
|
27005
|
+
stdio: ["ignore", fd, fd]
|
|
27006
|
+
});
|
|
27007
|
+
await new Promise((resolve4, reject) => {
|
|
27008
|
+
child.once("error", reject);
|
|
27009
|
+
child.once("exit", (code, signal) => {
|
|
27010
|
+
if (code === 0)
|
|
27011
|
+
resolve4();
|
|
27012
|
+
else
|
|
27013
|
+
reject(new Error(`rollback resume exited ${signal ?? code}`));
|
|
27014
|
+
});
|
|
27015
|
+
});
|
|
27016
|
+
} finally {
|
|
27017
|
+
fs10.closeSync(fd);
|
|
27018
|
+
}
|
|
27019
|
+
}
|
|
27020
|
+
async function daemonReplace(opts) {
|
|
27021
|
+
if (!REQUEST_ID_PATTERN.test(opts.requestId))
|
|
27022
|
+
throw new Error("invalid replacement request id");
|
|
27023
|
+
const baseDir = opts.baseDir ?? process.env.ALOOK_DATA_DIR;
|
|
27024
|
+
if (!baseDir)
|
|
27025
|
+
throw new Error("daemon replace requires --base-dir");
|
|
27026
|
+
const intent = readIntent(baseDir, opts.id);
|
|
27027
|
+
if (intent.machineId !== opts.id || intent.requestId !== opts.requestId) {
|
|
27028
|
+
throw new Error("daemon update intent mismatch");
|
|
27029
|
+
}
|
|
27030
|
+
const launch = readDaemonLaunchRecord(baseDir, opts.id);
|
|
27031
|
+
const currentVersion = readDaemonVersion();
|
|
27032
|
+
if (!parseReleaseVersion(currentVersion) || !parseReleaseVersion(launch.daemonVersion)) {
|
|
27033
|
+
throw new Error("daemon replacement version is invalid");
|
|
27034
|
+
}
|
|
27035
|
+
if (currentVersion === launch.daemonVersion || !releaseVersionGte(currentVersion, launch.daemonVersion)) {
|
|
27036
|
+
appendUpdateLog(baseDir, opts.id, "replacement_not_newer", {
|
|
27037
|
+
currentVersion,
|
|
27038
|
+
priorVersion: launch.daemonVersion
|
|
27039
|
+
});
|
|
27040
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27041
|
+
return;
|
|
27042
|
+
}
|
|
27043
|
+
if (!tuplesEqual(currentPidTuple(baseDir, opts.id), intent)) {
|
|
27044
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27045
|
+
throw new Error("daemon ownership changed before replacement");
|
|
27046
|
+
}
|
|
27047
|
+
const npmPath = npmExecPath();
|
|
27048
|
+
const acquired = acquireDaemonReplacementLock({ baseDir, machineId: opts.id, requestId: opts.requestId });
|
|
27049
|
+
let oldStopped = false;
|
|
27050
|
+
try {
|
|
27051
|
+
if (!tuplesEqual(currentPidTuple(baseDir, opts.id), intent)) {
|
|
27052
|
+
throw new Error("daemon ownership changed after replacement lock");
|
|
27053
|
+
}
|
|
27054
|
+
appendUpdateLog(baseDir, opts.id, "replacement_started", {
|
|
27055
|
+
priorVersion: launch.daemonVersion,
|
|
27056
|
+
nextVersion: currentVersion
|
|
27057
|
+
});
|
|
27058
|
+
await stopExactDaemonPid(intent.pid);
|
|
27059
|
+
oldStopped = true;
|
|
27060
|
+
removeOldPidfileIfMatches(baseDir, intent);
|
|
27061
|
+
try {
|
|
27062
|
+
await daemonResume({ id: opts.id, baseDir, requestId: opts.requestId });
|
|
27063
|
+
appendUpdateLog(baseDir, opts.id, "replacement_ready", { version: currentVersion });
|
|
27064
|
+
} catch (error51) {
|
|
27065
|
+
appendUpdateLog(baseDir, opts.id, "replacement_start_failed", { error: error51 });
|
|
27066
|
+
await runPinnedResume({
|
|
27067
|
+
npmPath,
|
|
27068
|
+
version: launch.daemonVersion,
|
|
27069
|
+
machineId: opts.id,
|
|
27070
|
+
baseDir,
|
|
27071
|
+
requestId: opts.requestId
|
|
27072
|
+
});
|
|
27073
|
+
appendUpdateLog(baseDir, opts.id, "rollback_ready", { version: launch.daemonVersion });
|
|
27074
|
+
}
|
|
27075
|
+
} catch (error51) {
|
|
27076
|
+
appendUpdateLog(baseDir, opts.id, oldStopped ? "replacement_terminal_failure" : "replacement_aborted", { error: error51 });
|
|
27077
|
+
throw error51;
|
|
27078
|
+
} finally {
|
|
27079
|
+
removeIntentIfMatches(baseDir, opts.id, opts.requestId);
|
|
27080
|
+
removeReplacementLockIfMatches(acquired.path, acquired.lock);
|
|
27081
|
+
}
|
|
27082
|
+
}
|
|
27083
|
+
|
|
26479
27084
|
// src/cli/daemonRunner.ts
|
|
26480
27085
|
var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach", "friend"];
|
|
26481
27086
|
var DAEMON_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -26580,9 +27185,9 @@ async function buildRunnerDiagnosticBundle(args) {
|
|
|
26580
27185
|
});
|
|
26581
27186
|
}
|
|
26582
27187
|
function createDaemonProcessLogger(daemonDir, foreground) {
|
|
26583
|
-
|
|
26584
|
-
|
|
26585
|
-
const logPath =
|
|
27188
|
+
fs11.mkdirSync(daemonDir, { recursive: true, mode: 448 });
|
|
27189
|
+
fs11.chmodSync(daemonDir, 448);
|
|
27190
|
+
const logPath = path12.join(daemonDir, "daemon.log");
|
|
26586
27191
|
let warnedOversize = false;
|
|
26587
27192
|
const sink = createRotatingFileSink(logPath, DAEMON_LOG_MAX_BYTES, {
|
|
26588
27193
|
mode: 384,
|
|
@@ -26656,7 +27261,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26656
27261
|
return;
|
|
26657
27262
|
const marker = process.env.ALOOK_DAEMON_TEST_SHUTDOWN_MARKER;
|
|
26658
27263
|
if (marker)
|
|
26659
|
-
|
|
27264
|
+
fs11.writeFileSync(marker, String(process.pid), { mode: 384 });
|
|
26660
27265
|
await new Promise((resolve4) => setTimeout(resolve4, delayMs));
|
|
26661
27266
|
};
|
|
26662
27267
|
const stopDaemon = async () => {
|
|
@@ -26704,6 +27309,13 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26704
27309
|
});
|
|
26705
27310
|
try {
|
|
26706
27311
|
logDaemonStartup(log2, prepared);
|
|
27312
|
+
const handleSelfUpdate = createDaemonSelfUpdateHandler({
|
|
27313
|
+
machineId: prepared.machineId,
|
|
27314
|
+
baseDir: prepared.baseDir,
|
|
27315
|
+
pid: process.pid,
|
|
27316
|
+
startedAt: prepared.startedAt,
|
|
27317
|
+
ownerToken: prepared.ownerToken
|
|
27318
|
+
}, { logger: log2 });
|
|
26707
27319
|
daemon = await createDaemon({
|
|
26708
27320
|
machineKey: prepared.machineKey,
|
|
26709
27321
|
serverUrl: prepared.serverUrl,
|
|
@@ -26729,6 +27341,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26729
27341
|
osRelease: prepared.osRelease,
|
|
26730
27342
|
daemonVersion: prepared.daemonVersion,
|
|
26731
27343
|
logger: log2,
|
|
27344
|
+
handleSelfUpdate,
|
|
26732
27345
|
handleDiagnosticCommand,
|
|
26733
27346
|
reportDiagnosticFailure,
|
|
26734
27347
|
onDiagnosticSources: ({ fsmTraceSource, statusFilePath }) => {
|
|
@@ -26793,7 +27406,7 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
26793
27406
|
}
|
|
26794
27407
|
|
|
26795
27408
|
// src/cli/daemonStart.ts
|
|
26796
|
-
import { spawn as
|
|
27409
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
26797
27410
|
var STOP_GRACE_MS = 5000;
|
|
26798
27411
|
var STOP_KILL_GRACE_MS = 2000;
|
|
26799
27412
|
var POLL_MS2 = 100;
|
|
@@ -26803,16 +27416,16 @@ var RUNNER_KILL_GRACE_MS = 2000;
|
|
|
26803
27416
|
var MACHINE_ID_PATTERN = /^cm_[A-Za-z0-9_-]{8,64}$/;
|
|
26804
27417
|
var LEGACY_DAEMON_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
26805
27418
|
function resolveDefaultBaseDir() {
|
|
26806
|
-
const root = process.env.ALOOK_PROJECT_ROOT ||
|
|
26807
|
-
return
|
|
27419
|
+
const root = process.env.ALOOK_PROJECT_ROOT || path13.join(homedir4(), ".alook");
|
|
27420
|
+
return path13.join(root, "daemon");
|
|
26808
27421
|
}
|
|
26809
27422
|
var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
|
|
26810
27423
|
var log2 = createLogger2({ header: "@alook/daemon" });
|
|
26811
27424
|
function daemonsDir(baseDir) {
|
|
26812
|
-
return
|
|
27425
|
+
return path13.join(baseDir, "daemons");
|
|
26813
27426
|
}
|
|
26814
27427
|
function daemonDirById(baseDir, id) {
|
|
26815
|
-
return
|
|
27428
|
+
return path13.join(daemonsDir(baseDir), validateDaemonId(id));
|
|
26816
27429
|
}
|
|
26817
27430
|
function validateMachineId(machineId) {
|
|
26818
27431
|
if (!MACHINE_ID_PATTERN.test(machineId))
|
|
@@ -26828,10 +27441,10 @@ function validateDaemonId(id) {
|
|
|
26828
27441
|
return id;
|
|
26829
27442
|
}
|
|
26830
27443
|
function pidfilePathById(baseDir, id) {
|
|
26831
|
-
return
|
|
27444
|
+
return path13.join(daemonDirById(baseDir, id), "daemon.pid");
|
|
26832
27445
|
}
|
|
26833
27446
|
function statusFilePathById(baseDir, id) {
|
|
26834
|
-
return
|
|
27447
|
+
return path13.join(daemonDirById(baseDir, id), "status.json");
|
|
26835
27448
|
}
|
|
26836
27449
|
function isProcessAlive(pid) {
|
|
26837
27450
|
try {
|
|
@@ -26869,64 +27482,88 @@ function parsePidFileContent(raw) {
|
|
|
26869
27482
|
return null;
|
|
26870
27483
|
}
|
|
26871
27484
|
function readPidFile(filePath) {
|
|
26872
|
-
if (!
|
|
27485
|
+
if (!fs12.existsSync(filePath))
|
|
26873
27486
|
return null;
|
|
26874
27487
|
try {
|
|
26875
|
-
return parsePidFileContent(
|
|
27488
|
+
return parsePidFileContent(fs12.readFileSync(filePath, "utf8"));
|
|
26876
27489
|
} catch {}
|
|
26877
27490
|
return null;
|
|
26878
27491
|
}
|
|
26879
|
-
function
|
|
26880
|
-
|
|
26881
|
-
|
|
27492
|
+
function ensurePrivateDir2(dir) {
|
|
27493
|
+
fs12.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
27494
|
+
fs12.chmodSync(dir, 448);
|
|
26882
27495
|
}
|
|
26883
27496
|
function syncDirectory(dir) {
|
|
26884
27497
|
if (process.platform === "win32")
|
|
26885
27498
|
return;
|
|
26886
|
-
const fd =
|
|
27499
|
+
const fd = fs12.openSync(dir, "r");
|
|
26887
27500
|
try {
|
|
26888
|
-
|
|
27501
|
+
fs12.fsyncSync(fd);
|
|
26889
27502
|
} finally {
|
|
26890
|
-
|
|
27503
|
+
fs12.closeSync(fd);
|
|
26891
27504
|
}
|
|
26892
27505
|
}
|
|
26893
27506
|
function writeExclusive(filePath, value) {
|
|
26894
|
-
const dir =
|
|
26895
|
-
|
|
26896
|
-
const tempPath =
|
|
26897
|
-
let fd =
|
|
27507
|
+
const dir = path13.dirname(filePath);
|
|
27508
|
+
ensurePrivateDir2(dir);
|
|
27509
|
+
const tempPath = path13.join(dir, `.${path13.basename(filePath)}.${process.pid}.${crypto5.randomBytes(12).toString("hex")}.tmp`);
|
|
27510
|
+
let fd = fs12.openSync(tempPath, "wx", 384);
|
|
26898
27511
|
try {
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
27512
|
+
fs12.writeFileSync(fd, JSON.stringify(value));
|
|
27513
|
+
fs12.fsyncSync(fd);
|
|
27514
|
+
fs12.closeSync(fd);
|
|
26902
27515
|
fd = null;
|
|
26903
|
-
|
|
26904
|
-
|
|
27516
|
+
fs12.chmodSync(tempPath, 384);
|
|
27517
|
+
fs12.linkSync(tempPath, filePath);
|
|
26905
27518
|
syncDirectory(dir);
|
|
26906
27519
|
} finally {
|
|
26907
27520
|
if (fd !== null) {
|
|
26908
27521
|
try {
|
|
26909
|
-
|
|
27522
|
+
fs12.closeSync(fd);
|
|
26910
27523
|
} catch {}
|
|
26911
27524
|
}
|
|
26912
27525
|
try {
|
|
26913
|
-
|
|
27526
|
+
fs12.unlinkSync(tempPath);
|
|
27527
|
+
} catch {}
|
|
27528
|
+
}
|
|
27529
|
+
}
|
|
27530
|
+
function writePrivateJsonAtomic2(filePath, value) {
|
|
27531
|
+
const dir = path13.dirname(filePath);
|
|
27532
|
+
ensurePrivateDir2(dir);
|
|
27533
|
+
const tempPath = path13.join(dir, `.${path13.basename(filePath)}.${process.pid}.${crypto5.randomBytes(12).toString("hex")}.tmp`);
|
|
27534
|
+
let fd = fs12.openSync(tempPath, "wx", 384);
|
|
27535
|
+
try {
|
|
27536
|
+
fs12.writeFileSync(fd, JSON.stringify(value));
|
|
27537
|
+
fs12.fsyncSync(fd);
|
|
27538
|
+
fs12.closeSync(fd);
|
|
27539
|
+
fd = null;
|
|
27540
|
+
fs12.chmodSync(tempPath, 384);
|
|
27541
|
+
fs12.renameSync(tempPath, filePath);
|
|
27542
|
+
syncDirectory(dir);
|
|
27543
|
+
} finally {
|
|
27544
|
+
if (fd !== null) {
|
|
27545
|
+
try {
|
|
27546
|
+
fs12.closeSync(fd);
|
|
27547
|
+
} catch {}
|
|
27548
|
+
}
|
|
27549
|
+
try {
|
|
27550
|
+
fs12.unlinkSync(tempPath);
|
|
26914
27551
|
} catch {}
|
|
26915
27552
|
}
|
|
26916
27553
|
}
|
|
26917
27554
|
function secureExistingFile(filePath) {
|
|
26918
|
-
if (!
|
|
27555
|
+
if (!fs12.existsSync(filePath))
|
|
26919
27556
|
return;
|
|
26920
|
-
const stat =
|
|
27557
|
+
const stat = fs12.lstatSync(filePath);
|
|
26921
27558
|
if (!stat.isFile())
|
|
26922
27559
|
throw new Error("unsafe daemon ownership file type");
|
|
26923
|
-
|
|
27560
|
+
fs12.chmodSync(filePath, 384);
|
|
26924
27561
|
}
|
|
26925
27562
|
function removeOwnedFile(filePath, pid, ownerToken) {
|
|
26926
27563
|
try {
|
|
26927
27564
|
const content = readPidFile(filePath);
|
|
26928
27565
|
if (content?.pid === pid && content.ownerToken === ownerToken) {
|
|
26929
|
-
|
|
27566
|
+
fs12.unlinkSync(filePath);
|
|
26930
27567
|
}
|
|
26931
27568
|
} catch {}
|
|
26932
27569
|
}
|
|
@@ -26938,7 +27575,7 @@ function removePidFileIfMatches(filePath, expected) {
|
|
|
26938
27575
|
try {
|
|
26939
27576
|
const current = readPidFile(filePath);
|
|
26940
27577
|
if (current?.pid === expected.pid && current.key === expected.key)
|
|
26941
|
-
|
|
27578
|
+
fs12.unlinkSync(filePath);
|
|
26942
27579
|
} catch {}
|
|
26943
27580
|
}
|
|
26944
27581
|
function malformedPidHint(raw) {
|
|
@@ -26949,7 +27586,7 @@ function malformedPidHint(raw) {
|
|
|
26949
27586
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
26950
27587
|
}
|
|
26951
27588
|
function clearStaleOwnership(filePath, liveError) {
|
|
26952
|
-
if (!
|
|
27589
|
+
if (!fs12.existsSync(filePath))
|
|
26953
27590
|
return;
|
|
26954
27591
|
const valid = readPidFile(filePath);
|
|
26955
27592
|
if (valid) {
|
|
@@ -26961,9 +27598,9 @@ function clearStaleOwnership(filePath, liveError) {
|
|
|
26961
27598
|
let raw;
|
|
26962
27599
|
let before;
|
|
26963
27600
|
try {
|
|
26964
|
-
before =
|
|
26965
|
-
raw =
|
|
26966
|
-
const afterRead =
|
|
27601
|
+
before = fs12.statSync(filePath);
|
|
27602
|
+
raw = fs12.readFileSync(filePath, "utf8");
|
|
27603
|
+
const afterRead = fs12.statSync(filePath);
|
|
26967
27604
|
if (afterRead.dev !== before.dev || afterRead.ino !== before.ino || afterRead.size !== before.size || afterRead.mtimeMs !== before.mtimeMs)
|
|
26968
27605
|
return;
|
|
26969
27606
|
} catch {
|
|
@@ -26973,28 +27610,112 @@ function clearStaleOwnership(filePath, liveError) {
|
|
|
26973
27610
|
if (pid && isProcessAlive(pid))
|
|
26974
27611
|
throw liveError(pid);
|
|
26975
27612
|
try {
|
|
26976
|
-
const current =
|
|
27613
|
+
const current = fs12.statSync(filePath);
|
|
26977
27614
|
if (current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size || current.mtimeMs !== before.mtimeMs)
|
|
26978
27615
|
return;
|
|
26979
|
-
|
|
27616
|
+
fs12.unlinkSync(filePath);
|
|
27617
|
+
} catch {}
|
|
27618
|
+
}
|
|
27619
|
+
function replacementLockPathById(baseDir, machineId) {
|
|
27620
|
+
return path13.join(daemonDirById(baseDir, machineId), "daemon.replace.lock");
|
|
27621
|
+
}
|
|
27622
|
+
function readReplacementLock(filePath) {
|
|
27623
|
+
if (!fs12.existsSync(filePath))
|
|
27624
|
+
return null;
|
|
27625
|
+
try {
|
|
27626
|
+
const stat = fs12.lstatSync(filePath);
|
|
27627
|
+
if (!stat.isFile())
|
|
27628
|
+
throw new Error("unsafe daemon replacement lock type");
|
|
27629
|
+
fs12.chmodSync(filePath, 384);
|
|
27630
|
+
const content = JSON.parse(fs12.readFileSync(filePath, "utf8"));
|
|
27631
|
+
if (!Number.isInteger(content.pid) || (content.pid ?? 0) <= 0 || typeof content.machineId !== "string" || typeof content.startedAt !== "string" || typeof content.ownerToken !== "string" || typeof content.requestId !== "string")
|
|
27632
|
+
return null;
|
|
27633
|
+
return content;
|
|
27634
|
+
} catch (error51) {
|
|
27635
|
+
if (error51 instanceof SyntaxError)
|
|
27636
|
+
return null;
|
|
27637
|
+
throw error51;
|
|
27638
|
+
}
|
|
27639
|
+
}
|
|
27640
|
+
function replacementLocksEqual(a, b) {
|
|
27641
|
+
return a.pid === b.pid && a.machineId === b.machineId && a.startedAt === b.startedAt && a.ownerToken === b.ownerToken && a.requestId === b.requestId;
|
|
27642
|
+
}
|
|
27643
|
+
function removeReplacementLockIfMatches(filePath, expected) {
|
|
27644
|
+
try {
|
|
27645
|
+
const current = readReplacementLock(filePath);
|
|
27646
|
+
if (current && replacementLocksEqual(current, expected))
|
|
27647
|
+
fs12.unlinkSync(filePath);
|
|
26980
27648
|
} catch {}
|
|
26981
27649
|
}
|
|
27650
|
+
function checkReplacementLock(baseDir, machineId, resumeRequestId) {
|
|
27651
|
+
const lockPath = replacementLockPathById(baseDir, machineId);
|
|
27652
|
+
if (!fs12.existsSync(lockPath))
|
|
27653
|
+
return;
|
|
27654
|
+
const lock = readReplacementLock(lockPath);
|
|
27655
|
+
if (!lock) {
|
|
27656
|
+
let before;
|
|
27657
|
+
let raw;
|
|
27658
|
+
try {
|
|
27659
|
+
before = fs12.statSync(lockPath);
|
|
27660
|
+
raw = fs12.readFileSync(lockPath, "utf8");
|
|
27661
|
+
const afterRead = fs12.statSync(lockPath);
|
|
27662
|
+
if (afterRead.dev !== before.dev || afterRead.ino !== before.ino || afterRead.size !== before.size || afterRead.mtimeMs !== before.mtimeMs)
|
|
27663
|
+
throw new Error(`daemon '${machineId}' replacement lock changed during recovery`);
|
|
27664
|
+
} catch (error51) {
|
|
27665
|
+
if (error51?.code === "ENOENT")
|
|
27666
|
+
return;
|
|
27667
|
+
throw error51;
|
|
27668
|
+
}
|
|
27669
|
+
const pid = malformedPidHint(raw);
|
|
27670
|
+
if (pid && isProcessAlive(pid)) {
|
|
27671
|
+
throw new Error(`daemon '${machineId}' replacement lock is malformed but owned by live pid ${pid}`);
|
|
27672
|
+
}
|
|
27673
|
+
try {
|
|
27674
|
+
const current = fs12.statSync(lockPath);
|
|
27675
|
+
if (current.dev === before.dev && current.ino === before.ino && current.size === before.size && current.mtimeMs === before.mtimeMs)
|
|
27676
|
+
fs12.unlinkSync(lockPath);
|
|
27677
|
+
} catch {}
|
|
27678
|
+
return;
|
|
27679
|
+
}
|
|
27680
|
+
if (!isProcessAlive(lock.pid)) {
|
|
27681
|
+
removeReplacementLockIfMatches(lockPath, lock);
|
|
27682
|
+
return;
|
|
27683
|
+
}
|
|
27684
|
+
if (resumeRequestId && lock.requestId === resumeRequestId)
|
|
27685
|
+
return;
|
|
27686
|
+
throw new Error(`daemon '${machineId}' replacement already in progress (pid ${lock.pid})`);
|
|
27687
|
+
}
|
|
27688
|
+
function acquireDaemonReplacementLock(args) {
|
|
27689
|
+
const lockPath = replacementLockPathById(args.baseDir, args.machineId);
|
|
27690
|
+
ensurePrivateDir2(path13.dirname(lockPath));
|
|
27691
|
+
checkReplacementLock(args.baseDir, args.machineId);
|
|
27692
|
+
const lock = {
|
|
27693
|
+
pid: process.pid,
|
|
27694
|
+
machineId: args.machineId,
|
|
27695
|
+
startedAt: new Date().toISOString(),
|
|
27696
|
+
ownerToken: crypto5.randomBytes(24).toString("base64url"),
|
|
27697
|
+
requestId: args.requestId
|
|
27698
|
+
};
|
|
27699
|
+
writeExclusive(lockPath, lock);
|
|
27700
|
+
return { path: lockPath, lock };
|
|
27701
|
+
}
|
|
26982
27702
|
function coarseStartLockPath(baseDir) {
|
|
26983
|
-
return
|
|
27703
|
+
return path13.join(daemonsDir(baseDir), ".start.lock");
|
|
26984
27704
|
}
|
|
26985
27705
|
function acquireCoarseLock(baseDir, ownerToken) {
|
|
26986
27706
|
const lf = coarseStartLockPath(baseDir);
|
|
26987
|
-
|
|
27707
|
+
ensurePrivateDir2(path13.dirname(lf));
|
|
26988
27708
|
secureExistingFile(lf);
|
|
26989
27709
|
clearStaleOwnership(lf, (pid) => new Error(`another daemon start is in progress on this machine (pid ${pid})`));
|
|
26990
27710
|
writeExclusive(lf, { pid: process.pid, machineId: "coarse", startedAt: new Date().toISOString(), ownerToken });
|
|
26991
27711
|
return lf;
|
|
26992
27712
|
}
|
|
26993
|
-
function acquireLaunchLock(baseDir, machineId, ownerToken) {
|
|
27713
|
+
function acquireLaunchLock(baseDir, machineId, ownerToken, resumeRequestId) {
|
|
26994
27714
|
const daemonDir = daemonDirById(baseDir, machineId);
|
|
26995
|
-
|
|
26996
|
-
const lockPath =
|
|
27715
|
+
ensurePrivateDir2(daemonDir);
|
|
27716
|
+
const lockPath = path13.join(daemonDir, "daemon.launch.lock");
|
|
26997
27717
|
const finalPath = pidfilePathById(baseDir, machineId);
|
|
27718
|
+
checkReplacementLock(baseDir, machineId, resumeRequestId);
|
|
26998
27719
|
secureExistingFile(lockPath);
|
|
26999
27720
|
secureExistingFile(finalPath);
|
|
27000
27721
|
clearStaleOwnership(finalPath, (pid) => new Error(`daemon '${machineId}' already running (pid ${pid})`));
|
|
@@ -27009,14 +27730,14 @@ function commitFinalPidfile(baseDir, machineId, pid, startedAt, ownerToken) {
|
|
|
27009
27730
|
}
|
|
27010
27731
|
function legacyPidfileCandidates(baseDir) {
|
|
27011
27732
|
const dir = daemonsDir(baseDir);
|
|
27012
|
-
if (!
|
|
27733
|
+
if (!fs12.existsSync(dir))
|
|
27013
27734
|
return [];
|
|
27014
27735
|
const candidates = [];
|
|
27015
|
-
for (const entry of
|
|
27736
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
27016
27737
|
if (entry.isDirectory()) {
|
|
27017
|
-
candidates.push(
|
|
27738
|
+
candidates.push(path13.join(dir, entry.name, "daemon.pid"));
|
|
27018
27739
|
} else if (entry.isFile() && entry.name.endsWith(".pid")) {
|
|
27019
|
-
candidates.push(
|
|
27740
|
+
candidates.push(path13.join(dir, entry.name));
|
|
27020
27741
|
}
|
|
27021
27742
|
}
|
|
27022
27743
|
return candidates;
|
|
@@ -27039,7 +27760,7 @@ function reconcileLegacyMachineKeyOwnership(baseDir, machineKey) {
|
|
|
27039
27760
|
function daemonList(opts) {
|
|
27040
27761
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
27041
27762
|
const dir = daemonsDir(baseDir);
|
|
27042
|
-
if (!
|
|
27763
|
+
if (!fs12.existsSync(dir))
|
|
27043
27764
|
return [];
|
|
27044
27765
|
const results = [];
|
|
27045
27766
|
const now = Date.now();
|
|
@@ -27064,10 +27785,10 @@ function daemonList(opts) {
|
|
|
27064
27785
|
}
|
|
27065
27786
|
results.push({ id, pid: data.pid, alive, agents, running, lastActiveMs });
|
|
27066
27787
|
};
|
|
27067
|
-
for (const entry of
|
|
27788
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
27068
27789
|
if (entry.isDirectory() && isDaemonId(entry.name)) {
|
|
27069
27790
|
const id = entry.name;
|
|
27070
|
-
pushRow(id,
|
|
27791
|
+
pushRow(id, path13.join(dir, id, "daemon.pid"), path13.join(dir, id, "status.json"));
|
|
27071
27792
|
}
|
|
27072
27793
|
}
|
|
27073
27794
|
return results;
|
|
@@ -27075,10 +27796,10 @@ function daemonList(opts) {
|
|
|
27075
27796
|
var STATUS_STALE_MS = 20000;
|
|
27076
27797
|
var MISSING_STATUS = { found: false, ageMs: null, freshness: "missing", writtenAt: null, agents: [] };
|
|
27077
27798
|
function daemonStatusFromFile(statusPath, nowMs) {
|
|
27078
|
-
if (!
|
|
27799
|
+
if (!fs12.existsSync(statusPath))
|
|
27079
27800
|
return MISSING_STATUS;
|
|
27080
27801
|
try {
|
|
27081
|
-
const snap = JSON.parse(
|
|
27802
|
+
const snap = JSON.parse(fs12.readFileSync(statusPath, "utf8"));
|
|
27082
27803
|
const ageMs = nowMs - snap.writtenAt;
|
|
27083
27804
|
return {
|
|
27084
27805
|
found: true,
|
|
@@ -27093,11 +27814,11 @@ function daemonStatusFromFile(statusPath, nowMs) {
|
|
|
27093
27814
|
}
|
|
27094
27815
|
function daemonIdsWithStatus(baseDir) {
|
|
27095
27816
|
const dir = daemonsDir(baseDir);
|
|
27096
|
-
if (!
|
|
27817
|
+
if (!fs12.existsSync(dir))
|
|
27097
27818
|
return [];
|
|
27098
27819
|
const ids = [];
|
|
27099
|
-
for (const entry of
|
|
27100
|
-
if (entry.isDirectory() && isDaemonId(entry.name) &&
|
|
27820
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
27821
|
+
if (entry.isDirectory() && isDaemonId(entry.name) && fs12.existsSync(path13.join(dir, entry.name, "status.json"))) {
|
|
27101
27822
|
ids.push(entry.name);
|
|
27102
27823
|
}
|
|
27103
27824
|
}
|
|
@@ -27118,6 +27839,30 @@ function daemonStatus(opts) {
|
|
|
27118
27839
|
}
|
|
27119
27840
|
return MISSING_STATUS;
|
|
27120
27841
|
}
|
|
27842
|
+
async function stopExactPid(pid) {
|
|
27843
|
+
try {
|
|
27844
|
+
process.kill(pid, "SIGTERM");
|
|
27845
|
+
} catch (error51) {
|
|
27846
|
+
if (isProcessAlive(pid))
|
|
27847
|
+
throw error51;
|
|
27848
|
+
}
|
|
27849
|
+
if (!await waitForPidExit(pid, STOP_GRACE_MS)) {
|
|
27850
|
+
try {
|
|
27851
|
+
process.kill(pid, "SIGKILL");
|
|
27852
|
+
} catch (error51) {
|
|
27853
|
+
if (isProcessAlive(pid))
|
|
27854
|
+
throw error51;
|
|
27855
|
+
}
|
|
27856
|
+
if (!await waitForPidExit(pid, STOP_KILL_GRACE_MS)) {
|
|
27857
|
+
throw new Error(`daemon (pid ${pid}) is still running after SIGKILL`);
|
|
27858
|
+
}
|
|
27859
|
+
}
|
|
27860
|
+
}
|
|
27861
|
+
async function stopExactDaemonPid(pid) {
|
|
27862
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
27863
|
+
throw new Error("invalid daemon pid");
|
|
27864
|
+
await stopExactPid(pid);
|
|
27865
|
+
}
|
|
27121
27866
|
async function stopByPidfile(pf, notFoundHint) {
|
|
27122
27867
|
const data = readPidFile(pf);
|
|
27123
27868
|
if (!data) {
|
|
@@ -27130,81 +27875,63 @@ async function stopByPidfile(pf, notFoundHint) {
|
|
|
27130
27875
|
return;
|
|
27131
27876
|
}
|
|
27132
27877
|
log2.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
|
|
27133
|
-
|
|
27134
|
-
process.kill(data.pid, "SIGTERM");
|
|
27135
|
-
} catch (error51) {
|
|
27136
|
-
if (isProcessAlive(data.pid))
|
|
27137
|
-
throw error51;
|
|
27138
|
-
}
|
|
27139
|
-
if (!await waitForPidExit(data.pid, STOP_GRACE_MS)) {
|
|
27140
|
-
log2.error(`daemon (pid ${data.pid}) did not exit in ${STOP_GRACE_MS / 1000}s — sending SIGKILL`);
|
|
27141
|
-
try {
|
|
27142
|
-
process.kill(data.pid, "SIGKILL");
|
|
27143
|
-
} catch (error51) {
|
|
27144
|
-
if (isProcessAlive(data.pid))
|
|
27145
|
-
throw error51;
|
|
27146
|
-
}
|
|
27147
|
-
if (!await waitForPidExit(data.pid, STOP_KILL_GRACE_MS)) {
|
|
27148
|
-
throw new Error(`daemon (pid ${data.pid}) is still running after SIGKILL`);
|
|
27149
|
-
}
|
|
27150
|
-
}
|
|
27878
|
+
await stopExactPid(data.pid);
|
|
27151
27879
|
log2.info("daemon stopped");
|
|
27152
27880
|
removePidFileIfMatches(pf, data);
|
|
27153
27881
|
}
|
|
27154
27882
|
async function daemonStop(opts) {
|
|
27155
27883
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
27156
|
-
await stopByPidfile(
|
|
27884
|
+
await stopByPidfile(path13.join(daemonDirById(baseDir, opts.id), "daemon.pid"), `no daemon with id '${opts.id}' (pidfile not found — check \`alook daemon list\`)`);
|
|
27157
27885
|
}
|
|
27158
27886
|
function credentialFilePathByMachineId(baseDir, machineId) {
|
|
27159
|
-
return
|
|
27887
|
+
return path13.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
|
|
27160
27888
|
}
|
|
27161
27889
|
function readCredentialFile(filePath) {
|
|
27162
|
-
if (!
|
|
27890
|
+
if (!fs12.existsSync(filePath))
|
|
27163
27891
|
return null;
|
|
27164
|
-
const stat =
|
|
27892
|
+
const stat = fs12.lstatSync(filePath);
|
|
27165
27893
|
if (!stat.isFile())
|
|
27166
27894
|
throw new Error("unsafe daemon credential file type");
|
|
27167
|
-
|
|
27895
|
+
fs12.chmodSync(filePath, 384);
|
|
27168
27896
|
try {
|
|
27169
|
-
const content = JSON.parse(
|
|
27897
|
+
const content = JSON.parse(fs12.readFileSync(filePath, "utf8"));
|
|
27170
27898
|
if (typeof content.credential === "string" && content.credential.startsWith("cmk_") && typeof content.machineId === "string") {
|
|
27899
|
+
if (content.schemaVersion === 1 && typeof content.serverUrl === "string" && typeof content.wsUrl === "string" && typeof content.daemonVersion === "string") {
|
|
27900
|
+
return {
|
|
27901
|
+
schemaVersion: 1,
|
|
27902
|
+
credential: content.credential,
|
|
27903
|
+
machineId: content.machineId,
|
|
27904
|
+
serverUrl: content.serverUrl,
|
|
27905
|
+
wsUrl: content.wsUrl,
|
|
27906
|
+
daemonVersion: content.daemonVersion
|
|
27907
|
+
};
|
|
27908
|
+
}
|
|
27171
27909
|
return { credential: content.credential, machineId: content.machineId };
|
|
27172
27910
|
}
|
|
27173
27911
|
} catch {}
|
|
27174
27912
|
return null;
|
|
27175
27913
|
}
|
|
27176
|
-
function writeCredentialFile(filePath,
|
|
27177
|
-
|
|
27178
|
-
|
|
27179
|
-
|
|
27180
|
-
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
fs11.fsyncSync(fd);
|
|
27184
|
-
fs11.closeSync(fd);
|
|
27185
|
-
fd = null;
|
|
27186
|
-
fs11.chmodSync(tempPath, 384);
|
|
27187
|
-
fs11.renameSync(tempPath, filePath);
|
|
27188
|
-
syncDirectory(dir);
|
|
27189
|
-
} finally {
|
|
27190
|
-
if (fd !== null) {
|
|
27191
|
-
try {
|
|
27192
|
-
fs11.closeSync(fd);
|
|
27193
|
-
} catch {}
|
|
27194
|
-
}
|
|
27195
|
-
try {
|
|
27196
|
-
fs11.unlinkSync(tempPath);
|
|
27197
|
-
} catch {}
|
|
27914
|
+
function writeCredentialFile(filePath, record2) {
|
|
27915
|
+
writePrivateJsonAtomic2(filePath, record2);
|
|
27916
|
+
}
|
|
27917
|
+
function readDaemonLaunchRecord(baseDir, machineId) {
|
|
27918
|
+
const record2 = readCredentialFile(credentialFilePathByMachineId(baseDir, machineId));
|
|
27919
|
+
if (!record2 || !("schemaVersion" in record2) || record2.schemaVersion !== 1 || !parseReleaseVersion(record2.daemonVersion)) {
|
|
27920
|
+
throw new Error("daemon launch record is missing or requires a manual start upgrade");
|
|
27198
27921
|
}
|
|
27922
|
+
validateMachineId(record2.machineId);
|
|
27923
|
+
if (record2.machineId !== machineId)
|
|
27924
|
+
throw new Error("daemon launch record machine mismatch");
|
|
27925
|
+
return record2;
|
|
27199
27926
|
}
|
|
27200
27927
|
function findExistingCredentialForBearer(baseDir, bearer) {
|
|
27201
27928
|
const dir = daemonsDir(baseDir);
|
|
27202
|
-
if (!
|
|
27929
|
+
if (!fs12.existsSync(dir))
|
|
27203
27930
|
return null;
|
|
27204
|
-
for (const file2 of
|
|
27931
|
+
for (const file2 of fs12.readdirSync(dir)) {
|
|
27205
27932
|
if (!file2.endsWith(".credential.json"))
|
|
27206
27933
|
continue;
|
|
27207
|
-
const parsed = readCredentialFile(
|
|
27934
|
+
const parsed = readCredentialFile(path13.join(dir, file2));
|
|
27208
27935
|
if (parsed && parsed.credential === bearer)
|
|
27209
27936
|
return parsed;
|
|
27210
27937
|
}
|
|
@@ -27246,7 +27973,10 @@ async function prepareDaemonStart(opts) {
|
|
|
27246
27973
|
throw new Error("invalid machine key format — expected `cmt_` or `cmk_`");
|
|
27247
27974
|
}
|
|
27248
27975
|
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
27249
|
-
const
|
|
27976
|
+
const daemonVersion = readDaemonVersion();
|
|
27977
|
+
if (!parseReleaseVersion(daemonVersion))
|
|
27978
|
+
throw new Error("daemon package version is not a strict release version");
|
|
27979
|
+
const ownerToken = crypto5.randomBytes(24).toString("base64url");
|
|
27250
27980
|
const persisted = opts.machineKey.startsWith("cmk_") ? findExistingCredentialForBearer(baseDir, opts.machineKey) : null;
|
|
27251
27981
|
let coarseLockPath = null;
|
|
27252
27982
|
let launchLockPath = null;
|
|
@@ -27254,7 +27984,7 @@ async function prepareDaemonStart(opts) {
|
|
|
27254
27984
|
let machineKey = persisted?.credential;
|
|
27255
27985
|
let machineId = persisted ? validateMachineId(persisted.machineId) : undefined;
|
|
27256
27986
|
if (machineKey && machineId) {
|
|
27257
|
-
launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken);
|
|
27987
|
+
launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken, opts.resumeRequestId);
|
|
27258
27988
|
} else {
|
|
27259
27989
|
coarseLockPath = acquireCoarseLock(baseDir, ownerToken);
|
|
27260
27990
|
if (opts.machineKey.startsWith("cmk_")) {
|
|
@@ -27264,15 +27994,22 @@ async function prepareDaemonStart(opts) {
|
|
|
27264
27994
|
const runtimeReport = await detectRuntimes();
|
|
27265
27995
|
const healthyRuntimeIds = runtimeReport.filter((runtime) => runtime.status === "healthy").map((runtime) => runtime.id);
|
|
27266
27996
|
if (opts.machineKey.startsWith("cmt_")) {
|
|
27267
|
-
const activated = await activatePairingToken(serverUrl, opts.machineKey, os3.hostname(), process.platform, process.arch, os3.release(),
|
|
27997
|
+
const activated = await activatePairingToken(serverUrl, opts.machineKey, os3.hostname(), process.platform, process.arch, os3.release(), daemonVersion, runtimeReport);
|
|
27268
27998
|
machineKey = activated.credential;
|
|
27269
27999
|
machineId = validateMachineId(activated.machineId);
|
|
27270
28000
|
} else {
|
|
27271
28001
|
machineKey ??= opts.machineKey;
|
|
27272
28002
|
machineId ??= validateMachineId(await resolveMachineIdentity(serverUrl, opts.machineKey));
|
|
27273
28003
|
}
|
|
27274
|
-
|
|
27275
|
-
|
|
28004
|
+
launchLockPath ??= acquireLaunchLock(baseDir, machineId, ownerToken, opts.resumeRequestId);
|
|
28005
|
+
writeCredentialFile(credentialFilePathByMachineId(baseDir, machineId), {
|
|
28006
|
+
schemaVersion: 1,
|
|
28007
|
+
credential: machineKey,
|
|
28008
|
+
machineId,
|
|
28009
|
+
serverUrl,
|
|
28010
|
+
wsUrl,
|
|
28011
|
+
daemonVersion
|
|
28012
|
+
});
|
|
27276
28013
|
if (coarseLockPath)
|
|
27277
28014
|
removeOwnedFile(coarseLockPath, process.pid, ownerToken);
|
|
27278
28015
|
const startedAt = new Date().toISOString();
|
|
@@ -27293,7 +28030,7 @@ async function prepareDaemonStart(opts) {
|
|
|
27293
28030
|
platform: process.platform,
|
|
27294
28031
|
arch: process.arch,
|
|
27295
28032
|
osRelease: os3.release(),
|
|
27296
|
-
daemonVersion
|
|
28033
|
+
daemonVersion,
|
|
27297
28034
|
ownerToken,
|
|
27298
28035
|
startedAt
|
|
27299
28036
|
}
|
|
@@ -27306,12 +28043,37 @@ async function prepareDaemonStart(opts) {
|
|
|
27306
28043
|
throw error51;
|
|
27307
28044
|
}
|
|
27308
28045
|
}
|
|
28046
|
+
async function daemonResume(opts) {
|
|
28047
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/.test(opts.requestId))
|
|
28048
|
+
throw new Error("invalid replacement request id");
|
|
28049
|
+
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
28050
|
+
const record2 = readDaemonLaunchRecord(baseDir, opts.id);
|
|
28051
|
+
if (false) {}
|
|
28052
|
+
await daemonStart({
|
|
28053
|
+
machineKey: record2.credential,
|
|
28054
|
+
serverUrl: record2.serverUrl,
|
|
28055
|
+
wsUrl: record2.wsUrl,
|
|
28056
|
+
baseDir,
|
|
28057
|
+
resumeRequestId: opts.requestId
|
|
28058
|
+
});
|
|
28059
|
+
}
|
|
28060
|
+
async function daemonStartById(opts) {
|
|
28061
|
+
const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
|
|
28062
|
+
const record2 = readDaemonLaunchRecord(baseDir, opts.id);
|
|
28063
|
+
await daemonStart({
|
|
28064
|
+
machineKey: record2.credential,
|
|
28065
|
+
serverUrl: record2.serverUrl,
|
|
28066
|
+
wsUrl: record2.wsUrl,
|
|
28067
|
+
baseDir,
|
|
28068
|
+
foreground: opts.foreground
|
|
28069
|
+
});
|
|
28070
|
+
}
|
|
27309
28071
|
function runnerArguments() {
|
|
27310
28072
|
const command = process.env.ALOOK_DAEMON_PACKAGE_WRAPPER === "1" ? ["run"] : ["daemon", "run"];
|
|
27311
28073
|
return [...process.execArgv, process.argv[1], ...command];
|
|
27312
28074
|
}
|
|
27313
28075
|
function spawnBlockedRunner() {
|
|
27314
|
-
return
|
|
28076
|
+
return spawn3(process.execPath, runnerArguments(), {
|
|
27315
28077
|
detached: true,
|
|
27316
28078
|
stdio: ["ignore", "ignore", "ignore", "ipc"]
|
|
27317
28079
|
});
|
|
@@ -27322,9 +28084,9 @@ function testCheckpoint(name, childPid) {
|
|
|
27322
28084
|
const checkpointFile = process.env.ALOOK_DAEMON_TEST_CHECKPOINT_FILE;
|
|
27323
28085
|
if (!checkpointFile)
|
|
27324
28086
|
return;
|
|
27325
|
-
|
|
28087
|
+
fs12.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
|
|
27326
28088
|
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
27327
|
-
while (!
|
|
28089
|
+
while (!fs12.existsSync(`${checkpointFile}.continue`))
|
|
27328
28090
|
Atomics.wait(view, 0, 0, 25);
|
|
27329
28091
|
}
|
|
27330
28092
|
function sendPrepared(child, prepared) {
|
|
@@ -27350,7 +28112,7 @@ function waitForReceipt(child, prepared) {
|
|
|
27350
28112
|
};
|
|
27351
28113
|
const configuredTestTimeout = NaN;
|
|
27352
28114
|
const receiptTimeoutMs = Number.isFinite(configuredTestTimeout) && configuredTestTimeout > 0 ? configuredTestTimeout : START_RECEIPT_TIMEOUT_MS;
|
|
27353
|
-
const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${
|
|
28115
|
+
const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${path13.join(prepared.daemonDir, "daemon.log")}`)), receiptTimeoutMs);
|
|
27354
28116
|
child.on("message", (message2) => {
|
|
27355
28117
|
const accepted = message2;
|
|
27356
28118
|
if (accepted.type === "daemon:accepted") {
|
|
@@ -27369,7 +28131,7 @@ function waitForReceipt(child, prepared) {
|
|
|
27369
28131
|
settle(undefined, receipt);
|
|
27370
28132
|
});
|
|
27371
28133
|
child.once("exit", (code, signal) => {
|
|
27372
|
-
settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${
|
|
28134
|
+
settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${path13.join(prepared.daemonDir, "daemon.log")}`));
|
|
27373
28135
|
});
|
|
27374
28136
|
child.once("error", (error51) => settle(error51));
|
|
27375
28137
|
});
|
|
@@ -27502,6 +28264,70 @@ function sendIpcBestEffort(ipc, message2, disconnectAfter = false) {
|
|
|
27502
28264
|
}
|
|
27503
28265
|
}
|
|
27504
28266
|
|
|
28267
|
+
// src/cli/messageReminderClient.ts
|
|
28268
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
28269
|
+
var LOCAL_MESSAGE_REMINDER_PATH2 = "/__alook/local/message-reminder";
|
|
28270
|
+
var MIN_REMINDER_MS = 60000;
|
|
28271
|
+
var MAX_REMINDER_MS = 24 * 60 * 60000;
|
|
28272
|
+
function parseRemindAfter(value) {
|
|
28273
|
+
const match = /^(\d+)(m|h)$/.exec(value);
|
|
28274
|
+
if (!match) {
|
|
28275
|
+
throw new Error("message send: --remind-after must be a positive integer followed by m or h (1m..24h)");
|
|
28276
|
+
}
|
|
28277
|
+
const amount = Number(match[1]);
|
|
28278
|
+
const milliseconds = amount * (match[2] === "h" ? 60 * 60000 : 60000);
|
|
28279
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < MIN_REMINDER_MS || milliseconds > MAX_REMINDER_MS) {
|
|
28280
|
+
throw new Error("message send: --remind-after must be between 1m and 24h");
|
|
28281
|
+
}
|
|
28282
|
+
return milliseconds;
|
|
28283
|
+
}
|
|
28284
|
+
async function armMessageReminderFromEnv(input, env = process.env, fetchImpl = fetch) {
|
|
28285
|
+
const proxyUrl = env.ALOOK_PROXY_URL;
|
|
28286
|
+
const tokenFile = env.ALOOK_PROXY_TOKEN_FILE;
|
|
28287
|
+
if (!proxyUrl || !tokenFile) {
|
|
28288
|
+
return { armed: false, reason: "local reminder proxy unavailable" };
|
|
28289
|
+
}
|
|
28290
|
+
let voucher;
|
|
28291
|
+
try {
|
|
28292
|
+
voucher = readFileSync9(tokenFile, "utf8").trim();
|
|
28293
|
+
} catch {
|
|
28294
|
+
return { armed: false, reason: "local reminder voucher unavailable" };
|
|
28295
|
+
}
|
|
28296
|
+
if (!voucher)
|
|
28297
|
+
return { armed: false, reason: "local reminder voucher unavailable" };
|
|
28298
|
+
let response;
|
|
28299
|
+
try {
|
|
28300
|
+
response = await fetchImpl(`${proxyUrl.replace(/\/+$/, "")}${LOCAL_MESSAGE_REMINDER_PATH2}`, {
|
|
28301
|
+
method: "PUT",
|
|
28302
|
+
headers: {
|
|
28303
|
+
authorization: `Bearer ${voucher}`,
|
|
28304
|
+
"content-type": "application/json"
|
|
28305
|
+
},
|
|
28306
|
+
body: JSON.stringify(input),
|
|
28307
|
+
signal: AbortSignal.timeout(2000)
|
|
28308
|
+
});
|
|
28309
|
+
} catch {
|
|
28310
|
+
return { armed: false, reason: "local reminder request failed" };
|
|
28311
|
+
}
|
|
28312
|
+
let body;
|
|
28313
|
+
try {
|
|
28314
|
+
body = await response.json();
|
|
28315
|
+
} catch {
|
|
28316
|
+
return { armed: false, reason: "local reminder returned an invalid response" };
|
|
28317
|
+
}
|
|
28318
|
+
if (!response.ok) {
|
|
28319
|
+
const code = typeof body === "object" && body !== null && typeof body.code === "string" ? body.code : `http_${response.status}`;
|
|
28320
|
+
return { armed: false, reason: `local reminder rejected (${code})` };
|
|
28321
|
+
}
|
|
28322
|
+
if (typeof body === "object" && body !== null && body.armed === true && Number.isSafeInteger(body.dueAt)) {
|
|
28323
|
+
return { armed: true, dueAt: body.dueAt };
|
|
28324
|
+
}
|
|
28325
|
+
if (typeof body === "object" && body !== null && body.armed === false && typeof body.reason === "string") {
|
|
28326
|
+
return { armed: false, reason: body.reason };
|
|
28327
|
+
}
|
|
28328
|
+
return { armed: false, reason: "local reminder returned an invalid response" };
|
|
28329
|
+
}
|
|
28330
|
+
|
|
27505
28331
|
// src/cli/index.ts
|
|
27506
28332
|
function messagesInLocalTime(messages) {
|
|
27507
28333
|
return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
|
|
@@ -27635,6 +28461,8 @@ async function sendWithRetry(api2, req) {
|
|
|
27635
28461
|
return withTransientMutationRetry(() => api2.send(req));
|
|
27636
28462
|
}
|
|
27637
28463
|
async function cmdMessageSend(opts) {
|
|
28464
|
+
const remindAfterFlag = opts.remindAfter;
|
|
28465
|
+
const remindAfterMs = remindAfterFlag === undefined ? undefined : parseRemindAfter(remindAfterFlag);
|
|
27638
28466
|
const api2 = getApi();
|
|
27639
28467
|
const agent2 = agentId(opts);
|
|
27640
28468
|
const channel2 = opts.target;
|
|
@@ -27644,10 +28472,10 @@ async function cmdMessageSend(opts) {
|
|
|
27644
28472
|
const fileFlag = opts.file;
|
|
27645
28473
|
const textFlag = opts.text;
|
|
27646
28474
|
if (fileFlag) {
|
|
27647
|
-
const
|
|
27648
|
-
if (!
|
|
28475
|
+
const fs13 = await import("fs");
|
|
28476
|
+
if (!fs13.existsSync(fileFlag))
|
|
27649
28477
|
throw new CliError(`message send: file not found: ${fileFlag}`);
|
|
27650
|
-
text2 =
|
|
28478
|
+
text2 = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27651
28479
|
} else if (typeof textFlag === "string") {
|
|
27652
28480
|
text2 = decodeTextEscapes(textFlag);
|
|
27653
28481
|
}
|
|
@@ -27667,7 +28495,7 @@ async function cmdMessageSend(opts) {
|
|
|
27667
28495
|
}
|
|
27668
28496
|
replyToSeq = n;
|
|
27669
28497
|
}
|
|
27670
|
-
const nonce =
|
|
28498
|
+
const nonce = randomUUID6();
|
|
27671
28499
|
const res = await sendWithRetry(api2, {
|
|
27672
28500
|
agentId: agent2,
|
|
27673
28501
|
channel: channel2,
|
|
@@ -27677,9 +28505,26 @@ async function cmdMessageSend(opts) {
|
|
|
27677
28505
|
nonce
|
|
27678
28506
|
});
|
|
27679
28507
|
if (res.state === "blocked") {
|
|
27680
|
-
throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` to
|
|
28508
|
+
throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` and READ the new messages before deciding whether to resend, adjust, or skip your message.`);
|
|
28509
|
+
}
|
|
28510
|
+
const sent = `${res.message.channel}${res.message.seq}`;
|
|
28511
|
+
if (remindAfterMs === undefined)
|
|
28512
|
+
return { sent };
|
|
28513
|
+
const seqText = res.message.seq.replace(/^#/, "");
|
|
28514
|
+
const sentSeq = Number(seqText);
|
|
28515
|
+
if (!/^\d+$/.test(seqText) || !Number.isSafeInteger(sentSeq) || sentSeq < 1) {
|
|
28516
|
+
return { sent, reminder: { armed: false, reason: "server returned an invalid canonical message seq" } };
|
|
28517
|
+
}
|
|
28518
|
+
try {
|
|
28519
|
+
const reminder = await armMessageReminderFromEnv({
|
|
28520
|
+
channel: res.message.channel,
|
|
28521
|
+
sentSeq,
|
|
28522
|
+
remindAfterMs
|
|
28523
|
+
});
|
|
28524
|
+
return { sent, reminder };
|
|
28525
|
+
} catch {
|
|
28526
|
+
return { sent, reminder: { armed: false, reason: "local reminder request failed" } };
|
|
27681
28527
|
}
|
|
27682
|
-
return { sent: `${res.message.channel}${res.message.seq}` };
|
|
27683
28528
|
}
|
|
27684
28529
|
async function createPostWithRetry(api2, req) {
|
|
27685
28530
|
return withTransientMutationRetry(() => api2.createPost(req));
|
|
@@ -27697,10 +28542,10 @@ async function cmdMessagePost(opts) {
|
|
|
27697
28542
|
const fileFlag = opts.file;
|
|
27698
28543
|
const textFlag = opts.text;
|
|
27699
28544
|
if (fileFlag) {
|
|
27700
|
-
const
|
|
27701
|
-
if (!
|
|
28545
|
+
const fs13 = await import("fs");
|
|
28546
|
+
if (!fs13.existsSync(fileFlag))
|
|
27702
28547
|
throw new CliError(`message post: file not found: ${fileFlag}`);
|
|
27703
|
-
text2 =
|
|
28548
|
+
text2 = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27704
28549
|
} else if (typeof textFlag === "string") {
|
|
27705
28550
|
text2 = decodeTextEscapes(textFlag);
|
|
27706
28551
|
}
|
|
@@ -27709,7 +28554,7 @@ async function cmdMessagePost(opts) {
|
|
|
27709
28554
|
if (!hasText && attachmentIds.length === 0) {
|
|
27710
28555
|
throw new CliError("message post: --text <text>, --file <path>, or --attachment <id> is required");
|
|
27711
28556
|
}
|
|
27712
|
-
const nonce =
|
|
28557
|
+
const nonce = randomUUID6();
|
|
27713
28558
|
const res = await createPostWithRetry(api2, {
|
|
27714
28559
|
agentId: agent2,
|
|
27715
28560
|
forum,
|
|
@@ -27784,10 +28629,10 @@ async function cmdAttachmentUpload(opts) {
|
|
|
27784
28629
|
throw new CliError("message attachment upload: --target <ref> is required");
|
|
27785
28630
|
if (!filePath)
|
|
27786
28631
|
throw new CliError("message attachment upload: --file <path> is required");
|
|
27787
|
-
const
|
|
28632
|
+
const fs13 = await import("fs/promises");
|
|
27788
28633
|
let bytes;
|
|
27789
28634
|
try {
|
|
27790
|
-
bytes = await
|
|
28635
|
+
bytes = await fs13.readFile(filePath);
|
|
27791
28636
|
} catch (err) {
|
|
27792
28637
|
throw new CliError(`message attachment upload: cannot read file: ${err.message}`);
|
|
27793
28638
|
}
|
|
@@ -27841,13 +28686,13 @@ async function cmdAttachmentDownload(opts) {
|
|
|
27841
28686
|
const destPath = outFlag ?? pathMod.join(os4.tmpdir(), "alook-attachments", agent2, id, "file");
|
|
27842
28687
|
const result = await api2.attachmentDownload({ agentId: agent2, id, destPath });
|
|
27843
28688
|
if (!outFlag) {
|
|
27844
|
-
const
|
|
28689
|
+
const fs13 = await import("fs/promises");
|
|
27845
28690
|
const destDir = pathMod.dirname(destPath);
|
|
27846
28691
|
const safeName = pathMod.basename(result.filename) || "file";
|
|
27847
28692
|
const renamed = pathMod.join(destDir, safeName);
|
|
27848
28693
|
if (renamed !== destPath) {
|
|
27849
28694
|
try {
|
|
27850
|
-
await
|
|
28695
|
+
await fs13.rename(destPath, renamed);
|
|
27851
28696
|
return { ...result, path: renamed };
|
|
27852
28697
|
} catch {
|
|
27853
28698
|
return { ...result, path: destPath };
|
|
@@ -27982,10 +28827,10 @@ async function cmdNap(opts) {
|
|
|
27982
28827
|
const textFlag = opts.text;
|
|
27983
28828
|
let handoff;
|
|
27984
28829
|
if (fileFlag) {
|
|
27985
|
-
const
|
|
27986
|
-
if (!
|
|
28830
|
+
const fs13 = await import("fs");
|
|
28831
|
+
if (!fs13.existsSync(fileFlag))
|
|
27987
28832
|
throw new CliError(`nap: handoff file not found: ${fileFlag}`);
|
|
27988
|
-
handoff =
|
|
28833
|
+
handoff = fs13.readFileSync(fileFlag, "utf8").trim();
|
|
27989
28834
|
} else if (typeof textFlag === "string") {
|
|
27990
28835
|
handoff = decodeTextEscapes(textFlag).trim();
|
|
27991
28836
|
}
|
|
@@ -28001,7 +28846,7 @@ function buildProgram() {
|
|
|
28001
28846
|
}).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
|
|
28002
28847
|
const message2 = program.command("message").description("message operations").exitOverride();
|
|
28003
28848
|
message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
28004
|
-
message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28849
|
+
message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').option("--remind-after <duration>", "optionally arm one local follow-up wake after 1m..24h; a newer same-scope message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28005
28850
|
const localOpts = this.opts();
|
|
28006
28851
|
const globalOpts = program.opts();
|
|
28007
28852
|
const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
|
|
@@ -28117,10 +28962,23 @@ function buildProgram() {
|
|
|
28117
28962
|
});
|
|
28118
28963
|
const daemon = program.command("daemon").description("daemon operations").exitOverride();
|
|
28119
28964
|
daemon.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
28120
|
-
daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").
|
|
28965
|
+
daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").option("--machine-key <key>", "machine key for first-time pairing").option("--id <machineId>", "restart a previously paired machine by id").option("--server-url <url>", "server HTTP URL (or ALOOK_SERVER_URL env)").option("--ws-url <url>", "server WebSocket URL (or ALOOK_SERVER_WS_URL env)").option("--base-dir <path>", "data directory for agent workspaces and pidfile (or ALOOK_DATA_DIR env)").option("--foreground", "run in the current process and tee daemon logs to the terminal").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28121
28966
|
const localOpts = this.opts();
|
|
28967
|
+
const machineKey = localOpts.machineKey;
|
|
28968
|
+
const id = localOpts.id;
|
|
28969
|
+
if (!machineKey && !id || machineKey && id) {
|
|
28970
|
+
throw new CliError("daemon start requires exactly one of --machine-key <key> or --id <machineId>");
|
|
28971
|
+
}
|
|
28972
|
+
if (id) {
|
|
28973
|
+
await daemonStartById({
|
|
28974
|
+
id,
|
|
28975
|
+
baseDir: localOpts.baseDir,
|
|
28976
|
+
foreground: localOpts.foreground === true
|
|
28977
|
+
});
|
|
28978
|
+
return;
|
|
28979
|
+
}
|
|
28122
28980
|
await daemonStart({
|
|
28123
|
-
machineKey
|
|
28981
|
+
machineKey,
|
|
28124
28982
|
serverUrl: localOpts.serverUrl,
|
|
28125
28983
|
wsUrl: localOpts.wsUrl,
|
|
28126
28984
|
baseDir: localOpts.baseDir,
|
|
@@ -28130,6 +28988,22 @@ function buildProgram() {
|
|
|
28130
28988
|
daemon.command("run", { hidden: true }).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async () => {
|
|
28131
28989
|
await daemonRunFromIpc();
|
|
28132
28990
|
});
|
|
28991
|
+
daemon.command("resume", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28992
|
+
const localOpts = this.opts();
|
|
28993
|
+
await daemonResume({
|
|
28994
|
+
id: localOpts.id,
|
|
28995
|
+
baseDir: localOpts.baseDir,
|
|
28996
|
+
requestId: localOpts.requestId
|
|
28997
|
+
});
|
|
28998
|
+
});
|
|
28999
|
+
daemon.command("replace", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
29000
|
+
const localOpts = this.opts();
|
|
29001
|
+
await daemonReplace({
|
|
29002
|
+
id: localOpts.id,
|
|
29003
|
+
baseDir: localOpts.baseDir,
|
|
29004
|
+
requestId: localOpts.requestId
|
|
29005
|
+
});
|
|
29006
|
+
});
|
|
28133
29007
|
daemon.command("stop").argument("<id>", "daemon id from `alook daemon list` (the ID column)").description("stop a daemon by its id (from `alook daemon list`)").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function(id) {
|
|
28134
29008
|
const localOpts = this.opts();
|
|
28135
29009
|
await daemonStop({
|
|
@@ -28137,9 +29011,13 @@ function buildProgram() {
|
|
|
28137
29011
|
baseDir: localOpts.baseDir
|
|
28138
29012
|
});
|
|
28139
29013
|
});
|
|
28140
|
-
daemon.command("list").description("list running daemons on this machine").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
|
|
29014
|
+
daemon.command("list").description("list running daemons on this machine").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").option("--json", "print a machine-readable JSON envelope").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
|
|
28141
29015
|
const localOpts = this.opts();
|
|
28142
29016
|
const daemons = daemonList({ baseDir: localOpts.baseDir });
|
|
29017
|
+
if (localOpts.json === true) {
|
|
29018
|
+
printEnvelope({ success: { daemons } });
|
|
29019
|
+
return;
|
|
29020
|
+
}
|
|
28143
29021
|
process.stdout.write(renderDaemonList(daemons) + `
|
|
28144
29022
|
`);
|
|
28145
29023
|
});
|
|
@@ -28159,6 +29037,7 @@ function buildProgram() {
|
|
|
28159
29037
|
}
|
|
28160
29038
|
async function main(argv = process.argv.slice(2)) {
|
|
28161
29039
|
const program = buildProgram();
|
|
29040
|
+
let internalExitCode = 0;
|
|
28162
29041
|
try {
|
|
28163
29042
|
await program.parseAsync(argv, { from: "user" });
|
|
28164
29043
|
} catch (err) {
|
|
@@ -28180,8 +29059,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
28180
29059
|
hint: err.hint
|
|
28181
29060
|
});
|
|
28182
29061
|
}
|
|
29062
|
+
if (argv[0] === "daemon" && (argv[1] === "resume" || argv[1] === "replace")) {
|
|
29063
|
+
internalExitCode = 1;
|
|
29064
|
+
}
|
|
28183
29065
|
}
|
|
28184
|
-
return
|
|
29066
|
+
return internalExitCode;
|
|
28185
29067
|
}
|
|
28186
29068
|
function getHelpText(program, argv) {
|
|
28187
29069
|
const args = argv.filter((a) => a !== "-h" && a !== "--help");
|