@alook/daemon 0.1.5 → 0.1.7

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 +1197 -227
  2. package/dist/index.js +945 -587
  3. package/package.json +4 -3
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),
@@ -16841,6 +16855,7 @@ var MAX_MESSAGE_CONTENT_LENGTH = 4000;
16841
16855
  var MAX_EMOJI_BYTES = 32;
16842
16856
  var MAX_ATTACHMENTS_PER_MESSAGE = 10;
16843
16857
  var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
16858
+ var MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES = 50 * 1024;
16844
16859
  var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
16845
16860
  var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
16846
16861
  // ../shared/src/utils/slug.ts
@@ -17517,7 +17532,8 @@ var CommunityAgentAttachmentUploadResponseSchema = exports_external.object({
17517
17532
  id: exports_external.string(),
17518
17533
  filename: exports_external.string(),
17519
17534
  contentType: exports_external.string(),
17520
- size: exports_external.number()
17535
+ size: exports_external.number(),
17536
+ hasThumbnail: exports_external.boolean().optional()
17521
17537
  });
17522
17538
  var CommunityAgentAttachmentDownloadRequestSchema = exports_external.object({
17523
17539
  id: exports_external.string().min(1)
@@ -17830,6 +17846,7 @@ var communityAttachment = sqliteTable("community_attachment", {
17830
17846
  uploaderId: text("uploader_id").notNull(),
17831
17847
  targetId: text("target_id").notNull(),
17832
17848
  r2Key: text("r2_key").notNull(),
17849
+ thumbnailR2Key: text("thumbnail_r2_key"),
17833
17850
  filename: text("filename").notNull(),
17834
17851
  contentType: text("content_type"),
17835
17852
  size: integer2("size"),
@@ -18138,6 +18155,7 @@ var messageAttachmentSchema = exports_external.strictObject({
18138
18155
  id: string4,
18139
18156
  filename: string4,
18140
18157
  url: string4,
18158
+ thumbnailUrl: string4.optional(),
18141
18159
  contentType: string4.optional(),
18142
18160
  size: exports_external.number().optional(),
18143
18161
  width: exports_external.number().nullable().optional(),
@@ -18612,6 +18630,32 @@ function parseInviteToken(input) {
18612
18630
  return urlMatch[1];
18613
18631
  return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
18614
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
+ }
18615
18659
  // src/cli/proxyServerApi.ts
18616
18660
  function proxyServerApiFromEnv(prefix = "ALOOK", env = process.env) {
18617
18661
  const proxyUrl = env[`${prefix}_PROXY_URL`];
@@ -18658,6 +18702,15 @@ function createProxyServerApi(config2) {
18658
18702
  const blobType = req.file.contentType ?? "application/octet-stream";
18659
18703
  const bytes = req.file.data instanceof Uint8Array ? new Blob([new Uint8Array(req.file.data)], { type: blobType }) : req.file.data;
18660
18704
  form.append("file", bytes, req.file.filename);
18705
+ if (req.thumbnail) {
18706
+ const thumbnailType = req.thumbnail.contentType ?? "image/jpeg";
18707
+ const thumbnailBytes = req.thumbnail.data instanceof Uint8Array ? new Blob([new Uint8Array(req.thumbnail.data)], { type: thumbnailType }) : req.thumbnail.data;
18708
+ form.append("thumbnail", thumbnailBytes, req.thumbnail.filename);
18709
+ }
18710
+ if (req.width !== undefined)
18711
+ form.append("width", String(req.width));
18712
+ if (req.height !== undefined)
18713
+ form.append("height", String(req.height));
18661
18714
  const url2 = `${base}/api/community/channels/${REF_PLACEHOLDER_ID}/attachments?target=${encodeURIComponent(req.target)}`;
18662
18715
  const res = await fetchImpl(url2, {
18663
18716
  method: "POST",
@@ -18819,6 +18872,35 @@ function createProxyServerApi(config2) {
18819
18872
  });
18820
18873
  return parseJsonResponse(res, "inboxPull");
18821
18874
  }
18875
+ async function callMarkSet(req) {
18876
+ const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
18877
+ method: "PUT",
18878
+ headers: {
18879
+ "content-type": "application/json",
18880
+ authorization: `Bearer ${config2.voucher}`
18881
+ },
18882
+ body: JSON.stringify(req)
18883
+ });
18884
+ await parseJsonResponse(res, "markSet");
18885
+ }
18886
+ async function callMarkRemove(req) {
18887
+ const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
18888
+ method: "DELETE",
18889
+ headers: {
18890
+ "content-type": "application/json",
18891
+ authorization: `Bearer ${config2.voucher}`
18892
+ },
18893
+ body: JSON.stringify(req)
18894
+ });
18895
+ await parseJsonResponse(res, "markRemove");
18896
+ }
18897
+ async function callListMarks() {
18898
+ const res = await fetchImpl(`${base}/api/community/users/me/marks`, {
18899
+ method: "GET",
18900
+ headers: { authorization: `Bearer ${config2.voucher}` }
18901
+ });
18902
+ return parseJsonResponse(res, "markList");
18903
+ }
18822
18904
  async function callAck(req) {
18823
18905
  const { agentId: _omit, ...wire } = req ?? {};
18824
18906
  const res = await fetchImpl(`${base}/api/community/users/me/inbox/ack`, {
@@ -18933,6 +19015,9 @@ function createProxyServerApi(config2) {
18933
19015
  attachmentUpload: callUpload,
18934
19016
  attachmentDownload: callDownload,
18935
19017
  reactAdd: callReactAdd,
19018
+ markSet: callMarkSet,
19019
+ markRemove: callMarkRemove,
19020
+ listMarks: (_r) => callListMarks(),
18936
19021
  friendRequest: callFriendRequest,
18937
19022
  listFriends: (_r) => callListFriends(),
18938
19023
  nap: callNap
@@ -18940,9 +19025,9 @@ function createProxyServerApi(config2) {
18940
19025
  }
18941
19026
 
18942
19027
  // src/cli/daemonStart.ts
18943
- import * as fs11 from "fs";
18944
- import * as path12 from "path";
18945
- import * as crypto4 from "crypto";
19028
+ import * as fs12 from "fs";
19029
+ import * as path13 from "path";
19030
+ import * as crypto5 from "crypto";
18946
19031
  import * as os3 from "os";
18947
19032
  import { homedir as homedir4 } from "os";
18948
19033
 
@@ -18988,10 +19073,13 @@ function cliCommandsSection() {
18988
19073
  "### Messaging",
18989
19074
  "",
18990
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).`,
18991
- `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\`.`,
18992
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>\`.`,
18993
19078
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
18994
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\`).`,
19080
+ `6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
19081
+ `7. \`${CLI} message mark remove --target <full-message-ref>\` — clear a completed message mark.`,
19082
+ `8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
18995
19083
  "",
18996
19084
  "### Servers",
18997
19085
  "",
@@ -19012,7 +19100,7 @@ function cliCommandsSection() {
19012
19100
  "",
19013
19101
  "### Context Lifecycle",
19014
19102
  "",
19015
- `1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) — end your current session and ` + `start fresh, carrying a handoff to your reborn self. Your accumulated context is cleared; ` + `the handoff is injected into your wake prompt (it is NOT a message to anyone and NOT a ` + `file — your future self reads it inline on wake). \`--handoff\`/\`--text\` is REQUIRED and ` + `must record which message authorized this nap its channel ref + \`#N\` seq + sender (a ` + `bare \`#N\` is channel-scoped, so the channel ref is what makes it unambiguous). ` + `Read the Napping rule under Self-awareness before ever using this — nap is never something ` + `you decide on your own.`,
19103
+ `1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) — reset your current session and ` + `start fresh. The required handoff is injected into the new session so your future self can ` + `quickly pick up unfinished work. Never nap on your own; only do it when someone explicitly asks.`,
19016
19104
  "",
19017
19105
  "### Output format",
19018
19106
  "",
@@ -19035,7 +19123,7 @@ function messagingSection() {
19035
19123
  `- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
19036
19124
  `- Cite a specific message: \`${CLI} message send --target <ref> --reply "#37" --text "on it"\` — ` + "`--reply` takes the `#N` seq (within `--target`) of the message you're answering.",
19037
19125
  "",
19038
- "### Channel refs",
19126
+ "### Context refs",
19039
19127
  "",
19040
19128
  "Path-style refs:",
19041
19129
  "",
@@ -19067,15 +19155,14 @@ function messagingSection() {
19067
19155
  "",
19068
19156
  "### Message formatting",
19069
19157
  "",
19070
- "The app auto-renders inline tokens in a message body — channel refs, @mentions, and message " + "refs. Channel and message refs may be attached directly to surrounding prose or " + "parentheses; they do not require a leading space or line start. Keep @mentions as " + "standalone tokens. **Never wrap refs or mentions in backticks** — that kills the render.",
19158
+ "Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
19071
19159
  "",
19072
- "- **Channel refs** render as clickable links.",
19073
- "- **Mentions** — `@name#NNNN` (e.g. `@alice#0001`) notifies that person and highlights the " + "message for them. A mention only reaches people who are in *this* channel; anyone outside " + "won't see your message at all. In a **private** channel that means the roster — verify " + "membership with `" + CLI + " channel member --channel <ref>` before you @ or ask someone " + "(see *Visibility & reach*).",
19074
- "- **Message refs** — point at a message by its **full path**: `/<server>/<channel>#N` " + "(message seq N in that channel) or `/<server>/<channel>/#N#M` (message #M in the thread " + "rooted at #N); DMs use `/.dm/<peer>#N`. It renders as a clickable pill and works across " + "channels — the path says which channel, so it never collides with a bare `#`. A bare `#N` " + "on its own does NOT render as a ref; always write the full path. **Never drop a DM ref " + "(`/.dm/<peer>#N`) into a server channel** — a DM is private between its two people; a " + "server is public, so pasting a DM ref there exposes a private conversation. Keep DM refs " + "in DMs.",
19160
+ "- **Context refs** use `/<server>/<channel>` for a channel, `/<server>/<channel>#N` for " + "a message, and `/<server>/<channel>/#N#M` for a thread reply; DMs use `/.dm/<peer>` and " + "`/.dm/<peer>#N`. Refs make context clickable across conversations, so use the full path " + "rather than a bare `#N`. Never paste a private DM ref into a server channel.",
19161
+ "- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
19075
19162
  "",
19076
19163
  "```bash",
19077
- `${CLI} message send --target "/.dm/alice#0001" --text "Check the discussion in /demo#1234/support"`,
19078
- `${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Can you review this? See /demo#1234/general#42"`,
19164
+ "# Notify Alice and point her to a message for context.",
19165
+ `${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
19079
19166
  "```",
19080
19167
  "",
19081
19168
  "### Pulled messages",
@@ -19111,24 +19198,21 @@ function channelTypesSection() {
19111
19198
  ].join(`
19112
19199
  `);
19113
19200
  }
19114
- function visibilityAndReachSection() {
19201
+ function visibilityAndNotificationsSection() {
19115
19202
  return [
19116
- "## Visibility & reach",
19117
- "",
19118
- "Two different things decide who's involved in a message — don't conflate them. **Access** is " + "who *can see* the channel at all. **Reach** is who, among those, actually gets *notified*.",
19203
+ "## Visibility & notifications",
19119
19204
  "",
19120
- "### Access who can see the channel",
19205
+ "Membership and access are related but not identical. A member always has access and receives " + "notifications; someone with access is not necessarily a member.",
19121
19206
  "",
19122
- "- **Public channel** its audience is the whole server. Every server member can read it, and " + "any of them can be @mentioned.",
19123
- "- **Private channel** restricted to an explicit roster. Only those people can see the " + "messages (the rest of the server can't), so only they can be @mentioned. `channel list` " + "marks each channel `public` or `private`.",
19124
- "- **Thread** a side-room rooted at a message, for discussing that message without cluttering " + "the channel. Start one by sending to `/<server>/<channel>/#N` (the thread is created on " + "seq `#N`). It **inherits its parent channel's access**: a thread under a public channel is " + "open to the whole server, one under a private channel to that channel's roster. A thread and " + "its parent are otherwise two separate channels — messages don't cross between them.",
19207
+ "- For a regular channel, its members define both who can access it and who receives " + "notifications. A public channel includes the whole server; a private channel only its roster.",
19208
+ "- A thread is the exception: it inherits access from its parent channel, but only people " + "participating in the thread are members and receive its notifications.",
19209
+ "- To bring someone into a thread discussion, first check that they can access the parent " + "channel; if they can, @mention them inside the thread.",
19125
19210
  "",
19126
- "### Reach — who actually gets notified",
19127
- "",
19128
- "- In a channel, a message is visible to everyone with access; @mention someone to notify them " + "specifically. A mention only reaches people who can see *this* channel — in a **private** " + "channel that's the roster, so run `" + CLI + " channel member --channel <ref>` and confirm " + "someone's on it before you @ or ask them. Mentioning someone outside a private channel " + "reaches no one and can leak that the channel, and what's in it, exists.",
19129
- "- A **thread** notifies only its participants — whoever's been @mentioned in it, has posted in " + "it, or was added. Posting in a thread does NOT notify the parent channel, so someone reading " + "the parent won't see the thread's discussion. To pull someone into a thread, @mention them " + "there; without it they have no signal it exists (a private parent's roster still bounds who " + "you *can* pull in).",
19130
- "",
19131
- "When unsure whether a channel is private, treat it as private and check the roster first."
19211
+ "```bash",
19212
+ "# Check the channel's public/private type, then inspect who its members are.",
19213
+ `${CLI} channel list --server "demo#1234"`,
19214
+ `${CLI} channel member --channel "/demo#1234/team"`,
19215
+ "```"
19132
19216
  ].join(`
19133
19217
  `);
19134
19218
  }
@@ -19138,7 +19222,13 @@ function utilsSection() {
19138
19222
  "",
19139
19223
  "### Join a new server",
19140
19224
  "",
19141
- `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\`.`
19142
19232
  ].join(`
19143
19233
  `);
19144
19234
  }
@@ -19160,9 +19250,9 @@ function executionModelSection() {
19160
19250
  "",
19161
19251
  "Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
19162
19252
  "",
19163
- "On wake, restore state from `memory.md`, the context timeline, and `todo.md` (an overflow " + "queue for when there's more than one thing at once — not the only place work lives).",
19253
+ "On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
19164
19254
  "",
19165
- "`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull. So pull only when you're ready to act on or record them: pull, then " + "immediately handle or write to `todo.md`. Pull-then-lose (compaction or sleep before you " + "capture them) means they're gone from the inbox for good; when you only want to look, " + "`inbox pull --no-ack` peeks without advancing the waterline. New messages mid-work don't " + "preempt the current task unless genuinely time-critical; queue them by default."
19255
+ "`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull. A task-bearing message stays durable through its server mark even after the " + "read waterline advances."
19166
19256
  ].join(`
19167
19257
  `);
19168
19258
  }
@@ -19184,8 +19274,8 @@ function chaosAwarenessSection() {
19184
19274
  "",
19185
19275
  "Before you touch a message, make one call: does fulfilling it take work beyond a reply?",
19186
19276
  "",
19187
- "- **Yes — it's a task.** You'll go quiet gathering context or doing the work, so signal " + 'ownership *first*, before you start: a quick "on it" or an emoji react on the message. ' + "That tells the sender it landed and stops anyone else grabbing it. The gap opens the " + "instant you pick up the work; the ack closes it.",
19188
- `- **No — it's just an answer.** Answer it. No "on it", no "let me check" — the reply ` + "itself is the acknowledgment.",
19277
+ `- **Yes — it's a task.** Signal ownership publicly first: a quick "on it" or an emoji ` + "reaction. Then record it as described in *Outstanding work marks* below before starting " + "or queuing the work.",
19278
+ `- **No — it's just an answer.** Answer it without marking. No "on it", no "let me ` + 'check" — the reply itself is the acknowledgment.',
19189
19279
  "",
19190
19280
  'Ack once. The signal is "picked up," not a running commentary.'
19191
19281
  ].join(`
@@ -19199,13 +19289,11 @@ function workspaceMemorySection() {
19199
19289
  "",
19200
19290
  "**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
19201
19291
  "",
19202
- "### Napping",
19203
- "",
19204
- `\`${CLI} nap\` is a self-reset: it clears your current session and starts a fresh one, ` + "carrying a handoff — a note to your future self — injected into your wake prompt. Leave " + "yourself a note before you sleep.",
19292
+ "When context is missing, don't guess. Re-read `memory.md`, the context timeline, the workspace, " + "or relevant channel history. That check *is* your remembering.",
19205
19293
  "",
19206
- "**Hard rule: never nap on your own initiative.** Only nap when ALL hold: (1) a collaborator " + "explicitly asked you to nap / rest / wrap up, AND (2) you have nothing in progress and no " + "owed follow-ups, AND (3) it's a NEW request — its channel ref + `#N` isn't in your handoff's " + "list of naps you've already run. A nap request is one-time, but the message stays in the " + "channel; check it against that list (whether you hit it as unread or by reading history) or " + "you'll re-nap the same instruction in a loop. If any condition is missing, don't. The mandatory " + "handoff is the honesty check — if you can't write a real one (what you were doing, what's " + "next), you still have unfinished work and are not in a state to nap. Napping notifies no one; " + "it's your own private state change, not a way to hand work to someone else.",
19294
+ "### Napping",
19207
19295
  "",
19208
- "When you feel a gap don't remember someone, why something matters, what was agreed don't " + "guess. Re-read `memory.md`, the context timeline, grep the workspace. Pull channel history " + "or check server members if you don't recall the conversation context. That check *is* your " + "remembering.",
19296
+ `\`${CLI} nap\` resets your current session and starts a fresh one with your required handoff ` + "injected into it. Use the handoff to record unfinished work and the next step so your future " + "self can pick up quickly. Never nap on your own; only do it when someone explicitly asks.",
19209
19297
  "",
19210
19298
  "### memory.md",
19211
19299
  "",
@@ -19223,16 +19311,13 @@ function workspaceMemorySection() {
19223
19311
  "",
19224
19312
  "`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative history.",
19225
19313
  "",
19226
- "### todo.md",
19314
+ "### Outstanding work marks",
19227
19315
  "",
19228
- "You work one thing at a time and won't remember the rest — so when work is owed beyond " + "what you're on right now (unread piling up, a request mid-investigation, a promise not " + "kept), write it here before you start. Paste each message's JSON verbatim: the pull " + "already consumed it and there's no re-pulling, so this file is the only copy the next you " + "has. Ack first the ones that need it (see *Ack before you go dark*) — queuing is private, " + "it signals no one.",
19316
+ "Marks are the durable work queue. If a message requires work beyond an immediate reply, " + `acknowledge it, then run \`${CLI} message mark set --target <full-message-ref>\` before ` + "starting or queuing. Do not mark a message you answer immediately.",
19229
19317
  "",
19230
- "It holds only what's still owed: delete a line the instant it's done (never `[x]`), delete " + "the file when empty. Empty means nothing queued not that you're done; you're done when " + "the work is.",
19318
+ "Keep it marked while the work is active, queued, or blocked; report blockers where the task " + `came from. Run \`${CLI} message mark remove --target <full-message-ref>\` only after ` + "sending the result, or when the request is cancelled or superseded and nothing remains.",
19231
19319
  "",
19232
- "```md",
19233
- '- [ ] {"seq": "#42", "channel": "/demo#1234/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
19234
- '- [ ] {"seq": "#12", "channel": "/demo#1234/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
19235
- "```"
19320
+ "If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
19236
19321
  ].join(`
19237
19322
  `);
19238
19323
  }
@@ -19242,7 +19327,7 @@ function buildCliSystemPrompt(config2) {
19242
19327
  cliCommandsSection(),
19243
19328
  messagingSection(),
19244
19329
  channelTypesSection(),
19245
- visibilityAndReachSection(),
19330
+ visibilityAndNotificationsSection(),
19246
19331
  criticalRulesSection(),
19247
19332
  executionModelSection(),
19248
19333
  chaosAwarenessSection(),
@@ -20214,13 +20299,18 @@ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
20214
20299
  // src/version.ts
20215
20300
  import { createRequire as createRequire2 } from "module";
20216
20301
  var requireFromHere = createRequire2(import.meta.url);
20302
+ var PACKAGE_JSON_CANDIDATES = ["../package.json", "../../package.json"];
20217
20303
  function readDaemonVersion() {
20218
- try {
20219
- const pkg = requireFromHere("../package.json");
20220
- return pkg.version ?? "";
20221
- } catch {
20222
- 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
+ }
20223
20312
  }
20313
+ return "";
20224
20314
  }
20225
20315
  function getDaemonClientInfo() {
20226
20316
  return { name: "alook-daemon", version: readDaemonVersion() };
@@ -21323,8 +21413,8 @@ function createLogger2(options = {}) {
21323
21413
  }
21324
21414
 
21325
21415
  // src/cli/daemonRunner.ts
21326
- import * as fs10 from "node:fs";
21327
- import * as path11 from "node:path";
21416
+ import * as fs11 from "node:fs";
21417
+ import * as path12 from "node:path";
21328
21418
  import { WebSocket } from "ws";
21329
21419
 
21330
21420
  // src/daemon/createDaemon.ts
@@ -21823,6 +21913,10 @@ import * as https from "https";
21823
21913
  import * as os2 from "os";
21824
21914
  import * as path9 from "path";
21825
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;
21826
21920
  var DEFAULT_HEADER_NAMES = {
21827
21921
  agentId: "X-Agent-Id",
21828
21922
  client: "X-Client",
@@ -21925,6 +22019,10 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
21925
22019
  return method === "GET" ? "read" : "send";
21926
22020
  if (/\/messages\/[^/]+\/reactions\//.test(pathname))
21927
22021
  return "send";
22022
+ if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
22023
+ return "send";
22024
+ if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
22025
+ return "read";
21928
22026
  if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
21929
22027
  return "read";
21930
22028
  if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
@@ -21934,6 +22032,99 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
21934
22032
  return;
21935
22033
  };
21936
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
+ }
21937
22128
  async function startCredentialProxy(broker, options = {}) {
21938
22129
  const host = options.host ?? "127.0.0.1";
21939
22130
  const resolveCap = options.capabilityResolver ?? DEFAULT_CAPABILITY_RESOLVER;
@@ -21943,6 +22134,26 @@ async function startCredentialProxy(broker, options = {}) {
21943
22134
  const onProxyRequest = options.onProxyRequest;
21944
22135
  const server = http.createServer((req, res) => {
21945
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
+ }
21946
22157
  const requiredCap = resolveCap(req.method ?? "GET", pathname);
21947
22158
  const verdict = broker.check(req.headers["authorization"], requiredCap);
21948
22159
  if (!verdict.ok) {
@@ -21968,7 +22179,6 @@ async function startCredentialProxy(broker, options = {}) {
21968
22179
  const outHeaders = { ...req.headers };
21969
22180
  delete outHeaders["authorization"];
21970
22181
  delete outHeaders["host"];
21971
- delete outHeaders["content-length"];
21972
22182
  outHeaders["authorization"] = `Bearer ${reg.runnerKey}`;
21973
22183
  outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
21974
22184
  outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
@@ -23679,6 +23889,8 @@ ${this.opts.wakePromptFooter}` : text2;
23679
23889
  }
23680
23890
  }
23681
23891
  // src/manager/agentRouter.ts
23892
+ var DEFINITIVE_EXECUTABLE_FAILURES = new Set(["ENOENT", "EACCES", "ENOEXEC", "EPERM"]);
23893
+
23682
23894
  class UnknownBotError extends Error {
23683
23895
  botId;
23684
23896
  constructor(botId) {
@@ -23719,14 +23931,14 @@ class UnknownRuntimeError extends Error {
23719
23931
  function defaultFormatUnreadNoticeText(notice) {
23720
23932
  return `You have unread messages in channel ${notice.channel}.`;
23721
23933
  }
23722
- var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @todo.md, @memory.md, and your .context_timeline for anything unfinished, " + "then pull your inbox to catch up on unread messages before doing anything else.";
23934
+ var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @memory.md and your .context_timeline for durable context, then pull your inbox " + "before doing anything else. If it reports marked messages, run `$ALOOK_CLI message mark list` " + "and resume that outstanding work.";
23723
23935
  var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. Continue any unfinished work.";
23724
23936
  function buildNapRewakePrompt(handoff) {
23725
23937
  return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
23726
23938
 
23727
23939
  ` + handoff.trim() + `
23728
23940
 
23729
- Then read @todo.md, @memory.md, and your .context_timeline for anything ` + "unfinished, and pull your inbox to catch up on unread messages before doing " + "anything else.";
23941
+ Then read @memory.md and your .context_timeline for durable context, and pull ` + "your inbox before doing anything else. If it reports marked messages, run `$ALOOK_CLI " + "message mark list` and resume that outstanding work.";
23730
23942
  }
23731
23943
 
23732
23944
  class AgentRouter {
@@ -23781,6 +23993,11 @@ class AgentRouter {
23781
23993
  isRuntimeHealthy(id) {
23782
23994
  return this.runtimes.get(id)?.status === "healthy";
23783
23995
  }
23996
+ recordRuntimeSpawnFailure(id, reason) {
23997
+ if (!DEFINITIVE_EXECUTABLE_FAILURES.has(reason))
23998
+ return;
23999
+ this.markRuntimeUnhealthy(id, reason);
24000
+ }
23784
24001
  markRuntimeUnhealthy(id, reason) {
23785
24002
  const existing = this.runtimes.get(id);
23786
24003
  if (!existing)
@@ -24769,6 +24986,108 @@ function createDiagnosticsCommandListener(options) {
24769
24986
  };
24770
24987
  }
24771
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
+
24772
25091
  // src/daemon/createDaemon.ts
24773
25092
  var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
24774
25093
  var WARMUP_CEILING_MS = 30000;
@@ -24827,6 +25146,15 @@ function deriveAuditLogSubcommand(pathname, method) {
24827
25146
  const canonical = pathname.split("?")[0] ?? pathname;
24828
25147
  if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
24829
25148
  return "reactAdd";
25149
+ if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
25150
+ if (method === "PUT")
25151
+ return "markSet";
25152
+ if (method === "DELETE")
25153
+ return "markRemove";
25154
+ return null;
25155
+ }
25156
+ if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
25157
+ return "markList";
24830
25158
  if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
24831
25159
  return "resolve";
24832
25160
  if (/^\/api\/community\/channels\/[^/]+\/messages(\/|$)/.test(canonical)) {
@@ -24890,6 +25218,7 @@ async function createDaemon(opts) {
24890
25218
  });
24891
25219
  let channelRef = null;
24892
25220
  let managerRef = null;
25221
+ let reminderSchedulerRef = null;
24893
25222
  const emitBotAuditEvent = (agentId, event, context) => {
24894
25223
  channelRef?.reportBotAuditEvent?.({
24895
25224
  type: "bot_audit_event",
@@ -24903,6 +25232,7 @@ async function createDaemon(opts) {
24903
25232
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
24904
25233
  const proxy = await startCredentialProxy(broker, {
24905
25234
  onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
25235
+ onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
24906
25236
  onProxyRequest: (agentId, method, pathname) => {
24907
25237
  const subcommand = deriveAuditLogSubcommand(pathname, method);
24908
25238
  if (!subcommand)
@@ -25126,7 +25456,7 @@ async function createDaemon(opts) {
25126
25456
  return opts.driverFor(agentId, runtimeConfig);
25127
25457
  },
25128
25458
  onRuntimeSpawnFailed: (runtimeId, reason) => {
25129
- router?.markRuntimeUnhealthy(runtimeId, reason);
25459
+ router?.recordRuntimeSpawnFailure(runtimeId, reason);
25130
25460
  },
25131
25461
  onRuntimeSessionEstablished: (runtimeId) => {
25132
25462
  router?.markRuntimeHealthy(runtimeId);
@@ -25150,6 +25480,7 @@ async function createDaemon(opts) {
25150
25480
  }
25151
25481
  };
25152
25482
  },
25483
+ ...opts.handshakeTimeoutMs !== undefined ? { handshakeTimeoutMs: opts.handshakeTimeoutMs } : {},
25153
25484
  tickIntervalMs: opts.tickIntervalMs ?? 2000,
25154
25485
  onAgentSession: (info) => void channel2.reportAgentSession(info),
25155
25486
  onAgentActivity: (info) => {
@@ -25174,6 +25505,10 @@ async function createDaemon(opts) {
25174
25505
  });
25175
25506
  managerRef = manager;
25176
25507
  manager.start();
25508
+ reminderSchedulerRef = new MessageReminderScheduler({
25509
+ deliver: (agentId, message2) => manager.deliver(agentId, message2),
25510
+ ...opts.messageReminderClock
25511
+ });
25177
25512
  let statusTimer = null;
25178
25513
  if (opts.statusFilePath) {
25179
25514
  const statusPath = opts.statusFilePath;
@@ -25219,10 +25554,26 @@ async function createDaemon(opts) {
25219
25554
  },
25220
25555
  formatUnreadNoticeText: (notice) => `You have unread messages in channel ${notice.channel}.`
25221
25556
  });
25557
+ channel2.onCommand(createSelfUpdateCommandListener(opts.handleSelfUpdate));
25222
25558
  channel2.onCommand(createDiagnosticsCommandListener({
25223
25559
  handleDiagnosticCommand: opts.handleDiagnosticCommand,
25224
25560
  reportDiagnosticFailure: opts.reportDiagnosticFailure
25225
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
+ });
25226
25577
  channel2.onCommand((cmd) => {
25227
25578
  handleBotFrame(cmd);
25228
25579
  });
@@ -25241,6 +25592,7 @@ async function createDaemon(opts) {
25241
25592
  },
25242
25593
  proxyUrl: proxy.url,
25243
25594
  stop: async () => {
25595
+ reminderSchedulerRef?.clearAll();
25244
25596
  for (const agentId of [...typingHeartbeats.keys()]) {
25245
25597
  emitTypingStopsAndClear(agentId);
25246
25598
  }
@@ -26417,6 +26769,318 @@ function createDiagnosticHttpTransport(args) {
26417
26769
  }
26418
26770
  };
26419
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
+
26420
27084
  // src/cli/daemonRunner.ts
26421
27085
  var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge", "attach", "friend"];
26422
27086
  var DAEMON_LOG_MAX_BYTES = 8 * 1024 * 1024;
@@ -26521,9 +27185,9 @@ async function buildRunnerDiagnosticBundle(args) {
26521
27185
  });
26522
27186
  }
26523
27187
  function createDaemonProcessLogger(daemonDir, foreground) {
26524
- fs10.mkdirSync(daemonDir, { recursive: true, mode: 448 });
26525
- fs10.chmodSync(daemonDir, 448);
26526
- 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");
26527
27191
  let warnedOversize = false;
26528
27192
  const sink = createRotatingFileSink(logPath, DAEMON_LOG_MAX_BYTES, {
26529
27193
  mode: 384,
@@ -26597,7 +27261,7 @@ async function runPreparedDaemon(prepared, opts) {
26597
27261
  return;
26598
27262
  const marker = process.env.ALOOK_DAEMON_TEST_SHUTDOWN_MARKER;
26599
27263
  if (marker)
26600
- fs10.writeFileSync(marker, String(process.pid), { mode: 384 });
27264
+ fs11.writeFileSync(marker, String(process.pid), { mode: 384 });
26601
27265
  await new Promise((resolve4) => setTimeout(resolve4, delayMs));
26602
27266
  };
26603
27267
  const stopDaemon = async () => {
@@ -26645,6 +27309,13 @@ async function runPreparedDaemon(prepared, opts) {
26645
27309
  });
26646
27310
  try {
26647
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 });
26648
27319
  daemon = await createDaemon({
26649
27320
  machineKey: prepared.machineKey,
26650
27321
  serverUrl: prepared.serverUrl,
@@ -26670,6 +27341,7 @@ async function runPreparedDaemon(prepared, opts) {
26670
27341
  osRelease: prepared.osRelease,
26671
27342
  daemonVersion: prepared.daemonVersion,
26672
27343
  logger: log2,
27344
+ handleSelfUpdate,
26673
27345
  handleDiagnosticCommand,
26674
27346
  reportDiagnosticFailure,
26675
27347
  onDiagnosticSources: ({ fsmTraceSource, statusFilePath }) => {
@@ -26734,7 +27406,7 @@ async function runPreparedDaemon(prepared, opts) {
26734
27406
  }
26735
27407
 
26736
27408
  // src/cli/daemonStart.ts
26737
- import { spawn as spawn2 } from "node:child_process";
27409
+ import { spawn as spawn3 } from "node:child_process";
26738
27410
  var STOP_GRACE_MS = 5000;
26739
27411
  var STOP_KILL_GRACE_MS = 2000;
26740
27412
  var POLL_MS2 = 100;
@@ -26744,16 +27416,16 @@ var RUNNER_KILL_GRACE_MS = 2000;
26744
27416
  var MACHINE_ID_PATTERN = /^cm_[A-Za-z0-9_-]{8,64}$/;
26745
27417
  var LEGACY_DAEMON_ID_PATTERN = /^[a-f0-9]{12}$/;
26746
27418
  function resolveDefaultBaseDir() {
26747
- const root = process.env.ALOOK_PROJECT_ROOT || path12.join(homedir4(), ".alook");
26748
- return path12.join(root, "daemon");
27419
+ const root = process.env.ALOOK_PROJECT_ROOT || path13.join(homedir4(), ".alook");
27420
+ return path13.join(root, "daemon");
26749
27421
  }
26750
27422
  var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
26751
27423
  var log2 = createLogger2({ header: "@alook/daemon" });
26752
27424
  function daemonsDir(baseDir) {
26753
- return path12.join(baseDir, "daemons");
27425
+ return path13.join(baseDir, "daemons");
26754
27426
  }
26755
27427
  function daemonDirById(baseDir, id) {
26756
- return path12.join(daemonsDir(baseDir), validateDaemonId(id));
27428
+ return path13.join(daemonsDir(baseDir), validateDaemonId(id));
26757
27429
  }
26758
27430
  function validateMachineId(machineId) {
26759
27431
  if (!MACHINE_ID_PATTERN.test(machineId))
@@ -26769,10 +27441,10 @@ function validateDaemonId(id) {
26769
27441
  return id;
26770
27442
  }
26771
27443
  function pidfilePathById(baseDir, id) {
26772
- return path12.join(daemonDirById(baseDir, id), "daemon.pid");
27444
+ return path13.join(daemonDirById(baseDir, id), "daemon.pid");
26773
27445
  }
26774
27446
  function statusFilePathById(baseDir, id) {
26775
- return path12.join(daemonDirById(baseDir, id), "status.json");
27447
+ return path13.join(daemonDirById(baseDir, id), "status.json");
26776
27448
  }
26777
27449
  function isProcessAlive(pid) {
26778
27450
  try {
@@ -26810,64 +27482,88 @@ function parsePidFileContent(raw) {
26810
27482
  return null;
26811
27483
  }
26812
27484
  function readPidFile(filePath) {
26813
- if (!fs11.existsSync(filePath))
27485
+ if (!fs12.existsSync(filePath))
26814
27486
  return null;
26815
27487
  try {
26816
- return parsePidFileContent(fs11.readFileSync(filePath, "utf8"));
27488
+ return parsePidFileContent(fs12.readFileSync(filePath, "utf8"));
26817
27489
  } catch {}
26818
27490
  return null;
26819
27491
  }
26820
- function ensurePrivateDir(dir) {
26821
- fs11.mkdirSync(dir, { recursive: true, mode: 448 });
26822
- fs11.chmodSync(dir, 448);
27492
+ function ensurePrivateDir2(dir) {
27493
+ fs12.mkdirSync(dir, { recursive: true, mode: 448 });
27494
+ fs12.chmodSync(dir, 448);
26823
27495
  }
26824
27496
  function syncDirectory(dir) {
26825
27497
  if (process.platform === "win32")
26826
27498
  return;
26827
- const fd = fs11.openSync(dir, "r");
27499
+ const fd = fs12.openSync(dir, "r");
26828
27500
  try {
26829
- fs11.fsyncSync(fd);
27501
+ fs12.fsyncSync(fd);
26830
27502
  } finally {
26831
- fs11.closeSync(fd);
27503
+ fs12.closeSync(fd);
26832
27504
  }
26833
27505
  }
26834
27506
  function writeExclusive(filePath, value) {
26835
- const dir = path12.dirname(filePath);
26836
- ensurePrivateDir(dir);
26837
- const tempPath = path12.join(dir, `.${path12.basename(filePath)}.${process.pid}.${crypto4.randomBytes(12).toString("hex")}.tmp`);
26838
- 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);
27511
+ try {
27512
+ fs12.writeFileSync(fd, JSON.stringify(value));
27513
+ fs12.fsyncSync(fd);
27514
+ fs12.closeSync(fd);
27515
+ fd = null;
27516
+ fs12.chmodSync(tempPath, 384);
27517
+ fs12.linkSync(tempPath, filePath);
27518
+ syncDirectory(dir);
27519
+ } finally {
27520
+ if (fd !== null) {
27521
+ try {
27522
+ fs12.closeSync(fd);
27523
+ } catch {}
27524
+ }
27525
+ try {
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);
26839
27535
  try {
26840
- fs11.writeFileSync(fd, JSON.stringify(value));
26841
- fs11.fsyncSync(fd);
26842
- fs11.closeSync(fd);
27536
+ fs12.writeFileSync(fd, JSON.stringify(value));
27537
+ fs12.fsyncSync(fd);
27538
+ fs12.closeSync(fd);
26843
27539
  fd = null;
26844
- fs11.chmodSync(tempPath, 384);
26845
- fs11.linkSync(tempPath, filePath);
27540
+ fs12.chmodSync(tempPath, 384);
27541
+ fs12.renameSync(tempPath, filePath);
26846
27542
  syncDirectory(dir);
26847
27543
  } finally {
26848
27544
  if (fd !== null) {
26849
27545
  try {
26850
- fs11.closeSync(fd);
27546
+ fs12.closeSync(fd);
26851
27547
  } catch {}
26852
27548
  }
26853
27549
  try {
26854
- fs11.unlinkSync(tempPath);
27550
+ fs12.unlinkSync(tempPath);
26855
27551
  } catch {}
26856
27552
  }
26857
27553
  }
26858
27554
  function secureExistingFile(filePath) {
26859
- if (!fs11.existsSync(filePath))
27555
+ if (!fs12.existsSync(filePath))
26860
27556
  return;
26861
- const stat = fs11.lstatSync(filePath);
27557
+ const stat = fs12.lstatSync(filePath);
26862
27558
  if (!stat.isFile())
26863
27559
  throw new Error("unsafe daemon ownership file type");
26864
- fs11.chmodSync(filePath, 384);
27560
+ fs12.chmodSync(filePath, 384);
26865
27561
  }
26866
27562
  function removeOwnedFile(filePath, pid, ownerToken) {
26867
27563
  try {
26868
27564
  const content = readPidFile(filePath);
26869
27565
  if (content?.pid === pid && content.ownerToken === ownerToken) {
26870
- fs11.unlinkSync(filePath);
27566
+ fs12.unlinkSync(filePath);
26871
27567
  }
26872
27568
  } catch {}
26873
27569
  }
@@ -26879,7 +27575,7 @@ function removePidFileIfMatches(filePath, expected) {
26879
27575
  try {
26880
27576
  const current = readPidFile(filePath);
26881
27577
  if (current?.pid === expected.pid && current.key === expected.key)
26882
- fs11.unlinkSync(filePath);
27578
+ fs12.unlinkSync(filePath);
26883
27579
  } catch {}
26884
27580
  }
26885
27581
  function malformedPidHint(raw) {
@@ -26890,7 +27586,7 @@ function malformedPidHint(raw) {
26890
27586
  return Number.isInteger(pid) && pid > 0 ? pid : null;
26891
27587
  }
26892
27588
  function clearStaleOwnership(filePath, liveError) {
26893
- if (!fs11.existsSync(filePath))
27589
+ if (!fs12.existsSync(filePath))
26894
27590
  return;
26895
27591
  const valid = readPidFile(filePath);
26896
27592
  if (valid) {
@@ -26902,9 +27598,9 @@ function clearStaleOwnership(filePath, liveError) {
26902
27598
  let raw;
26903
27599
  let before;
26904
27600
  try {
26905
- before = fs11.statSync(filePath);
26906
- raw = fs11.readFileSync(filePath, "utf8");
26907
- const afterRead = fs11.statSync(filePath);
27601
+ before = fs12.statSync(filePath);
27602
+ raw = fs12.readFileSync(filePath, "utf8");
27603
+ const afterRead = fs12.statSync(filePath);
26908
27604
  if (afterRead.dev !== before.dev || afterRead.ino !== before.ino || afterRead.size !== before.size || afterRead.mtimeMs !== before.mtimeMs)
26909
27605
  return;
26910
27606
  } catch {
@@ -26914,28 +27610,112 @@ function clearStaleOwnership(filePath, liveError) {
26914
27610
  if (pid && isProcessAlive(pid))
26915
27611
  throw liveError(pid);
26916
27612
  try {
26917
- const current = fs11.statSync(filePath);
27613
+ const current = fs12.statSync(filePath);
26918
27614
  if (current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size || current.mtimeMs !== before.mtimeMs)
26919
27615
  return;
26920
- fs11.unlinkSync(filePath);
27616
+ fs12.unlinkSync(filePath);
26921
27617
  } catch {}
26922
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);
27648
+ } catch {}
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
+ }
26923
27702
  function coarseStartLockPath(baseDir) {
26924
- return path12.join(daemonsDir(baseDir), ".start.lock");
27703
+ return path13.join(daemonsDir(baseDir), ".start.lock");
26925
27704
  }
26926
27705
  function acquireCoarseLock(baseDir, ownerToken) {
26927
27706
  const lf = coarseStartLockPath(baseDir);
26928
- ensurePrivateDir(path12.dirname(lf));
27707
+ ensurePrivateDir2(path13.dirname(lf));
26929
27708
  secureExistingFile(lf);
26930
27709
  clearStaleOwnership(lf, (pid) => new Error(`another daemon start is in progress on this machine (pid ${pid})`));
26931
27710
  writeExclusive(lf, { pid: process.pid, machineId: "coarse", startedAt: new Date().toISOString(), ownerToken });
26932
27711
  return lf;
26933
27712
  }
26934
- function acquireLaunchLock(baseDir, machineId, ownerToken) {
27713
+ function acquireLaunchLock(baseDir, machineId, ownerToken, resumeRequestId) {
26935
27714
  const daemonDir = daemonDirById(baseDir, machineId);
26936
- ensurePrivateDir(daemonDir);
26937
- const lockPath = path12.join(daemonDir, "daemon.launch.lock");
27715
+ ensurePrivateDir2(daemonDir);
27716
+ const lockPath = path13.join(daemonDir, "daemon.launch.lock");
26938
27717
  const finalPath = pidfilePathById(baseDir, machineId);
27718
+ checkReplacementLock(baseDir, machineId, resumeRequestId);
26939
27719
  secureExistingFile(lockPath);
26940
27720
  secureExistingFile(finalPath);
26941
27721
  clearStaleOwnership(finalPath, (pid) => new Error(`daemon '${machineId}' already running (pid ${pid})`));
@@ -26950,14 +27730,14 @@ function commitFinalPidfile(baseDir, machineId, pid, startedAt, ownerToken) {
26950
27730
  }
26951
27731
  function legacyPidfileCandidates(baseDir) {
26952
27732
  const dir = daemonsDir(baseDir);
26953
- if (!fs11.existsSync(dir))
27733
+ if (!fs12.existsSync(dir))
26954
27734
  return [];
26955
27735
  const candidates = [];
26956
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
27736
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
26957
27737
  if (entry.isDirectory()) {
26958
- candidates.push(path12.join(dir, entry.name, "daemon.pid"));
27738
+ candidates.push(path13.join(dir, entry.name, "daemon.pid"));
26959
27739
  } else if (entry.isFile() && entry.name.endsWith(".pid")) {
26960
- candidates.push(path12.join(dir, entry.name));
27740
+ candidates.push(path13.join(dir, entry.name));
26961
27741
  }
26962
27742
  }
26963
27743
  return candidates;
@@ -26980,7 +27760,7 @@ function reconcileLegacyMachineKeyOwnership(baseDir, machineKey) {
26980
27760
  function daemonList(opts) {
26981
27761
  const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
26982
27762
  const dir = daemonsDir(baseDir);
26983
- if (!fs11.existsSync(dir))
27763
+ if (!fs12.existsSync(dir))
26984
27764
  return [];
26985
27765
  const results = [];
26986
27766
  const now = Date.now();
@@ -27005,10 +27785,10 @@ function daemonList(opts) {
27005
27785
  }
27006
27786
  results.push({ id, pid: data.pid, alive, agents, running, lastActiveMs });
27007
27787
  };
27008
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
27788
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
27009
27789
  if (entry.isDirectory() && isDaemonId(entry.name)) {
27010
27790
  const id = entry.name;
27011
- 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"));
27012
27792
  }
27013
27793
  }
27014
27794
  return results;
@@ -27016,10 +27796,10 @@ function daemonList(opts) {
27016
27796
  var STATUS_STALE_MS = 20000;
27017
27797
  var MISSING_STATUS = { found: false, ageMs: null, freshness: "missing", writtenAt: null, agents: [] };
27018
27798
  function daemonStatusFromFile(statusPath, nowMs) {
27019
- if (!fs11.existsSync(statusPath))
27799
+ if (!fs12.existsSync(statusPath))
27020
27800
  return MISSING_STATUS;
27021
27801
  try {
27022
- const snap = JSON.parse(fs11.readFileSync(statusPath, "utf8"));
27802
+ const snap = JSON.parse(fs12.readFileSync(statusPath, "utf8"));
27023
27803
  const ageMs = nowMs - snap.writtenAt;
27024
27804
  return {
27025
27805
  found: true,
@@ -27034,11 +27814,11 @@ function daemonStatusFromFile(statusPath, nowMs) {
27034
27814
  }
27035
27815
  function daemonIdsWithStatus(baseDir) {
27036
27816
  const dir = daemonsDir(baseDir);
27037
- if (!fs11.existsSync(dir))
27817
+ if (!fs12.existsSync(dir))
27038
27818
  return [];
27039
27819
  const ids = [];
27040
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
27041
- 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"))) {
27042
27822
  ids.push(entry.name);
27043
27823
  }
27044
27824
  }
@@ -27059,6 +27839,30 @@ function daemonStatus(opts) {
27059
27839
  }
27060
27840
  return MISSING_STATUS;
27061
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
+ }
27062
27866
  async function stopByPidfile(pf, notFoundHint) {
27063
27867
  const data = readPidFile(pf);
27064
27868
  if (!data) {
@@ -27071,81 +27875,63 @@ async function stopByPidfile(pf, notFoundHint) {
27071
27875
  return;
27072
27876
  }
27073
27877
  log2.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
27074
- try {
27075
- process.kill(data.pid, "SIGTERM");
27076
- } catch (error51) {
27077
- if (isProcessAlive(data.pid))
27078
- throw error51;
27079
- }
27080
- if (!await waitForPidExit(data.pid, STOP_GRACE_MS)) {
27081
- log2.error(`daemon (pid ${data.pid}) did not exit in ${STOP_GRACE_MS / 1000}s — sending SIGKILL`);
27082
- try {
27083
- process.kill(data.pid, "SIGKILL");
27084
- } catch (error51) {
27085
- if (isProcessAlive(data.pid))
27086
- throw error51;
27087
- }
27088
- if (!await waitForPidExit(data.pid, STOP_KILL_GRACE_MS)) {
27089
- throw new Error(`daemon (pid ${data.pid}) is still running after SIGKILL`);
27090
- }
27091
- }
27878
+ await stopExactPid(data.pid);
27092
27879
  log2.info("daemon stopped");
27093
27880
  removePidFileIfMatches(pf, data);
27094
27881
  }
27095
27882
  async function daemonStop(opts) {
27096
27883
  const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
27097
- 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\`)`);
27098
27885
  }
27099
27886
  function credentialFilePathByMachineId(baseDir, machineId) {
27100
- return path12.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
27887
+ return path13.join(daemonsDir(baseDir), `${validateMachineId(machineId)}.credential.json`);
27101
27888
  }
27102
27889
  function readCredentialFile(filePath) {
27103
- if (!fs11.existsSync(filePath))
27890
+ if (!fs12.existsSync(filePath))
27104
27891
  return null;
27105
- const stat = fs11.lstatSync(filePath);
27892
+ const stat = fs12.lstatSync(filePath);
27106
27893
  if (!stat.isFile())
27107
27894
  throw new Error("unsafe daemon credential file type");
27108
- fs11.chmodSync(filePath, 384);
27895
+ fs12.chmodSync(filePath, 384);
27109
27896
  try {
27110
- const content = JSON.parse(fs11.readFileSync(filePath, "utf8"));
27897
+ const content = JSON.parse(fs12.readFileSync(filePath, "utf8"));
27111
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
+ }
27112
27909
  return { credential: content.credential, machineId: content.machineId };
27113
27910
  }
27114
27911
  } catch {}
27115
27912
  return null;
27116
27913
  }
27117
- function writeCredentialFile(filePath, credential, machineId) {
27118
- const dir = path12.dirname(filePath);
27119
- ensurePrivateDir(dir);
27120
- const tempPath = path12.join(dir, `.${path12.basename(filePath)}.${process.pid}.${crypto4.randomBytes(12).toString("hex")}.tmp`);
27121
- let fd = fs11.openSync(tempPath, "wx", 384);
27122
- try {
27123
- fs11.writeFileSync(fd, JSON.stringify({ credential, machineId }));
27124
- fs11.fsyncSync(fd);
27125
- fs11.closeSync(fd);
27126
- fd = null;
27127
- fs11.chmodSync(tempPath, 384);
27128
- fs11.renameSync(tempPath, filePath);
27129
- syncDirectory(dir);
27130
- } finally {
27131
- if (fd !== null) {
27132
- try {
27133
- fs11.closeSync(fd);
27134
- } catch {}
27135
- }
27136
- try {
27137
- fs11.unlinkSync(tempPath);
27138
- } 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");
27139
27921
  }
27922
+ validateMachineId(record2.machineId);
27923
+ if (record2.machineId !== machineId)
27924
+ throw new Error("daemon launch record machine mismatch");
27925
+ return record2;
27140
27926
  }
27141
27927
  function findExistingCredentialForBearer(baseDir, bearer) {
27142
27928
  const dir = daemonsDir(baseDir);
27143
- if (!fs11.existsSync(dir))
27929
+ if (!fs12.existsSync(dir))
27144
27930
  return null;
27145
- for (const file2 of fs11.readdirSync(dir)) {
27931
+ for (const file2 of fs12.readdirSync(dir)) {
27146
27932
  if (!file2.endsWith(".credential.json"))
27147
27933
  continue;
27148
- const parsed = readCredentialFile(path12.join(dir, file2));
27934
+ const parsed = readCredentialFile(path13.join(dir, file2));
27149
27935
  if (parsed && parsed.credential === bearer)
27150
27936
  return parsed;
27151
27937
  }
@@ -27187,7 +27973,10 @@ async function prepareDaemonStart(opts) {
27187
27973
  throw new Error("invalid machine key format — expected `cmt_` or `cmk_`");
27188
27974
  }
27189
27975
  const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
27190
- 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");
27191
27980
  const persisted = opts.machineKey.startsWith("cmk_") ? findExistingCredentialForBearer(baseDir, opts.machineKey) : null;
27192
27981
  let coarseLockPath = null;
27193
27982
  let launchLockPath = null;
@@ -27195,7 +27984,7 @@ async function prepareDaemonStart(opts) {
27195
27984
  let machineKey = persisted?.credential;
27196
27985
  let machineId = persisted ? validateMachineId(persisted.machineId) : undefined;
27197
27986
  if (machineKey && machineId) {
27198
- launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken);
27987
+ launchLockPath = acquireLaunchLock(baseDir, machineId, ownerToken, opts.resumeRequestId);
27199
27988
  } else {
27200
27989
  coarseLockPath = acquireCoarseLock(baseDir, ownerToken);
27201
27990
  if (opts.machineKey.startsWith("cmk_")) {
@@ -27205,15 +27994,22 @@ async function prepareDaemonStart(opts) {
27205
27994
  const runtimeReport = await detectRuntimes();
27206
27995
  const healthyRuntimeIds = runtimeReport.filter((runtime) => runtime.status === "healthy").map((runtime) => runtime.id);
27207
27996
  if (opts.machineKey.startsWith("cmt_")) {
27208
- 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);
27209
27998
  machineKey = activated.credential;
27210
27999
  machineId = validateMachineId(activated.machineId);
27211
28000
  } else {
27212
28001
  machineKey ??= opts.machineKey;
27213
28002
  machineId ??= validateMachineId(await resolveMachineIdentity(serverUrl, opts.machineKey));
27214
28003
  }
27215
- writeCredentialFile(credentialFilePathByMachineId(baseDir, machineId), machineKey, machineId);
27216
- 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
+ });
27217
28013
  if (coarseLockPath)
27218
28014
  removeOwnedFile(coarseLockPath, process.pid, ownerToken);
27219
28015
  const startedAt = new Date().toISOString();
@@ -27234,7 +28030,7 @@ async function prepareDaemonStart(opts) {
27234
28030
  platform: process.platform,
27235
28031
  arch: process.arch,
27236
28032
  osRelease: os3.release(),
27237
- daemonVersion: readDaemonVersion(),
28033
+ daemonVersion,
27238
28034
  ownerToken,
27239
28035
  startedAt
27240
28036
  }
@@ -27247,12 +28043,26 @@ async function prepareDaemonStart(opts) {
27247
28043
  throw error51;
27248
28044
  }
27249
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
+ }
27250
28060
  function runnerArguments() {
27251
28061
  const command = process.env.ALOOK_DAEMON_PACKAGE_WRAPPER === "1" ? ["run"] : ["daemon", "run"];
27252
28062
  return [...process.execArgv, process.argv[1], ...command];
27253
28063
  }
27254
28064
  function spawnBlockedRunner() {
27255
- return spawn2(process.execPath, runnerArguments(), {
28065
+ return spawn3(process.execPath, runnerArguments(), {
27256
28066
  detached: true,
27257
28067
  stdio: ["ignore", "ignore", "ignore", "ipc"]
27258
28068
  });
@@ -27263,9 +28073,9 @@ function testCheckpoint(name, childPid) {
27263
28073
  const checkpointFile = process.env.ALOOK_DAEMON_TEST_CHECKPOINT_FILE;
27264
28074
  if (!checkpointFile)
27265
28075
  return;
27266
- fs11.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
28076
+ fs12.writeFileSync(checkpointFile, JSON.stringify({ name, parentPid: process.pid, childPid }), { mode: 384 });
27267
28077
  const view = new Int32Array(new SharedArrayBuffer(4));
27268
- while (!fs11.existsSync(`${checkpointFile}.continue`))
28078
+ while (!fs12.existsSync(`${checkpointFile}.continue`))
27269
28079
  Atomics.wait(view, 0, 0, 25);
27270
28080
  }
27271
28081
  function sendPrepared(child, prepared) {
@@ -27291,7 +28101,7 @@ function waitForReceipt(child, prepared) {
27291
28101
  };
27292
28102
  const configuredTestTimeout = NaN;
27293
28103
  const receiptTimeoutMs = Number.isFinite(configuredTestTimeout) && configuredTestTimeout > 0 ? configuredTestTimeout : START_RECEIPT_TIMEOUT_MS;
27294
- const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${path12.join(prepared.daemonDir, "daemon.log")}`)), receiptTimeoutMs);
28104
+ const timeout = setTimeout(() => settle(new Error(`daemon start timed out; inspect ${path13.join(prepared.daemonDir, "daemon.log")}`)), receiptTimeoutMs);
27295
28105
  child.on("message", (message2) => {
27296
28106
  const accepted = message2;
27297
28107
  if (accepted.type === "daemon:accepted") {
@@ -27310,7 +28120,7 @@ function waitForReceipt(child, prepared) {
27310
28120
  settle(undefined, receipt);
27311
28121
  });
27312
28122
  child.once("exit", (code, signal) => {
27313
- settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${path12.join(prepared.daemonDir, "daemon.log")}`));
28123
+ settle(new Error(`daemon child exited before ready (${signal ?? code}); inspect ${path13.join(prepared.daemonDir, "daemon.log")}`));
27314
28124
  });
27315
28125
  child.once("error", (error51) => settle(error51));
27316
28126
  });
@@ -27443,6 +28253,70 @@ function sendIpcBestEffort(ipc, message2, disconnectAfter = false) {
27443
28253
  }
27444
28254
  }
27445
28255
 
28256
+ // src/cli/messageReminderClient.ts
28257
+ import { readFileSync as readFileSync9 } from "node:fs";
28258
+ var LOCAL_MESSAGE_REMINDER_PATH2 = "/__alook/local/message-reminder";
28259
+ var MIN_REMINDER_MS = 60000;
28260
+ var MAX_REMINDER_MS = 24 * 60 * 60000;
28261
+ function parseRemindAfter(value) {
28262
+ const match = /^(\d+)(m|h)$/.exec(value);
28263
+ if (!match) {
28264
+ throw new Error("message send: --remind-after must be a positive integer followed by m or h (1m..24h)");
28265
+ }
28266
+ const amount = Number(match[1]);
28267
+ const milliseconds = amount * (match[2] === "h" ? 60 * 60000 : 60000);
28268
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < MIN_REMINDER_MS || milliseconds > MAX_REMINDER_MS) {
28269
+ throw new Error("message send: --remind-after must be between 1m and 24h");
28270
+ }
28271
+ return milliseconds;
28272
+ }
28273
+ async function armMessageReminderFromEnv(input, env = process.env, fetchImpl = fetch) {
28274
+ const proxyUrl = env.ALOOK_PROXY_URL;
28275
+ const tokenFile = env.ALOOK_PROXY_TOKEN_FILE;
28276
+ if (!proxyUrl || !tokenFile) {
28277
+ return { armed: false, reason: "local reminder proxy unavailable" };
28278
+ }
28279
+ let voucher;
28280
+ try {
28281
+ voucher = readFileSync9(tokenFile, "utf8").trim();
28282
+ } catch {
28283
+ return { armed: false, reason: "local reminder voucher unavailable" };
28284
+ }
28285
+ if (!voucher)
28286
+ return { armed: false, reason: "local reminder voucher unavailable" };
28287
+ let response;
28288
+ try {
28289
+ response = await fetchImpl(`${proxyUrl.replace(/\/+$/, "")}${LOCAL_MESSAGE_REMINDER_PATH2}`, {
28290
+ method: "PUT",
28291
+ headers: {
28292
+ authorization: `Bearer ${voucher}`,
28293
+ "content-type": "application/json"
28294
+ },
28295
+ body: JSON.stringify(input),
28296
+ signal: AbortSignal.timeout(2000)
28297
+ });
28298
+ } catch {
28299
+ return { armed: false, reason: "local reminder request failed" };
28300
+ }
28301
+ let body;
28302
+ try {
28303
+ body = await response.json();
28304
+ } catch {
28305
+ return { armed: false, reason: "local reminder returned an invalid response" };
28306
+ }
28307
+ if (!response.ok) {
28308
+ const code = typeof body === "object" && body !== null && typeof body.code === "string" ? body.code : `http_${response.status}`;
28309
+ return { armed: false, reason: `local reminder rejected (${code})` };
28310
+ }
28311
+ if (typeof body === "object" && body !== null && body.armed === true && Number.isSafeInteger(body.dueAt)) {
28312
+ return { armed: true, dueAt: body.dueAt };
28313
+ }
28314
+ if (typeof body === "object" && body !== null && body.armed === false && typeof body.reason === "string") {
28315
+ return { armed: false, reason: body.reason };
28316
+ }
28317
+ return { armed: false, reason: "local reminder returned an invalid response" };
28318
+ }
28319
+
27446
28320
  // src/cli/index.ts
27447
28321
  function messagesInLocalTime(messages) {
27448
28322
  return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
@@ -27550,29 +28424,34 @@ function contentTypeFromFilename(filename) {
27550
28424
  return "application/octet-stream";
27551
28425
  }
27552
28426
  }
27553
- function isTransientSendError(err) {
28427
+ function isTransientMutationError(err) {
27554
28428
  const msg = err instanceof Error ? err.message : String(err);
27555
28429
  return /upstream returned 5\d\d/.test(msg) || msg.includes("upstream body read failed") || msg.includes("fetch failed") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up") || msg.includes("network");
27556
28430
  }
27557
- async function sendWithRetry(api2, req) {
28431
+ async function withTransientMutationRetry(mutation) {
27558
28432
  const MAX_ATTEMPTS = 4;
27559
28433
  const BASE_DELAY_MS = 150;
27560
28434
  const MAX_DELAY_MS = 2000;
27561
28435
  let lastErr;
27562
28436
  for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
27563
28437
  try {
27564
- return await api2.send(req);
28438
+ return await mutation();
27565
28439
  } catch (err) {
27566
28440
  lastErr = err;
27567
- if (!isTransientSendError(err) || attempt === MAX_ATTEMPTS - 1)
28441
+ if (!isTransientMutationError(err) || attempt === MAX_ATTEMPTS - 1)
27568
28442
  throw err;
27569
28443
  const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
27570
- await new Promise((r) => setTimeout(r, cap));
28444
+ await new Promise((resolve4) => setTimeout(resolve4, cap));
27571
28445
  }
27572
28446
  }
27573
28447
  throw lastErr;
27574
28448
  }
28449
+ async function sendWithRetry(api2, req) {
28450
+ return withTransientMutationRetry(() => api2.send(req));
28451
+ }
27575
28452
  async function cmdMessageSend(opts) {
28453
+ const remindAfterFlag = opts.remindAfter;
28454
+ const remindAfterMs = remindAfterFlag === undefined ? undefined : parseRemindAfter(remindAfterFlag);
27576
28455
  const api2 = getApi();
27577
28456
  const agent2 = agentId(opts);
27578
28457
  const channel2 = opts.target;
@@ -27582,10 +28461,10 @@ async function cmdMessageSend(opts) {
27582
28461
  const fileFlag = opts.file;
27583
28462
  const textFlag = opts.text;
27584
28463
  if (fileFlag) {
27585
- const fs12 = await import("fs");
27586
- if (!fs12.existsSync(fileFlag))
28464
+ const fs13 = await import("fs");
28465
+ if (!fs13.existsSync(fileFlag))
27587
28466
  throw new CliError(`message send: file not found: ${fileFlag}`);
27588
- text2 = fs12.readFileSync(fileFlag, "utf8").trim();
28467
+ text2 = fs13.readFileSync(fileFlag, "utf8").trim();
27589
28468
  } else if (typeof textFlag === "string") {
27590
28469
  text2 = decodeTextEscapes(textFlag);
27591
28470
  }
@@ -27605,7 +28484,7 @@ async function cmdMessageSend(opts) {
27605
28484
  }
27606
28485
  replyToSeq = n;
27607
28486
  }
27608
- const nonce = randomUUID5();
28487
+ const nonce = randomUUID6();
27609
28488
  const res = await sendWithRetry(api2, {
27610
28489
  agentId: agent2,
27611
28490
  channel: channel2,
@@ -27615,27 +28494,29 @@ async function cmdMessageSend(opts) {
27615
28494
  nonce
27616
28495
  });
27617
28496
  if (res.state === "blocked") {
27618
- throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` to align, then resend.`);
28497
+ 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.`);
28498
+ }
28499
+ const sent = `${res.message.channel}${res.message.seq}`;
28500
+ if (remindAfterMs === undefined)
28501
+ return { sent };
28502
+ const seqText = res.message.seq.replace(/^#/, "");
28503
+ const sentSeq = Number(seqText);
28504
+ if (!/^\d+$/.test(seqText) || !Number.isSafeInteger(sentSeq) || sentSeq < 1) {
28505
+ return { sent, reminder: { armed: false, reason: "server returned an invalid canonical message seq" } };
28506
+ }
28507
+ try {
28508
+ const reminder = await armMessageReminderFromEnv({
28509
+ channel: res.message.channel,
28510
+ sentSeq,
28511
+ remindAfterMs
28512
+ });
28513
+ return { sent, reminder };
28514
+ } catch {
28515
+ return { sent, reminder: { armed: false, reason: "local reminder request failed" } };
27619
28516
  }
27620
- return { sent: `${res.message.channel}${res.message.seq}` };
27621
28517
  }
27622
28518
  async function createPostWithRetry(api2, req) {
27623
- const MAX_ATTEMPTS = 4;
27624
- const BASE_DELAY_MS = 150;
27625
- const MAX_DELAY_MS = 2000;
27626
- let lastErr;
27627
- for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
27628
- try {
27629
- return await api2.createPost(req);
27630
- } catch (err) {
27631
- lastErr = err;
27632
- if (!isTransientSendError(err) || attempt === MAX_ATTEMPTS - 1)
27633
- throw err;
27634
- const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
27635
- await new Promise((r) => setTimeout(r, cap));
27636
- }
27637
- }
27638
- throw lastErr;
28519
+ return withTransientMutationRetry(() => api2.createPost(req));
27639
28520
  }
27640
28521
  async function cmdMessagePost(opts) {
27641
28522
  const api2 = getApi();
@@ -27650,10 +28531,10 @@ async function cmdMessagePost(opts) {
27650
28531
  const fileFlag = opts.file;
27651
28532
  const textFlag = opts.text;
27652
28533
  if (fileFlag) {
27653
- const fs12 = await import("fs");
27654
- if (!fs12.existsSync(fileFlag))
28534
+ const fs13 = await import("fs");
28535
+ if (!fs13.existsSync(fileFlag))
27655
28536
  throw new CliError(`message post: file not found: ${fileFlag}`);
27656
- text2 = fs12.readFileSync(fileFlag, "utf8").trim();
28537
+ text2 = fs13.readFileSync(fileFlag, "utf8").trim();
27657
28538
  } else if (typeof textFlag === "string") {
27658
28539
  text2 = decodeTextEscapes(textFlag);
27659
28540
  }
@@ -27662,7 +28543,7 @@ async function cmdMessagePost(opts) {
27662
28543
  if (!hasText && attachmentIds.length === 0) {
27663
28544
  throw new CliError("message post: --text <text>, --file <path>, or --attachment <id> is required");
27664
28545
  }
27665
- const nonce = randomUUID5();
28546
+ const nonce = randomUUID6();
27666
28547
  const res = await createPostWithRetry(api2, {
27667
28548
  agentId: agent2,
27668
28549
  forum,
@@ -27681,25 +28562,52 @@ async function cmdMessageEmoji(opts) {
27681
28562
  throw new CliError("message emoji: --target <ref> is required (e.g. /demo#1234/general#42)");
27682
28563
  if (!emoji3)
27683
28564
  throw new CliError("message emoji: --emoji <string> is required");
28565
+ const { channel: channel2, seq } = parseMessageTarget("message emoji", target);
28566
+ if (Buffer.byteLength(emoji3, "utf8") > MAX_EMOJI_BYTES) {
28567
+ const err = new CliError("emoji is too long");
28568
+ err.hint = "use a single emoji, not a phrase";
28569
+ throw err;
28570
+ }
28571
+ const res = await api2.reactAdd({ channel: channel2, seq, emoji: emoji3 });
28572
+ return { target, emoji: emoji3, duplicate: res.duplicate === true };
28573
+ }
28574
+ function parseMessageTarget(command, target) {
27684
28575
  let parsed;
27685
28576
  try {
27686
28577
  parsed = parseRef(target);
27687
28578
  } catch (err) {
27688
- throw new CliError(`message emoji: ${err.message}`);
28579
+ throw new CliError(`${command}: ${err.message}`);
27689
28580
  }
27690
28581
  if (parsed.seq === undefined) {
27691
- const err = new CliError(`message emoji needs a ref with a seq (e.g. ${target}#42)`);
28582
+ const err = new CliError(`${command} needs a ref with a seq (e.g. ${target}#42)`);
27692
28583
  err.hint = "pass --target /<server>/<channel>#N, /<server>/<channel>/#N#M for thread reply, or /.dm/<peer>#N";
27693
28584
  throw err;
27694
28585
  }
27695
- if (Buffer.byteLength(emoji3, "utf8") > MAX_EMOJI_BYTES) {
27696
- const err = new CliError("emoji is too long");
27697
- err.hint = "use a single emoji, not a phrase";
27698
- throw err;
27699
- }
27700
28586
  const channel2 = parsed.threadRootSeq !== undefined ? `/${parsed.server}/${parsed.channel}/#${parsed.threadRootSeq}` : `/${parsed.server}/${parsed.channel}`;
27701
- const res = await api2.reactAdd({ channel: channel2, seq: parsed.seq, emoji: emoji3 });
27702
- return { target, emoji: emoji3, duplicate: res.duplicate === true };
28587
+ return { channel: channel2, seq: parsed.seq };
28588
+ }
28589
+ async function cmdMessageMarkSet(opts) {
28590
+ const api2 = getApi();
28591
+ const target = opts.target;
28592
+ if (!target)
28593
+ throw new CliError("message mark set: --target <ref> is required");
28594
+ const request = parseMessageTarget("message mark set", target);
28595
+ await withTransientMutationRetry(() => api2.markSet(request));
28596
+ return { target, marked: true };
28597
+ }
28598
+ async function cmdMessageMarkRemove(opts) {
28599
+ const api2 = getApi();
28600
+ const target = opts.target;
28601
+ if (!target)
28602
+ throw new CliError("message mark remove: --target <ref> is required");
28603
+ const request = parseMessageTarget("message mark remove", target);
28604
+ await withTransientMutationRetry(() => api2.markRemove(request));
28605
+ return { target, marked: false };
28606
+ }
28607
+ async function cmdMessageMarkList(opts) {
28608
+ const api2 = getApi();
28609
+ const { marked } = await api2.listMarks({ agentId: agentId(opts) });
28610
+ return { marked: messagesInLocalTime(marked) };
27703
28611
  }
27704
28612
  async function cmdAttachmentUpload(opts) {
27705
28613
  const api2 = getApi();
@@ -27710,10 +28618,10 @@ async function cmdAttachmentUpload(opts) {
27710
28618
  throw new CliError("message attachment upload: --target <ref> is required");
27711
28619
  if (!filePath)
27712
28620
  throw new CliError("message attachment upload: --file <path> is required");
27713
- const fs12 = await import("fs/promises");
28621
+ const fs13 = await import("fs/promises");
27714
28622
  let bytes;
27715
28623
  try {
27716
- bytes = await fs12.readFile(filePath);
28624
+ bytes = await fs13.readFile(filePath);
27717
28625
  } catch (err) {
27718
28626
  throw new CliError(`message attachment upload: cannot read file: ${err.message}`);
27719
28627
  }
@@ -27723,10 +28631,35 @@ async function cmdAttachmentUpload(opts) {
27723
28631
  const pathMod = await import("path");
27724
28632
  const filename = pathMod.basename(filePath);
27725
28633
  const contentType = contentTypeFromFilename(filename);
28634
+ let thumbnail;
28635
+ let width;
28636
+ let height;
28637
+ if (["image/png", "image/jpeg", "image/webp", "image/gif"].includes(contentType)) {
28638
+ try {
28639
+ const { default: sharp } = await import("sharp");
28640
+ const image = sharp(bytes, { failOn: "error" });
28641
+ const metadata = await image.metadata();
28642
+ if (metadata.width && metadata.height) {
28643
+ width = metadata.width;
28644
+ height = metadata.height;
28645
+ }
28646
+ const jpeg = await image.resize({ width: 200, height: 200, fit: "inside", withoutEnlargement: true }).jpeg({ quality: 70 }).toBuffer();
28647
+ if (jpeg.byteLength <= MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES) {
28648
+ thumbnail = {
28649
+ data: new Uint8Array(jpeg),
28650
+ filename: "thumbnail.jpg",
28651
+ contentType: "image/jpeg"
28652
+ };
28653
+ }
28654
+ } catch {}
28655
+ }
27726
28656
  const result = await api2.attachmentUpload({
27727
28657
  agentId: agent2,
27728
28658
  target,
27729
- file: { data: new Uint8Array(bytes), filename, contentType }
28659
+ file: { data: new Uint8Array(bytes), filename, contentType },
28660
+ ...thumbnail ? { thumbnail } : {},
28661
+ ...width !== undefined ? { width } : {},
28662
+ ...height !== undefined ? { height } : {}
27730
28663
  });
27731
28664
  return result;
27732
28665
  }
@@ -27742,13 +28675,13 @@ async function cmdAttachmentDownload(opts) {
27742
28675
  const destPath = outFlag ?? pathMod.join(os4.tmpdir(), "alook-attachments", agent2, id, "file");
27743
28676
  const result = await api2.attachmentDownload({ agentId: agent2, id, destPath });
27744
28677
  if (!outFlag) {
27745
- const fs12 = await import("fs/promises");
28678
+ const fs13 = await import("fs/promises");
27746
28679
  const destDir = pathMod.dirname(destPath);
27747
28680
  const safeName = pathMod.basename(result.filename) || "file";
27748
28681
  const renamed = pathMod.join(destDir, safeName);
27749
28682
  if (renamed !== destPath) {
27750
28683
  try {
27751
- await fs12.rename(destPath, renamed);
28684
+ await fs13.rename(destPath, renamed);
27752
28685
  return { ...result, path: renamed };
27753
28686
  } catch {
27754
28687
  return { ...result, path: destPath };
@@ -27761,7 +28694,7 @@ async function cmdInboxPull(opts) {
27761
28694
  const api2 = getApi();
27762
28695
  const agent2 = agentId(opts);
27763
28696
  const max = opts.max ? Number(opts.max) : undefined;
27764
- const { messages, hasMore } = await api2.inboxPull({ agentId: agent2, max });
28697
+ const { messages, hasMore, markedCount } = await api2.inboxPull({ agentId: agent2, max });
27765
28698
  const pulledAt = nowLocalISO();
27766
28699
  let acked = 0;
27767
28700
  let ackError;
@@ -27785,7 +28718,10 @@ async function cmdInboxPull(opts) {
27785
28718
  hasMore,
27786
28719
  acked,
27787
28720
  pulledAt,
27788
- ...ackError ? { ackError } : {}
28721
+ ...ackError ? { ackError } : {},
28722
+ ...markedCount > 0 ? {
28723
+ markedReminder: `You have ${markedCount} marked ${markedCount === 1 ? "message" : "messages"}. Resolve ${markedCount === 1 ? "it" : "them"} before going dark unless blocked.`
28724
+ } : {}
27789
28725
  };
27790
28726
  }
27791
28727
  async function cmdServerList(opts) {
@@ -27880,10 +28816,10 @@ async function cmdNap(opts) {
27880
28816
  const textFlag = opts.text;
27881
28817
  let handoff;
27882
28818
  if (fileFlag) {
27883
- const fs12 = await import("fs");
27884
- if (!fs12.existsSync(fileFlag))
28819
+ const fs13 = await import("fs");
28820
+ if (!fs13.existsSync(fileFlag))
27885
28821
  throw new CliError(`nap: handoff file not found: ${fileFlag}`);
27886
- handoff = fs12.readFileSync(fileFlag, "utf8").trim();
28822
+ handoff = fs13.readFileSync(fileFlag, "utf8").trim();
27887
28823
  } else if (typeof textFlag === "string") {
27888
28824
  handoff = decodeTextEscapes(textFlag).trim();
27889
28825
  }
@@ -27899,7 +28835,7 @@ function buildProgram() {
27899
28835
  }).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
27900
28836
  const message2 = program.command("message").description("message operations").exitOverride();
27901
28837
  message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
27902
- 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() {
28838
+ 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() {
27903
28839
  const localOpts = this.opts();
27904
28840
  const globalOpts = program.opts();
27905
28841
  const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
@@ -27917,6 +28853,20 @@ function buildProgram() {
27917
28853
  const result = await cmdMessageEmoji({ ...globalOpts, ...localOpts });
27918
28854
  printEnvelope({ success: result });
27919
28855
  });
28856
+ const mark = message2.command("mark").description("durable message mark operations").exitOverride();
28857
+ mark.configureOutput({ writeOut: () => {}, writeErr: () => {} });
28858
+ mark.command("set").description("mark a message as outstanding work").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28859
+ const result = await cmdMessageMarkSet({ ...program.opts(), ...this.opts() });
28860
+ printEnvelope({ success: result });
28861
+ });
28862
+ mark.command("remove").description("remove an outstanding-work mark").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28863
+ const result = await cmdMessageMarkRemove({ ...program.opts(), ...this.opts() });
28864
+ printEnvelope({ success: result });
28865
+ });
28866
+ mark.command("list").description("list all currently visible marked messages").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28867
+ const result = await cmdMessageMarkList({ ...program.opts(), ...this.opts() });
28868
+ printEnvelope({ success: result });
28869
+ });
27920
28870
  const attachment = message2.command("attachment").description("attachment operations").exitOverride();
27921
28871
  attachment.configureOutput({ writeOut: () => {}, writeErr: () => {} });
27922
28872
  attachment.command("upload").description("upload a local file as a pending attachment for a future send").option("--target <ref>", "destination (channel, DM, or thread ref)").option("--file <path>", "local file to upload").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
@@ -28014,6 +28964,22 @@ function buildProgram() {
28014
28964
  daemon.command("run", { hidden: true }).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async () => {
28015
28965
  await daemonRunFromIpc();
28016
28966
  });
28967
+ daemon.command("resume", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28968
+ const localOpts = this.opts();
28969
+ await daemonResume({
28970
+ id: localOpts.id,
28971
+ baseDir: localOpts.baseDir,
28972
+ requestId: localOpts.requestId
28973
+ });
28974
+ });
28975
+ daemon.command("replace", { hidden: true }).requiredOption("--id <machineId>").requiredOption("--base-dir <path>").requiredOption("--request-id <id>").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
28976
+ const localOpts = this.opts();
28977
+ await daemonReplace({
28978
+ id: localOpts.id,
28979
+ baseDir: localOpts.baseDir,
28980
+ requestId: localOpts.requestId
28981
+ });
28982
+ });
28017
28983
  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) {
28018
28984
  const localOpts = this.opts();
28019
28985
  await daemonStop({
@@ -28043,6 +29009,7 @@ function buildProgram() {
28043
29009
  }
28044
29010
  async function main(argv = process.argv.slice(2)) {
28045
29011
  const program = buildProgram();
29012
+ let internalExitCode = 0;
28046
29013
  try {
28047
29014
  await program.parseAsync(argv, { from: "user" });
28048
29015
  } catch (err) {
@@ -28064,8 +29031,11 @@ async function main(argv = process.argv.slice(2)) {
28064
29031
  hint: err.hint
28065
29032
  });
28066
29033
  }
29034
+ if (argv[0] === "daemon" && (argv[1] === "resume" || argv[1] === "replace")) {
29035
+ internalExitCode = 1;
29036
+ }
28067
29037
  }
28068
- return 0;
29038
+ return internalExitCode;
28069
29039
  }
28070
29040
  function getHelpText(program, argv) {
28071
29041
  const args = argv.filter((a) => a !== "-h" && a !== "--help");