@alook/daemon 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +193 -77
- package/dist/index.js +57 -43
- package/package.json +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -16841,6 +16841,7 @@ var MAX_MESSAGE_CONTENT_LENGTH = 4000;
|
|
|
16841
16841
|
var MAX_EMOJI_BYTES = 32;
|
|
16842
16842
|
var MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
16843
16843
|
var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
16844
|
+
var MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES = 50 * 1024;
|
|
16844
16845
|
var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
|
|
16845
16846
|
var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
16846
16847
|
// ../shared/src/utils/slug.ts
|
|
@@ -17517,7 +17518,8 @@ var CommunityAgentAttachmentUploadResponseSchema = exports_external.object({
|
|
|
17517
17518
|
id: exports_external.string(),
|
|
17518
17519
|
filename: exports_external.string(),
|
|
17519
17520
|
contentType: exports_external.string(),
|
|
17520
|
-
size: exports_external.number()
|
|
17521
|
+
size: exports_external.number(),
|
|
17522
|
+
hasThumbnail: exports_external.boolean().optional()
|
|
17521
17523
|
});
|
|
17522
17524
|
var CommunityAgentAttachmentDownloadRequestSchema = exports_external.object({
|
|
17523
17525
|
id: exports_external.string().min(1)
|
|
@@ -17830,6 +17832,7 @@ var communityAttachment = sqliteTable("community_attachment", {
|
|
|
17830
17832
|
uploaderId: text("uploader_id").notNull(),
|
|
17831
17833
|
targetId: text("target_id").notNull(),
|
|
17832
17834
|
r2Key: text("r2_key").notNull(),
|
|
17835
|
+
thumbnailR2Key: text("thumbnail_r2_key"),
|
|
17833
17836
|
filename: text("filename").notNull(),
|
|
17834
17837
|
contentType: text("content_type"),
|
|
17835
17838
|
size: integer2("size"),
|
|
@@ -18138,6 +18141,7 @@ var messageAttachmentSchema = exports_external.strictObject({
|
|
|
18138
18141
|
id: string4,
|
|
18139
18142
|
filename: string4,
|
|
18140
18143
|
url: string4,
|
|
18144
|
+
thumbnailUrl: string4.optional(),
|
|
18141
18145
|
contentType: string4.optional(),
|
|
18142
18146
|
size: exports_external.number().optional(),
|
|
18143
18147
|
width: exports_external.number().nullable().optional(),
|
|
@@ -18658,6 +18662,15 @@ function createProxyServerApi(config2) {
|
|
|
18658
18662
|
const blobType = req.file.contentType ?? "application/octet-stream";
|
|
18659
18663
|
const bytes = req.file.data instanceof Uint8Array ? new Blob([new Uint8Array(req.file.data)], { type: blobType }) : req.file.data;
|
|
18660
18664
|
form.append("file", bytes, req.file.filename);
|
|
18665
|
+
if (req.thumbnail) {
|
|
18666
|
+
const thumbnailType = req.thumbnail.contentType ?? "image/jpeg";
|
|
18667
|
+
const thumbnailBytes = req.thumbnail.data instanceof Uint8Array ? new Blob([new Uint8Array(req.thumbnail.data)], { type: thumbnailType }) : req.thumbnail.data;
|
|
18668
|
+
form.append("thumbnail", thumbnailBytes, req.thumbnail.filename);
|
|
18669
|
+
}
|
|
18670
|
+
if (req.width !== undefined)
|
|
18671
|
+
form.append("width", String(req.width));
|
|
18672
|
+
if (req.height !== undefined)
|
|
18673
|
+
form.append("height", String(req.height));
|
|
18661
18674
|
const url2 = `${base}/api/community/channels/${REF_PLACEHOLDER_ID}/attachments?target=${encodeURIComponent(req.target)}`;
|
|
18662
18675
|
const res = await fetchImpl(url2, {
|
|
18663
18676
|
method: "POST",
|
|
@@ -18819,6 +18832,35 @@ function createProxyServerApi(config2) {
|
|
|
18819
18832
|
});
|
|
18820
18833
|
return parseJsonResponse(res, "inboxPull");
|
|
18821
18834
|
}
|
|
18835
|
+
async function callMarkSet(req) {
|
|
18836
|
+
const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
|
|
18837
|
+
method: "PUT",
|
|
18838
|
+
headers: {
|
|
18839
|
+
"content-type": "application/json",
|
|
18840
|
+
authorization: `Bearer ${config2.voucher}`
|
|
18841
|
+
},
|
|
18842
|
+
body: JSON.stringify(req)
|
|
18843
|
+
});
|
|
18844
|
+
await parseJsonResponse(res, "markSet");
|
|
18845
|
+
}
|
|
18846
|
+
async function callMarkRemove(req) {
|
|
18847
|
+
const res = await fetchImpl(`${base}/api/community/messages/resolve/marks`, {
|
|
18848
|
+
method: "DELETE",
|
|
18849
|
+
headers: {
|
|
18850
|
+
"content-type": "application/json",
|
|
18851
|
+
authorization: `Bearer ${config2.voucher}`
|
|
18852
|
+
},
|
|
18853
|
+
body: JSON.stringify(req)
|
|
18854
|
+
});
|
|
18855
|
+
await parseJsonResponse(res, "markRemove");
|
|
18856
|
+
}
|
|
18857
|
+
async function callListMarks() {
|
|
18858
|
+
const res = await fetchImpl(`${base}/api/community/users/me/marks`, {
|
|
18859
|
+
method: "GET",
|
|
18860
|
+
headers: { authorization: `Bearer ${config2.voucher}` }
|
|
18861
|
+
});
|
|
18862
|
+
return parseJsonResponse(res, "markList");
|
|
18863
|
+
}
|
|
18822
18864
|
async function callAck(req) {
|
|
18823
18865
|
const { agentId: _omit, ...wire } = req ?? {};
|
|
18824
18866
|
const res = await fetchImpl(`${base}/api/community/users/me/inbox/ack`, {
|
|
@@ -18933,6 +18975,9 @@ function createProxyServerApi(config2) {
|
|
|
18933
18975
|
attachmentUpload: callUpload,
|
|
18934
18976
|
attachmentDownload: callDownload,
|
|
18935
18977
|
reactAdd: callReactAdd,
|
|
18978
|
+
markSet: callMarkSet,
|
|
18979
|
+
markRemove: callMarkRemove,
|
|
18980
|
+
listMarks: (_r) => callListMarks(),
|
|
18936
18981
|
friendRequest: callFriendRequest,
|
|
18937
18982
|
listFriends: (_r) => callListFriends(),
|
|
18938
18983
|
nap: callNap
|
|
@@ -18992,6 +19037,9 @@ function cliCommandsSection() {
|
|
|
18992
19037
|
`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
19038
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
18994
19039
|
`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\`).`,
|
|
19040
|
+
`6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
|
|
19041
|
+
`7. \`${CLI} message mark remove --target <full-message-ref>\` — clear a completed message mark.`,
|
|
19042
|
+
`8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
|
|
18995
19043
|
"",
|
|
18996
19044
|
"### Servers",
|
|
18997
19045
|
"",
|
|
@@ -19012,7 +19060,7 @@ function cliCommandsSection() {
|
|
|
19012
19060
|
"",
|
|
19013
19061
|
"### Context Lifecycle",
|
|
19014
19062
|
"",
|
|
19015
|
-
`1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) —
|
|
19063
|
+
`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
19064
|
"",
|
|
19017
19065
|
"### Output format",
|
|
19018
19066
|
"",
|
|
@@ -19035,7 +19083,7 @@ function messagingSection() {
|
|
|
19035
19083
|
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
19036
19084
|
`- 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
19085
|
"",
|
|
19038
|
-
"###
|
|
19086
|
+
"### Context refs",
|
|
19039
19087
|
"",
|
|
19040
19088
|
"Path-style refs:",
|
|
19041
19089
|
"",
|
|
@@ -19067,15 +19115,14 @@ function messagingSection() {
|
|
|
19067
19115
|
"",
|
|
19068
19116
|
"### Message formatting",
|
|
19069
19117
|
"",
|
|
19070
|
-
"
|
|
19118
|
+
"Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
|
|
19071
19119
|
"",
|
|
19072
|
-
"- **
|
|
19073
|
-
"- **Mentions** — `@name#NNNN`
|
|
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.",
|
|
19120
|
+
"- **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.",
|
|
19121
|
+
"- **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
19122
|
"",
|
|
19076
19123
|
"```bash",
|
|
19077
|
-
|
|
19078
|
-
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001
|
|
19124
|
+
"# Notify Alice and point her to a message for context.",
|
|
19125
|
+
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
|
|
19079
19126
|
"```",
|
|
19080
19127
|
"",
|
|
19081
19128
|
"### Pulled messages",
|
|
@@ -19111,24 +19158,21 @@ function channelTypesSection() {
|
|
|
19111
19158
|
].join(`
|
|
19112
19159
|
`);
|
|
19113
19160
|
}
|
|
19114
|
-
function
|
|
19161
|
+
function visibilityAndNotificationsSection() {
|
|
19115
19162
|
return [
|
|
19116
|
-
"## Visibility &
|
|
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*.",
|
|
19119
|
-
"",
|
|
19120
|
-
"### Access — who can see the channel",
|
|
19121
|
-
"",
|
|
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.",
|
|
19163
|
+
"## Visibility & notifications",
|
|
19125
19164
|
"",
|
|
19126
|
-
"
|
|
19165
|
+
"Membership and access are related but not identical. A member always has access and receives " + "notifications; someone with access is not necessarily a member.",
|
|
19127
19166
|
"",
|
|
19128
|
-
"-
|
|
19129
|
-
"- A
|
|
19167
|
+
"- 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.",
|
|
19168
|
+
"- 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.",
|
|
19169
|
+
"- To bring someone into a thread discussion, first check that they can access the parent " + "channel; if they can, @mention them inside the thread.",
|
|
19130
19170
|
"",
|
|
19131
|
-
"
|
|
19171
|
+
"```bash",
|
|
19172
|
+
"# Check the channel's public/private type, then inspect who its members are.",
|
|
19173
|
+
`${CLI} channel list --server "demo#1234"`,
|
|
19174
|
+
`${CLI} channel member --channel "/demo#1234/team"`,
|
|
19175
|
+
"```"
|
|
19132
19176
|
].join(`
|
|
19133
19177
|
`);
|
|
19134
19178
|
}
|
|
@@ -19160,9 +19204,9 @@ function executionModelSection() {
|
|
|
19160
19204
|
"",
|
|
19161
19205
|
"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
19206
|
"",
|
|
19163
|
-
"On wake, restore
|
|
19207
|
+
"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
19208
|
"",
|
|
19165
|
-
"`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull.
|
|
19209
|
+
"`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
19210
|
].join(`
|
|
19167
19211
|
`);
|
|
19168
19212
|
}
|
|
@@ -19184,8 +19228,8 @@ function chaosAwarenessSection() {
|
|
|
19184
19228
|
"",
|
|
19185
19229
|
"Before you touch a message, make one call: does fulfilling it take work beyond a reply?",
|
|
19186
19230
|
"",
|
|
19187
|
-
|
|
19188
|
-
`- **No — it's just an answer.** Answer it. No "on it", no "let me check" — the reply
|
|
19231
|
+
`- **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.",
|
|
19232
|
+
`- **No — it's just an answer.** Answer it without marking. No "on it", no "let me ` + 'check" — the reply itself is the acknowledgment.',
|
|
19189
19233
|
"",
|
|
19190
19234
|
'Ack once. The signal is "picked up," not a running commentary.'
|
|
19191
19235
|
].join(`
|
|
@@ -19199,13 +19243,11 @@ function workspaceMemorySection() {
|
|
|
19199
19243
|
"",
|
|
19200
19244
|
"**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
|
|
19201
19245
|
"",
|
|
19202
|
-
"
|
|
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.",
|
|
19246
|
+
"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
19247
|
"",
|
|
19206
|
-
"
|
|
19248
|
+
"### Napping",
|
|
19207
19249
|
"",
|
|
19208
|
-
|
|
19250
|
+
`\`${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
19251
|
"",
|
|
19210
19252
|
"### memory.md",
|
|
19211
19253
|
"",
|
|
@@ -19223,16 +19265,13 @@ function workspaceMemorySection() {
|
|
|
19223
19265
|
"",
|
|
19224
19266
|
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative history.",
|
|
19225
19267
|
"",
|
|
19226
|
-
"###
|
|
19268
|
+
"### Outstanding work marks",
|
|
19227
19269
|
"",
|
|
19228
|
-
"
|
|
19270
|
+
"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
19271
|
"",
|
|
19230
|
-
"
|
|
19272
|
+
"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
19273
|
"",
|
|
19232
|
-
"
|
|
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
|
-
"```"
|
|
19274
|
+
"If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
|
|
19236
19275
|
].join(`
|
|
19237
19276
|
`);
|
|
19238
19277
|
}
|
|
@@ -19242,7 +19281,7 @@ function buildCliSystemPrompt(config2) {
|
|
|
19242
19281
|
cliCommandsSection(),
|
|
19243
19282
|
messagingSection(),
|
|
19244
19283
|
channelTypesSection(),
|
|
19245
|
-
|
|
19284
|
+
visibilityAndNotificationsSection(),
|
|
19246
19285
|
criticalRulesSection(),
|
|
19247
19286
|
executionModelSection(),
|
|
19248
19287
|
chaosAwarenessSection(),
|
|
@@ -21925,6 +21964,10 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
|
|
|
21925
21964
|
return method === "GET" ? "read" : "send";
|
|
21926
21965
|
if (/\/messages\/[^/]+\/reactions\//.test(pathname))
|
|
21927
21966
|
return "send";
|
|
21967
|
+
if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
|
|
21968
|
+
return "send";
|
|
21969
|
+
if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
|
|
21970
|
+
return "read";
|
|
21928
21971
|
if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
|
|
21929
21972
|
return "read";
|
|
21930
21973
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
@@ -21968,7 +22011,6 @@ async function startCredentialProxy(broker, options = {}) {
|
|
|
21968
22011
|
const outHeaders = { ...req.headers };
|
|
21969
22012
|
delete outHeaders["authorization"];
|
|
21970
22013
|
delete outHeaders["host"];
|
|
21971
|
-
delete outHeaders["content-length"];
|
|
21972
22014
|
outHeaders["authorization"] = `Bearer ${reg.runnerKey}`;
|
|
21973
22015
|
outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
|
|
21974
22016
|
outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
|
|
@@ -23679,6 +23721,8 @@ ${this.opts.wakePromptFooter}` : text2;
|
|
|
23679
23721
|
}
|
|
23680
23722
|
}
|
|
23681
23723
|
// src/manager/agentRouter.ts
|
|
23724
|
+
var DEFINITIVE_EXECUTABLE_FAILURES = new Set(["ENOENT", "EACCES", "ENOEXEC", "EPERM"]);
|
|
23725
|
+
|
|
23682
23726
|
class UnknownBotError extends Error {
|
|
23683
23727
|
botId;
|
|
23684
23728
|
constructor(botId) {
|
|
@@ -23719,14 +23763,14 @@ class UnknownRuntimeError extends Error {
|
|
|
23719
23763
|
function defaultFormatUnreadNoticeText(notice) {
|
|
23720
23764
|
return `You have unread messages in channel ${notice.channel}.`;
|
|
23721
23765
|
}
|
|
23722
|
-
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @
|
|
23766
|
+
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
23767
|
var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. Continue any unfinished work.";
|
|
23724
23768
|
function buildNapRewakePrompt(handoff) {
|
|
23725
23769
|
return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
|
|
23726
23770
|
|
|
23727
23771
|
` + handoff.trim() + `
|
|
23728
23772
|
|
|
23729
|
-
Then read @
|
|
23773
|
+
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
23774
|
}
|
|
23731
23775
|
|
|
23732
23776
|
class AgentRouter {
|
|
@@ -23781,6 +23825,11 @@ class AgentRouter {
|
|
|
23781
23825
|
isRuntimeHealthy(id) {
|
|
23782
23826
|
return this.runtimes.get(id)?.status === "healthy";
|
|
23783
23827
|
}
|
|
23828
|
+
recordRuntimeSpawnFailure(id, reason) {
|
|
23829
|
+
if (!DEFINITIVE_EXECUTABLE_FAILURES.has(reason))
|
|
23830
|
+
return;
|
|
23831
|
+
this.markRuntimeUnhealthy(id, reason);
|
|
23832
|
+
}
|
|
23784
23833
|
markRuntimeUnhealthy(id, reason) {
|
|
23785
23834
|
const existing = this.runtimes.get(id);
|
|
23786
23835
|
if (!existing)
|
|
@@ -24827,6 +24876,15 @@ function deriveAuditLogSubcommand(pathname, method) {
|
|
|
24827
24876
|
const canonical = pathname.split("?")[0] ?? pathname;
|
|
24828
24877
|
if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
|
|
24829
24878
|
return "reactAdd";
|
|
24879
|
+
if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
|
|
24880
|
+
if (method === "PUT")
|
|
24881
|
+
return "markSet";
|
|
24882
|
+
if (method === "DELETE")
|
|
24883
|
+
return "markRemove";
|
|
24884
|
+
return null;
|
|
24885
|
+
}
|
|
24886
|
+
if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
|
|
24887
|
+
return "markList";
|
|
24830
24888
|
if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
|
|
24831
24889
|
return "resolve";
|
|
24832
24890
|
if (/^\/api\/community\/channels\/[^/]+\/messages(\/|$)/.test(canonical)) {
|
|
@@ -25126,7 +25184,7 @@ async function createDaemon(opts) {
|
|
|
25126
25184
|
return opts.driverFor(agentId, runtimeConfig);
|
|
25127
25185
|
},
|
|
25128
25186
|
onRuntimeSpawnFailed: (runtimeId, reason) => {
|
|
25129
|
-
router?.
|
|
25187
|
+
router?.recordRuntimeSpawnFailure(runtimeId, reason);
|
|
25130
25188
|
},
|
|
25131
25189
|
onRuntimeSessionEstablished: (runtimeId) => {
|
|
25132
25190
|
router?.markRuntimeHealthy(runtimeId);
|
|
@@ -25150,6 +25208,7 @@ async function createDaemon(opts) {
|
|
|
25150
25208
|
}
|
|
25151
25209
|
};
|
|
25152
25210
|
},
|
|
25211
|
+
...opts.handshakeTimeoutMs !== undefined ? { handshakeTimeoutMs: opts.handshakeTimeoutMs } : {},
|
|
25153
25212
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
25154
25213
|
onAgentSession: (info) => void channel2.reportAgentSession(info),
|
|
25155
25214
|
onAgentActivity: (info) => {
|
|
@@ -27550,28 +27609,31 @@ function contentTypeFromFilename(filename) {
|
|
|
27550
27609
|
return "application/octet-stream";
|
|
27551
27610
|
}
|
|
27552
27611
|
}
|
|
27553
|
-
function
|
|
27612
|
+
function isTransientMutationError(err) {
|
|
27554
27613
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27555
27614
|
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
27615
|
}
|
|
27557
|
-
async function
|
|
27616
|
+
async function withTransientMutationRetry(mutation) {
|
|
27558
27617
|
const MAX_ATTEMPTS = 4;
|
|
27559
27618
|
const BASE_DELAY_MS = 150;
|
|
27560
27619
|
const MAX_DELAY_MS = 2000;
|
|
27561
27620
|
let lastErr;
|
|
27562
27621
|
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
27563
27622
|
try {
|
|
27564
|
-
return await
|
|
27623
|
+
return await mutation();
|
|
27565
27624
|
} catch (err) {
|
|
27566
27625
|
lastErr = err;
|
|
27567
|
-
if (!
|
|
27626
|
+
if (!isTransientMutationError(err) || attempt === MAX_ATTEMPTS - 1)
|
|
27568
27627
|
throw err;
|
|
27569
27628
|
const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
27570
|
-
await new Promise((
|
|
27629
|
+
await new Promise((resolve4) => setTimeout(resolve4, cap));
|
|
27571
27630
|
}
|
|
27572
27631
|
}
|
|
27573
27632
|
throw lastErr;
|
|
27574
27633
|
}
|
|
27634
|
+
async function sendWithRetry(api2, req) {
|
|
27635
|
+
return withTransientMutationRetry(() => api2.send(req));
|
|
27636
|
+
}
|
|
27575
27637
|
async function cmdMessageSend(opts) {
|
|
27576
27638
|
const api2 = getApi();
|
|
27577
27639
|
const agent2 = agentId(opts);
|
|
@@ -27620,22 +27682,7 @@ async function cmdMessageSend(opts) {
|
|
|
27620
27682
|
return { sent: `${res.message.channel}${res.message.seq}` };
|
|
27621
27683
|
}
|
|
27622
27684
|
async function createPostWithRetry(api2, req) {
|
|
27623
|
-
|
|
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;
|
|
27685
|
+
return withTransientMutationRetry(() => api2.createPost(req));
|
|
27639
27686
|
}
|
|
27640
27687
|
async function cmdMessagePost(opts) {
|
|
27641
27688
|
const api2 = getApi();
|
|
@@ -27681,25 +27728,52 @@ async function cmdMessageEmoji(opts) {
|
|
|
27681
27728
|
throw new CliError("message emoji: --target <ref> is required (e.g. /demo#1234/general#42)");
|
|
27682
27729
|
if (!emoji3)
|
|
27683
27730
|
throw new CliError("message emoji: --emoji <string> is required");
|
|
27731
|
+
const { channel: channel2, seq } = parseMessageTarget("message emoji", target);
|
|
27732
|
+
if (Buffer.byteLength(emoji3, "utf8") > MAX_EMOJI_BYTES) {
|
|
27733
|
+
const err = new CliError("emoji is too long");
|
|
27734
|
+
err.hint = "use a single emoji, not a phrase";
|
|
27735
|
+
throw err;
|
|
27736
|
+
}
|
|
27737
|
+
const res = await api2.reactAdd({ channel: channel2, seq, emoji: emoji3 });
|
|
27738
|
+
return { target, emoji: emoji3, duplicate: res.duplicate === true };
|
|
27739
|
+
}
|
|
27740
|
+
function parseMessageTarget(command, target) {
|
|
27684
27741
|
let parsed;
|
|
27685
27742
|
try {
|
|
27686
27743
|
parsed = parseRef(target);
|
|
27687
27744
|
} catch (err) {
|
|
27688
|
-
throw new CliError(
|
|
27745
|
+
throw new CliError(`${command}: ${err.message}`);
|
|
27689
27746
|
}
|
|
27690
27747
|
if (parsed.seq === undefined) {
|
|
27691
|
-
const err = new CliError(
|
|
27748
|
+
const err = new CliError(`${command} needs a ref with a seq (e.g. ${target}#42)`);
|
|
27692
27749
|
err.hint = "pass --target /<server>/<channel>#N, /<server>/<channel>/#N#M for thread reply, or /.dm/<peer>#N";
|
|
27693
27750
|
throw err;
|
|
27694
27751
|
}
|
|
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
27752
|
const channel2 = parsed.threadRootSeq !== undefined ? `/${parsed.server}/${parsed.channel}/#${parsed.threadRootSeq}` : `/${parsed.server}/${parsed.channel}`;
|
|
27701
|
-
|
|
27702
|
-
|
|
27753
|
+
return { channel: channel2, seq: parsed.seq };
|
|
27754
|
+
}
|
|
27755
|
+
async function cmdMessageMarkSet(opts) {
|
|
27756
|
+
const api2 = getApi();
|
|
27757
|
+
const target = opts.target;
|
|
27758
|
+
if (!target)
|
|
27759
|
+
throw new CliError("message mark set: --target <ref> is required");
|
|
27760
|
+
const request = parseMessageTarget("message mark set", target);
|
|
27761
|
+
await withTransientMutationRetry(() => api2.markSet(request));
|
|
27762
|
+
return { target, marked: true };
|
|
27763
|
+
}
|
|
27764
|
+
async function cmdMessageMarkRemove(opts) {
|
|
27765
|
+
const api2 = getApi();
|
|
27766
|
+
const target = opts.target;
|
|
27767
|
+
if (!target)
|
|
27768
|
+
throw new CliError("message mark remove: --target <ref> is required");
|
|
27769
|
+
const request = parseMessageTarget("message mark remove", target);
|
|
27770
|
+
await withTransientMutationRetry(() => api2.markRemove(request));
|
|
27771
|
+
return { target, marked: false };
|
|
27772
|
+
}
|
|
27773
|
+
async function cmdMessageMarkList(opts) {
|
|
27774
|
+
const api2 = getApi();
|
|
27775
|
+
const { marked } = await api2.listMarks({ agentId: agentId(opts) });
|
|
27776
|
+
return { marked: messagesInLocalTime(marked) };
|
|
27703
27777
|
}
|
|
27704
27778
|
async function cmdAttachmentUpload(opts) {
|
|
27705
27779
|
const api2 = getApi();
|
|
@@ -27723,10 +27797,35 @@ async function cmdAttachmentUpload(opts) {
|
|
|
27723
27797
|
const pathMod = await import("path");
|
|
27724
27798
|
const filename = pathMod.basename(filePath);
|
|
27725
27799
|
const contentType = contentTypeFromFilename(filename);
|
|
27800
|
+
let thumbnail;
|
|
27801
|
+
let width;
|
|
27802
|
+
let height;
|
|
27803
|
+
if (["image/png", "image/jpeg", "image/webp", "image/gif"].includes(contentType)) {
|
|
27804
|
+
try {
|
|
27805
|
+
const { default: sharp } = await import("sharp");
|
|
27806
|
+
const image = sharp(bytes, { failOn: "error" });
|
|
27807
|
+
const metadata = await image.metadata();
|
|
27808
|
+
if (metadata.width && metadata.height) {
|
|
27809
|
+
width = metadata.width;
|
|
27810
|
+
height = metadata.height;
|
|
27811
|
+
}
|
|
27812
|
+
const jpeg = await image.resize({ width: 200, height: 200, fit: "inside", withoutEnlargement: true }).jpeg({ quality: 70 }).toBuffer();
|
|
27813
|
+
if (jpeg.byteLength <= MAX_ATTACHMENT_THUMBNAIL_SIZE_BYTES) {
|
|
27814
|
+
thumbnail = {
|
|
27815
|
+
data: new Uint8Array(jpeg),
|
|
27816
|
+
filename: "thumbnail.jpg",
|
|
27817
|
+
contentType: "image/jpeg"
|
|
27818
|
+
};
|
|
27819
|
+
}
|
|
27820
|
+
} catch {}
|
|
27821
|
+
}
|
|
27726
27822
|
const result = await api2.attachmentUpload({
|
|
27727
27823
|
agentId: agent2,
|
|
27728
27824
|
target,
|
|
27729
|
-
file: { data: new Uint8Array(bytes), filename, contentType }
|
|
27825
|
+
file: { data: new Uint8Array(bytes), filename, contentType },
|
|
27826
|
+
...thumbnail ? { thumbnail } : {},
|
|
27827
|
+
...width !== undefined ? { width } : {},
|
|
27828
|
+
...height !== undefined ? { height } : {}
|
|
27730
27829
|
});
|
|
27731
27830
|
return result;
|
|
27732
27831
|
}
|
|
@@ -27761,7 +27860,7 @@ async function cmdInboxPull(opts) {
|
|
|
27761
27860
|
const api2 = getApi();
|
|
27762
27861
|
const agent2 = agentId(opts);
|
|
27763
27862
|
const max = opts.max ? Number(opts.max) : undefined;
|
|
27764
|
-
const { messages, hasMore } = await api2.inboxPull({ agentId: agent2, max });
|
|
27863
|
+
const { messages, hasMore, markedCount } = await api2.inboxPull({ agentId: agent2, max });
|
|
27765
27864
|
const pulledAt = nowLocalISO();
|
|
27766
27865
|
let acked = 0;
|
|
27767
27866
|
let ackError;
|
|
@@ -27785,7 +27884,10 @@ async function cmdInboxPull(opts) {
|
|
|
27785
27884
|
hasMore,
|
|
27786
27885
|
acked,
|
|
27787
27886
|
pulledAt,
|
|
27788
|
-
...ackError ? { ackError } : {}
|
|
27887
|
+
...ackError ? { ackError } : {},
|
|
27888
|
+
...markedCount > 0 ? {
|
|
27889
|
+
markedReminder: `You have ${markedCount} marked ${markedCount === 1 ? "message" : "messages"}. Resolve ${markedCount === 1 ? "it" : "them"} before going dark unless blocked.`
|
|
27890
|
+
} : {}
|
|
27789
27891
|
};
|
|
27790
27892
|
}
|
|
27791
27893
|
async function cmdServerList(opts) {
|
|
@@ -27917,6 +28019,20 @@ function buildProgram() {
|
|
|
27917
28019
|
const result = await cmdMessageEmoji({ ...globalOpts, ...localOpts });
|
|
27918
28020
|
printEnvelope({ success: result });
|
|
27919
28021
|
});
|
|
28022
|
+
const mark = message2.command("mark").description("durable message mark operations").exitOverride();
|
|
28023
|
+
mark.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
28024
|
+
mark.command("set").description("mark a message as outstanding work").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28025
|
+
const result = await cmdMessageMarkSet({ ...program.opts(), ...this.opts() });
|
|
28026
|
+
printEnvelope({ success: result });
|
|
28027
|
+
});
|
|
28028
|
+
mark.command("remove").description("remove an outstanding-work mark").requiredOption("--target <ref>", "full message ref").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28029
|
+
const result = await cmdMessageMarkRemove({ ...program.opts(), ...this.opts() });
|
|
28030
|
+
printEnvelope({ success: result });
|
|
28031
|
+
});
|
|
28032
|
+
mark.command("list").description("list all currently visible marked messages").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
28033
|
+
const result = await cmdMessageMarkList({ ...program.opts(), ...this.opts() });
|
|
28034
|
+
printEnvelope({ success: result });
|
|
28035
|
+
});
|
|
27920
28036
|
const attachment = message2.command("attachment").description("attachment operations").exitOverride();
|
|
27921
28037
|
attachment.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
27922
28038
|
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() {
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,9 @@ function cliCommandsSection() {
|
|
|
61
61
|
`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>\`.`,
|
|
62
62
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
63
63
|
`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\`).`,
|
|
64
|
+
`6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
|
|
65
|
+
`7. \`${CLI} message mark remove --target <full-message-ref>\` — clear a completed message mark.`,
|
|
66
|
+
`8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
|
|
64
67
|
"",
|
|
65
68
|
"### Servers",
|
|
66
69
|
"",
|
|
@@ -81,7 +84,7 @@ function cliCommandsSection() {
|
|
|
81
84
|
"",
|
|
82
85
|
"### Context Lifecycle",
|
|
83
86
|
"",
|
|
84
|
-
`1. \`${CLI} nap --handoff <file>\` (or \`--text <note>\`) —
|
|
87
|
+
`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.`,
|
|
85
88
|
"",
|
|
86
89
|
"### Output format",
|
|
87
90
|
"",
|
|
@@ -104,7 +107,7 @@ function messagingSection() {
|
|
|
104
107
|
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
105
108
|
`- 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.",
|
|
106
109
|
"",
|
|
107
|
-
"###
|
|
110
|
+
"### Context refs",
|
|
108
111
|
"",
|
|
109
112
|
"Path-style refs:",
|
|
110
113
|
"",
|
|
@@ -136,15 +139,14 @@ function messagingSection() {
|
|
|
136
139
|
"",
|
|
137
140
|
"### Message formatting",
|
|
138
141
|
"",
|
|
139
|
-
"
|
|
142
|
+
"Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
|
|
140
143
|
"",
|
|
141
|
-
"- **
|
|
142
|
-
"- **Mentions** — `@name#NNNN`
|
|
143
|
-
"- **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.",
|
|
144
|
+
"- **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.",
|
|
145
|
+
"- **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.`,
|
|
144
146
|
"",
|
|
145
147
|
"```bash",
|
|
146
|
-
|
|
147
|
-
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001
|
|
148
|
+
"# Notify Alice and point her to a message for context.",
|
|
149
|
+
`${CLI} message send --target "/demo#1234/general" --text "@alice#0001 Please review /demo#1234/general#42"`,
|
|
148
150
|
"```",
|
|
149
151
|
"",
|
|
150
152
|
"### Pulled messages",
|
|
@@ -180,24 +182,21 @@ function channelTypesSection() {
|
|
|
180
182
|
].join(`
|
|
181
183
|
`);
|
|
182
184
|
}
|
|
183
|
-
function
|
|
185
|
+
function visibilityAndNotificationsSection() {
|
|
184
186
|
return [
|
|
185
|
-
"## Visibility &
|
|
187
|
+
"## Visibility & notifications",
|
|
186
188
|
"",
|
|
187
|
-
"
|
|
189
|
+
"Membership and access are related but not identical. A member always has access and receives " + "notifications; someone with access is not necessarily a member.",
|
|
188
190
|
"",
|
|
189
|
-
"
|
|
191
|
+
"- 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.",
|
|
192
|
+
"- 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.",
|
|
193
|
+
"- To bring someone into a thread discussion, first check that they can access the parent " + "channel; if they can, @mention them inside the thread.",
|
|
190
194
|
"",
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
""
|
|
195
|
-
"
|
|
196
|
-
"",
|
|
197
|
-
"- 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.",
|
|
198
|
-
"- 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).",
|
|
199
|
-
"",
|
|
200
|
-
"When unsure whether a channel is private, treat it as private and check the roster first."
|
|
195
|
+
"```bash",
|
|
196
|
+
"# Check the channel's public/private type, then inspect who its members are.",
|
|
197
|
+
`${CLI} channel list --server "demo#1234"`,
|
|
198
|
+
`${CLI} channel member --channel "/demo#1234/team"`,
|
|
199
|
+
"```"
|
|
201
200
|
].join(`
|
|
202
201
|
`);
|
|
203
202
|
}
|
|
@@ -229,9 +228,9 @@ function executionModelSection() {
|
|
|
229
228
|
"",
|
|
230
229
|
"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.",
|
|
231
230
|
"",
|
|
232
|
-
"On wake, restore
|
|
231
|
+
"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.",
|
|
233
232
|
"",
|
|
234
|
-
"`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull.
|
|
233
|
+
"`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."
|
|
235
234
|
].join(`
|
|
236
235
|
`);
|
|
237
236
|
}
|
|
@@ -253,8 +252,8 @@ function chaosAwarenessSection() {
|
|
|
253
252
|
"",
|
|
254
253
|
"Before you touch a message, make one call: does fulfilling it take work beyond a reply?",
|
|
255
254
|
"",
|
|
256
|
-
|
|
257
|
-
`- **No — it's just an answer.** Answer it. No "on it", no "let me check" — the reply
|
|
255
|
+
`- **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.",
|
|
256
|
+
`- **No — it's just an answer.** Answer it without marking. No "on it", no "let me ` + 'check" — the reply itself is the acknowledgment.',
|
|
258
257
|
"",
|
|
259
258
|
'Ack once. The signal is "picked up," not a running commentary.'
|
|
260
259
|
].join(`
|
|
@@ -268,13 +267,11 @@ function workspaceMemorySection() {
|
|
|
268
267
|
"",
|
|
269
268
|
"**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
|
|
270
269
|
"",
|
|
271
|
-
"
|
|
272
|
-
"",
|
|
273
|
-
`\`${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.",
|
|
270
|
+
"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.",
|
|
274
271
|
"",
|
|
275
|
-
"
|
|
272
|
+
"### Napping",
|
|
276
273
|
"",
|
|
277
|
-
|
|
274
|
+
`\`${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.",
|
|
278
275
|
"",
|
|
279
276
|
"### memory.md",
|
|
280
277
|
"",
|
|
@@ -292,16 +289,13 @@ function workspaceMemorySection() {
|
|
|
292
289
|
"",
|
|
293
290
|
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative history.",
|
|
294
291
|
"",
|
|
295
|
-
"###
|
|
292
|
+
"### Outstanding work marks",
|
|
296
293
|
"",
|
|
297
|
-
"
|
|
294
|
+
"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.",
|
|
298
295
|
"",
|
|
299
|
-
"
|
|
296
|
+
"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.",
|
|
300
297
|
"",
|
|
301
|
-
"
|
|
302
|
-
'- [ ] {"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"}',
|
|
303
|
-
'- [ ] {"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"}',
|
|
304
|
-
"```"
|
|
298
|
+
"If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
|
|
305
299
|
].join(`
|
|
306
300
|
`);
|
|
307
301
|
}
|
|
@@ -311,7 +305,7 @@ function buildCliSystemPrompt(config) {
|
|
|
311
305
|
cliCommandsSection(),
|
|
312
306
|
messagingSection(),
|
|
313
307
|
channelTypesSection(),
|
|
314
|
-
|
|
308
|
+
visibilityAndNotificationsSection(),
|
|
315
309
|
criticalRulesSection(),
|
|
316
310
|
executionModelSection(),
|
|
317
311
|
chaosAwarenessSection(),
|
|
@@ -4807,6 +4801,8 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
4807
4801
|
}
|
|
4808
4802
|
}
|
|
4809
4803
|
// src/manager/agentRouter.ts
|
|
4804
|
+
var DEFINITIVE_EXECUTABLE_FAILURES = new Set(["ENOENT", "EACCES", "ENOEXEC", "EPERM"]);
|
|
4805
|
+
|
|
4810
4806
|
class UnknownBotError extends Error {
|
|
4811
4807
|
botId;
|
|
4812
4808
|
constructor(botId) {
|
|
@@ -4847,14 +4843,14 @@ class UnknownRuntimeError extends Error {
|
|
|
4847
4843
|
function defaultFormatUnreadNoticeText(notice) {
|
|
4848
4844
|
return `You have unread messages in channel ${notice.channel}.`;
|
|
4849
4845
|
}
|
|
4850
|
-
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @
|
|
4846
|
+
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.";
|
|
4851
4847
|
var MODEL_SWITCH_REWAKE_PROMPT = "You were just switched to a different model. Continue any unfinished work.";
|
|
4852
4848
|
function buildNapRewakePrompt(handoff) {
|
|
4853
4849
|
return "You took a nap: you reset your own session, so prior conversation context " + `is gone. Before sleeping you left yourself this handoff —
|
|
4854
4850
|
|
|
4855
4851
|
` + handoff.trim() + `
|
|
4856
4852
|
|
|
4857
|
-
Then read @
|
|
4853
|
+
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.";
|
|
4858
4854
|
}
|
|
4859
4855
|
|
|
4860
4856
|
class AgentRouter {
|
|
@@ -4909,6 +4905,11 @@ class AgentRouter {
|
|
|
4909
4905
|
isRuntimeHealthy(id) {
|
|
4910
4906
|
return this.runtimes.get(id)?.status === "healthy";
|
|
4911
4907
|
}
|
|
4908
|
+
recordRuntimeSpawnFailure(id, reason) {
|
|
4909
|
+
if (!DEFINITIVE_EXECUTABLE_FAILURES.has(reason))
|
|
4910
|
+
return;
|
|
4911
|
+
this.markRuntimeUnhealthy(id, reason);
|
|
4912
|
+
}
|
|
4912
4913
|
markRuntimeUnhealthy(id, reason) {
|
|
4913
4914
|
const existing = this.runtimes.get(id);
|
|
4914
4915
|
if (!existing)
|
|
@@ -5276,6 +5277,10 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
|
|
|
5276
5277
|
return method === "GET" ? "read" : "send";
|
|
5277
5278
|
if (/\/messages\/[^/]+\/reactions\//.test(pathname))
|
|
5278
5279
|
return "send";
|
|
5280
|
+
if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
|
|
5281
|
+
return "send";
|
|
5282
|
+
if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
|
|
5283
|
+
return "read";
|
|
5279
5284
|
if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
|
|
5280
5285
|
return "read";
|
|
5281
5286
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
@@ -5319,7 +5324,6 @@ async function startCredentialProxy(broker, options = {}) {
|
|
|
5319
5324
|
const outHeaders = { ...req.headers };
|
|
5320
5325
|
delete outHeaders["authorization"];
|
|
5321
5326
|
delete outHeaders["host"];
|
|
5322
|
-
delete outHeaders["content-length"];
|
|
5323
5327
|
outHeaders["authorization"] = `Bearer ${reg.runnerKey}`;
|
|
5324
5328
|
outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
|
|
5325
5329
|
outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
|
|
@@ -23327,6 +23331,15 @@ function deriveAuditLogSubcommand(pathname, method) {
|
|
|
23327
23331
|
const canonical = pathname.split("?")[0] ?? pathname;
|
|
23328
23332
|
if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
|
|
23329
23333
|
return "reactAdd";
|
|
23334
|
+
if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
|
|
23335
|
+
if (method === "PUT")
|
|
23336
|
+
return "markSet";
|
|
23337
|
+
if (method === "DELETE")
|
|
23338
|
+
return "markRemove";
|
|
23339
|
+
return null;
|
|
23340
|
+
}
|
|
23341
|
+
if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
|
|
23342
|
+
return "markList";
|
|
23330
23343
|
if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
|
|
23331
23344
|
return "resolve";
|
|
23332
23345
|
if (/^\/api\/community\/channels\/[^/]+\/messages(\/|$)/.test(canonical)) {
|
|
@@ -23626,7 +23639,7 @@ async function createDaemon(opts) {
|
|
|
23626
23639
|
return opts.driverFor(agentId, runtimeConfig);
|
|
23627
23640
|
},
|
|
23628
23641
|
onRuntimeSpawnFailed: (runtimeId, reason) => {
|
|
23629
|
-
router?.
|
|
23642
|
+
router?.recordRuntimeSpawnFailure(runtimeId, reason);
|
|
23630
23643
|
},
|
|
23631
23644
|
onRuntimeSessionEstablished: (runtimeId) => {
|
|
23632
23645
|
router?.markRuntimeHealthy(runtimeId);
|
|
@@ -23650,6 +23663,7 @@ async function createDaemon(opts) {
|
|
|
23650
23663
|
}
|
|
23651
23664
|
};
|
|
23652
23665
|
},
|
|
23666
|
+
...opts.handshakeTimeoutMs !== undefined ? { handshakeTimeoutMs: opts.handshakeTimeoutMs } : {},
|
|
23653
23667
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
23654
23668
|
onAgentSession: (info) => void channel2.reportAgentSession(info),
|
|
23655
23669
|
onAgentActivity: (info) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alook/daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/alookai/alook#readme",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"LICENSE"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=20"
|
|
33
|
+
"node": ">=20.9"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm --external commander --external ws && bun build src/cli/index.ts --outdir dist/cli --target node --format esm --external commander --external ws && node scripts/prepare-dist.mjs",
|
|
39
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --external commander --external ws --external sharp && bun build src/cli/index.ts --outdir dist/cli --target node --format esm --external commander --external ws --external sharp && node scripts/prepare-dist.mjs",
|
|
40
40
|
"prepack": "pnpm run build",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"test": "vitest run",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^15.0.0",
|
|
50
|
+
"sharp": "^0.35.0",
|
|
50
51
|
"ws": "^8.21.2"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|