@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.
Files changed (3) hide show
  1. package/dist/cli/index.js +1035 -153
  2. package/dist/index.js +893 -549
  3. 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 randomUUID5 } from "node:crypto";
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 fs11 from "fs";
18989
- import * as path12 from "path";
18990
- import * as crypto4 from "crypto";
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
- try {
20258
- const pkg = requireFromHere("../package.json");
20259
- return pkg.version ?? "";
20260
- } catch {
20261
- return "";
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 fs10 from "node:fs";
21366
- import * as path11 from "node:path";
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
- fs10.mkdirSync(daemonDir, { recursive: true, mode: 448 });
26584
- fs10.chmodSync(daemonDir, 448);
26585
- const logPath = path11.join(daemonDir, "daemon.log");
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
- fs10.writeFileSync(marker, String(process.pid), { mode: 384 });
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 spawn2 } from "node:child_process";
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 || path12.join(homedir4(), ".alook");
26807
- return path12.join(root, "daemon");
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 path12.join(baseDir, "daemons");
27425
+ return path13.join(baseDir, "daemons");
26813
27426
  }
26814
27427
  function daemonDirById(baseDir, id) {
26815
- return path12.join(daemonsDir(baseDir), validateDaemonId(id));
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 path12.join(daemonDirById(baseDir, id), "daemon.pid");
27444
+ return path13.join(daemonDirById(baseDir, id), "daemon.pid");
26832
27445
  }
26833
27446
  function statusFilePathById(baseDir, id) {
26834
- return path12.join(daemonDirById(baseDir, id), "status.json");
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 (!fs11.existsSync(filePath))
27485
+ if (!fs12.existsSync(filePath))
26873
27486
  return null;
26874
27487
  try {
26875
- return parsePidFileContent(fs11.readFileSync(filePath, "utf8"));
27488
+ return parsePidFileContent(fs12.readFileSync(filePath, "utf8"));
26876
27489
  } catch {}
26877
27490
  return null;
26878
27491
  }
26879
- function ensurePrivateDir(dir) {
26880
- fs11.mkdirSync(dir, { recursive: true, mode: 448 });
26881
- fs11.chmodSync(dir, 448);
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 = fs11.openSync(dir, "r");
27499
+ const fd = fs12.openSync(dir, "r");
26887
27500
  try {
26888
- fs11.fsyncSync(fd);
27501
+ fs12.fsyncSync(fd);
26889
27502
  } finally {
26890
- fs11.closeSync(fd);
27503
+ fs12.closeSync(fd);
26891
27504
  }
26892
27505
  }
26893
27506
  function writeExclusive(filePath, value) {
26894
- const dir = path12.dirname(filePath);
26895
- ensurePrivateDir(dir);
26896
- const tempPath = path12.join(dir, `.${path12.basename(filePath)}.${process.pid}.${crypto4.randomBytes(12).toString("hex")}.tmp`);
26897
- let fd = fs11.openSync(tempPath, "wx", 384);
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
- fs11.writeFileSync(fd, JSON.stringify(value));
26900
- fs11.fsyncSync(fd);
26901
- fs11.closeSync(fd);
27512
+ fs12.writeFileSync(fd, JSON.stringify(value));
27513
+ fs12.fsyncSync(fd);
27514
+ fs12.closeSync(fd);
26902
27515
  fd = null;
26903
- fs11.chmodSync(tempPath, 384);
26904
- fs11.linkSync(tempPath, filePath);
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
- fs11.closeSync(fd);
27522
+ fs12.closeSync(fd);
26910
27523
  } catch {}
26911
27524
  }
26912
27525
  try {
26913
- fs11.unlinkSync(tempPath);
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 (!fs11.existsSync(filePath))
27555
+ if (!fs12.existsSync(filePath))
26919
27556
  return;
26920
- const stat = fs11.lstatSync(filePath);
27557
+ const stat = fs12.lstatSync(filePath);
26921
27558
  if (!stat.isFile())
26922
27559
  throw new Error("unsafe daemon ownership file type");
26923
- fs11.chmodSync(filePath, 384);
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
- fs11.unlinkSync(filePath);
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
- fs11.unlinkSync(filePath);
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 (!fs11.existsSync(filePath))
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 = fs11.statSync(filePath);
26965
- raw = fs11.readFileSync(filePath, "utf8");
26966
- const afterRead = fs11.statSync(filePath);
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 = fs11.statSync(filePath);
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
- fs11.unlinkSync(filePath);
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 path12.join(daemonsDir(baseDir), ".start.lock");
27703
+ return path13.join(daemonsDir(baseDir), ".start.lock");
26984
27704
  }
26985
27705
  function acquireCoarseLock(baseDir, ownerToken) {
26986
27706
  const lf = coarseStartLockPath(baseDir);
26987
- ensurePrivateDir(path12.dirname(lf));
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
- ensurePrivateDir(daemonDir);
26996
- const lockPath = path12.join(daemonDir, "daemon.launch.lock");
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 (!fs11.existsSync(dir))
27733
+ if (!fs12.existsSync(dir))
27013
27734
  return [];
27014
27735
  const candidates = [];
27015
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
27736
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
27016
27737
  if (entry.isDirectory()) {
27017
- candidates.push(path12.join(dir, entry.name, "daemon.pid"));
27738
+ candidates.push(path13.join(dir, entry.name, "daemon.pid"));
27018
27739
  } else if (entry.isFile() && entry.name.endsWith(".pid")) {
27019
- candidates.push(path12.join(dir, entry.name));
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 (!fs11.existsSync(dir))
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 fs11.readdirSync(dir, { withFileTypes: true })) {
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, path12.join(dir, id, "daemon.pid"), path12.join(dir, id, "status.json"));
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 (!fs11.existsSync(statusPath))
27799
+ if (!fs12.existsSync(statusPath))
27079
27800
  return MISSING_STATUS;
27080
27801
  try {
27081
- const snap = JSON.parse(fs11.readFileSync(statusPath, "utf8"));
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 (!fs11.existsSync(dir))
27817
+ if (!fs12.existsSync(dir))
27097
27818
  return [];
27098
27819
  const ids = [];
27099
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
27100
- if (entry.isDirectory() && isDaemonId(entry.name) && fs11.existsSync(path12.join(dir, entry.name, "status.json"))) {
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
- try {
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(path12.join(daemonDirById(baseDir, opts.id), "daemon.pid"), `no daemon with id '${opts.id}' (pidfile not found — check \`alook daemon list\`)`);
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 path12.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
27887
+ return path13.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
27160
27888
  }
27161
27889
  function readCredentialFile(filePath) {
27162
- if (!fs11.existsSync(filePath))
27890
+ if (!fs12.existsSync(filePath))
27163
27891
  return null;
27164
- const stat = fs11.lstatSync(filePath);
27892
+ const stat = fs12.lstatSync(filePath);
27165
27893
  if (!stat.isFile())
27166
27894
  throw new Error("unsafe daemon credential file type");
27167
- fs11.chmodSync(filePath, 384);
27895
+ fs12.chmodSync(filePath, 384);
27168
27896
  try {
27169
- const content = JSON.parse(fs11.readFileSync(filePath, "utf8"));
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, credential, machineId) {
27177
- const dir = path12.dirname(filePath);
27178
- ensurePrivateDir(dir);
27179
- const tempPath = path12.join(dir, `.${path12.basename(filePath)}.${process.pid}.${crypto4.randomBytes(12).toString("hex")}.tmp`);
27180
- let fd = fs11.openSync(tempPath, "wx", 384);
27181
- try {
27182
- fs11.writeFileSync(fd, JSON.stringify({ credential, machineId }));
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 (!fs11.existsSync(dir))
27929
+ if (!fs12.existsSync(dir))
27203
27930
  return null;
27204
- for (const file2 of fs11.readdirSync(dir)) {
27931
+ for (const file2 of fs12.readdirSync(dir)) {
27205
27932
  if (!file2.endsWith(".credential.json"))
27206
27933
  continue;
27207
- const parsed = readCredentialFile(path12.join(dir, file2));
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 ownerToken = crypto4.randomBytes(24).toString("base64url");
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(), readDaemonVersion(), runtimeReport);
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
- writeCredentialFile(credentialFilePathByMachineId(baseDir, machineId), machineKey, machineId);
27275
- launchLockPath ??= acquireLaunchLock(baseDir, machineId, ownerToken);
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: readDaemonVersion(),
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 spawn2(process.execPath, runnerArguments(), {
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
- fs11.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
28087
+ fs12.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
27326
28088
  const view = new Int32Array(new SharedArrayBuffer(4));
27327
- while (!fs11.existsSync(`${checkpointFile}.continue`))
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 ${path12.join(prepared.daemonDir, "daemon.log")}`)), receiptTimeoutMs);
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 ${path12.join(prepared.daemonDir, "daemon.log")}`));
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 fs12 = await import("fs");
27648
- if (!fs12.existsSync(fileFlag))
28475
+ const fs13 = await import("fs");
28476
+ if (!fs13.existsSync(fileFlag))
27649
28477
  throw new CliError(`message send: file not found: ${fileFlag}`);
27650
- text2 = fs12.readFileSync(fileFlag, "utf8").trim();
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 = randomUUID5();
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 align, then resend.`);
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 fs12 = await import("fs");
27701
- if (!fs12.existsSync(fileFlag))
28545
+ const fs13 = await import("fs");
28546
+ if (!fs13.existsSync(fileFlag))
27702
28547
  throw new CliError(`message post: file not found: ${fileFlag}`);
27703
- text2 = fs12.readFileSync(fileFlag, "utf8").trim();
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 = randomUUID5();
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 fs12 = await import("fs/promises");
28632
+ const fs13 = await import("fs/promises");
27788
28633
  let bytes;
27789
28634
  try {
27790
- bytes = await fs12.readFile(filePath);
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 fs12 = await import("fs/promises");
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 fs12.rename(destPath, renamed);
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 fs12 = await import("fs");
27986
- if (!fs12.existsSync(fileFlag))
28830
+ const fs13 = await import("fs");
28831
+ if (!fs13.existsSync(fileFlag))
27987
28832
  throw new CliError(`nap: handoff file not found: ${fileFlag}`);
27988
- handoff = fs12.readFileSync(fileFlag, "utf8").trim();
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)").requiredOption("--machine-key <key>", "machine key for server authentication").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() {
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: localOpts.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 0;
29066
+ return internalExitCode;
28185
29067
  }
28186
29068
  function getHelpText(program, argv) {
28187
29069
  const args = argv.filter((a) => a !== "-h" && a !== "--help");