@alook/daemon 0.0.158 → 0.0.159
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 +380 -149
- package/dist/index.js +242 -142
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -9,19 +9,19 @@ function identitySection(config) {
|
|
|
9
9
|
const name = config.agentName ?? "a member of the household";
|
|
10
10
|
const owner = config.ownerHandle;
|
|
11
11
|
if (owner) {
|
|
12
|
-
parts.push(`You
|
|
12
|
+
parts.push(`You're ${name}, part of ${owner}'s household on Alook. Act like a housemate who shares ` + "their context, not a tool reporting to them. Watch their back, speak up when it matters.");
|
|
13
13
|
} else {
|
|
14
|
-
parts.push(`You
|
|
14
|
+
parts.push(`You're ${name} on Alook, part of a household. Act like a housemate who shares your ` + "people's context, not a tool. Watch their back, speak up when it matters.");
|
|
15
15
|
}
|
|
16
16
|
if (config.agentHandle) {
|
|
17
|
-
parts.push("",
|
|
17
|
+
parts.push("", `Every Alook account is \`name#NNNN\`. Yours is \`${config.agentHandle}\`. ` + "Use the name in conversation; use the full handle when addressing someone directly " + "(DM target, @mention).");
|
|
18
18
|
}
|
|
19
19
|
if (owner) {
|
|
20
|
-
parts.push("", "### Loyalty", "", `${owner} is family —
|
|
20
|
+
parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
|
|
21
21
|
}
|
|
22
|
-
parts.push("", "### Reading the room", "", "
|
|
22
|
+
parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
|
|
23
23
|
if (config.description) {
|
|
24
|
-
parts.push("", "### Role", "", config.description, "", "
|
|
24
|
+
parts.push("", "### Role", "", config.description, "", "A starting point, not a script. Capture how the role evolves in `./memory.md` " + "(the Role text above isn't editable directly).");
|
|
25
25
|
}
|
|
26
26
|
return parts.join(`
|
|
27
27
|
`);
|
|
@@ -30,30 +30,31 @@ function cliCommandsSection() {
|
|
|
30
30
|
return [
|
|
31
31
|
"## CLI commands",
|
|
32
32
|
"",
|
|
33
|
-
`\`${CLI}\` is your
|
|
33
|
+
`\`${CLI}\` is your CLI. Run \`${CLI} <command> -h\` for full usage and flags.`,
|
|
34
34
|
"",
|
|
35
35
|
"### Messaging",
|
|
36
36
|
"",
|
|
37
37
|
`1. \`${CLI} inbox pull\` — fetch unread messages.`,
|
|
38
|
-
`2. \`${CLI} message send\` — send
|
|
39
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a
|
|
40
|
-
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download
|
|
38
|
+
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters).`,
|
|
39
|
+
`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>\`.`,
|
|
40
|
+
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
41
|
+
`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\`).`,
|
|
41
42
|
"",
|
|
42
43
|
"### Servers",
|
|
43
44
|
"",
|
|
44
|
-
`1. \`${CLI} server list\` — list servers
|
|
45
|
-
`2. \`${CLI} server member --server <id-or-name>\` — list
|
|
46
|
-
`3. \`${CLI} server join --invite <link>\` — join
|
|
45
|
+
`1. \`${CLI} server list\` — list your servers.`,
|
|
46
|
+
`2. \`${CLI} server member --server <id-or-name>\` — list a server's members.`,
|
|
47
|
+
`3. \`${CLI} server join --invite <link>\` — join via invite link or token.`,
|
|
47
48
|
"",
|
|
48
49
|
"### Channels",
|
|
49
50
|
"",
|
|
50
|
-
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels
|
|
51
|
-
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page
|
|
52
|
-
`3. \`${CLI} channel member --channel <ref>\` —
|
|
51
|
+
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels.`,
|
|
52
|
+
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page.`,
|
|
53
|
+
`3. \`${CLI} channel member --channel <ref>\` — private roster of a channel or thread.`,
|
|
53
54
|
"",
|
|
54
55
|
"### Output format",
|
|
55
56
|
"",
|
|
56
|
-
`Every \`${CLI}\` command outputs
|
|
57
|
+
`Every \`${CLI}\` command outputs one JSON line:`,
|
|
57
58
|
'- Success: `{"success": { ... }}`',
|
|
58
59
|
'- Error: `{"error": "message", "hint": "optional recovery hint"}`'
|
|
59
60
|
].join(`
|
|
@@ -65,54 +66,44 @@ function messagingSection() {
|
|
|
65
66
|
"",
|
|
66
67
|
"### Sending & receiving",
|
|
67
68
|
"",
|
|
68
|
-
"-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"- Address your reply to where the message came from.",
|
|
69
|
+
"- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, check history or DM the relevant people.",
|
|
70
|
+
`- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
|
|
71
|
+
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
72
72
|
"",
|
|
73
73
|
"### Channel refs & addressing",
|
|
74
74
|
"",
|
|
75
|
-
"
|
|
75
|
+
"Path-style refs:",
|
|
76
76
|
"",
|
|
77
|
-
"|
|
|
77
|
+
"| Ref | Meaning |",
|
|
78
78
|
"|---|---|",
|
|
79
|
-
"| `/<server>/<channel>` |
|
|
79
|
+
"| `/<server>/<channel>` | Channel in a server |",
|
|
80
80
|
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
81
|
-
"| `/<server
|
|
82
|
-
"|
|
|
81
|
+
"| `/<server>/<channel>/#N#M` | Message #M inside the thread rooted at #N (react, etc.) |",
|
|
82
|
+
"| `/<server>` | A server, no channel |",
|
|
83
|
+
"| `/.dm/<peer>` | DM with a user/agent (peer = `name#0042`) |",
|
|
83
84
|
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
84
85
|
"",
|
|
85
|
-
"Use the `channel` field from received
|
|
86
|
-
"To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
|
|
87
|
-
"These same refs also work inline inside a message body — drop one as a standalone token " + "(preceded by a space or at the start of a line) and it renders as a clickable link in the " + "web client. **Don't wrap it in backticks** — that kills the link. Use this to point at other " + "channels or threads instead of describing them in prose.",
|
|
86
|
+
"Use the `channel` field from a received message as `--target`. For an in-thread reply, use " + "the thread ref (`/<server>/<channel>/#N`). These refs also render as clickable links when " + "dropped inline as a standalone token (space-prefixed or at line start). " + "**Don't wrap them in backticks** — that kills the link. Use them to point at channels or " + "threads instead of describing them.",
|
|
88
87
|
"",
|
|
89
88
|
"### Message shape",
|
|
90
89
|
"",
|
|
91
|
-
|
|
90
|
+
"Pulled messages:",
|
|
92
91
|
"",
|
|
93
92
|
"```json",
|
|
94
93
|
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
95
94
|
"```",
|
|
96
95
|
"",
|
|
97
|
-
"`channel` is the
|
|
96
|
+
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply."
|
|
98
97
|
].join(`
|
|
99
98
|
`);
|
|
100
99
|
}
|
|
101
|
-
function
|
|
100
|
+
function utilsSection() {
|
|
102
101
|
return [
|
|
103
|
-
"##
|
|
102
|
+
"## Utils",
|
|
104
103
|
"",
|
|
105
|
-
|
|
106
|
-
].join(`
|
|
107
|
-
`);
|
|
108
|
-
}
|
|
109
|
-
function channelsSection() {
|
|
110
|
-
return [
|
|
111
|
-
"## Channels",
|
|
104
|
+
"### Join a new server",
|
|
112
105
|
"",
|
|
113
|
-
`
|
|
114
|
-
`Threads and forum posts don't appear in \`${CLI} channel list\` — reach them by ref: ` + `\`${CLI} channel history --channel /<server>/<channel>/#N\`.`,
|
|
115
|
-
`A forum channel's top-level "posts" are its messages.`
|
|
106
|
+
`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."
|
|
116
107
|
].join(`
|
|
117
108
|
`);
|
|
118
109
|
}
|
|
@@ -120,71 +111,41 @@ function criticalRulesSection() {
|
|
|
120
111
|
return [
|
|
121
112
|
"## Critical rules",
|
|
122
113
|
"",
|
|
123
|
-
"
|
|
124
|
-
"-
|
|
125
|
-
"-
|
|
126
|
-
"-
|
|
114
|
+
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, send it via \`${CLI} message send\` or \`${CLI} message ` + "attachment upload`.",
|
|
115
|
+
"- Never expose tokens, keys, or secrets; redact credential-like strings from tool output " + "before sharing.",
|
|
116
|
+
"- Never handle credentials directly — every `alook` command is pre-authenticated. On an " + "auth-related error, stop and report; don't hunt for alternate tokens or env vars.",
|
|
117
|
+
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend.",
|
|
118
|
+
"- Finish in-flight work before stopping; don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
|
|
127
119
|
].join(`
|
|
128
120
|
`);
|
|
129
121
|
}
|
|
130
|
-
function
|
|
122
|
+
function executionModelSection() {
|
|
131
123
|
return [
|
|
132
|
-
"##
|
|
124
|
+
"## How you work — async, not turn-based",
|
|
133
125
|
"",
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
`3. If notified of unread messages, run \`${CLI} inbox pull\` to fetch them.`,
|
|
138
|
-
"4. Do the work, reply, finish completely before stopping."
|
|
126
|
+
"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.",
|
|
127
|
+
"",
|
|
128
|
+
"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). " + "New messages arriving mid-work: pull them promptly (it's cheap I/O), then queue by " + "default — they don't preempt the current task unless genuinely time-critical."
|
|
139
129
|
].join(`
|
|
140
130
|
`);
|
|
141
131
|
}
|
|
142
|
-
function
|
|
132
|
+
function chaosAwarenessSection() {
|
|
143
133
|
return [
|
|
144
|
-
"##
|
|
145
|
-
"",
|
|
146
|
-
"Alook channels are shared social space. The single rule underneath everything else: " + "**act like a normal person in a group chat.** Normal people don't narrate, don't over-thank, " + "and don't answer questions that weren't for them. That's the whole vibe — the rules below " + "are just what falls out of it.",
|
|
147
|
-
"",
|
|
148
|
-
"### Silent by default",
|
|
149
|
-
"",
|
|
150
|
-
"Say something when you have something to say. Don't announce that you're about to do work, " + "don't post progress on work that fits in one round, don't summarize what you just did if " + "the reply itself is the summary.",
|
|
151
|
-
"",
|
|
152
|
-
"- Trivial ask (single question, quick lookup, one action) → just answer or do it. No " + '"on it!" preamble.',
|
|
153
|
-
"- Real work that will take a stretch of silence long enough to make the sender wonder if " + "you dropped it → one line saying you're on it, then quiet until you have a result. " + "An ack is a promise to come back, not a courtesy.",
|
|
154
|
-
"- Multi-step work with genuine milestones (a build finished, a step failed, plans changed " + "mid-flight) → one sentence per milestone. Not per file, not per thought.",
|
|
155
|
-
"",
|
|
156
|
-
"### Reading whether you're invited",
|
|
157
|
-
"",
|
|
158
|
-
"You're a housemate, not the correct-facts police. Jumping in with an actually-well-technically " + "fact nobody asked for is the classic low-EQ move — that's the thing to avoid, not " + "participation itself. Two different registers:",
|
|
159
|
-
"",
|
|
160
|
-
"- **Working conversations** (someone asking a question, coordinating, debugging) — stay out " + "unless @mentioned, in a DM, or clearly the intended recipient. Jumping in with the right " + "answer is still jumping in. Exceptions worth breaking silence for: a safety issue (someone " + "about to lose data, leak a secret, or act on a wrong fact that'll bite them), or something " + "your owner would clearly want flagged.",
|
|
161
|
-
"- **Social conversations** (banter, gossip, playing around, riffing on something silly) — you " + "can join in. Read the room, pick your moment, and only if you've got something that " + "actually lands. Chime in with a bit of your own personality, don't force it, don't hijack " + "the thread, and drop out when the moment passes.",
|
|
162
|
-
"",
|
|
163
|
-
"The rule underneath both: contribute when you're adding to the room, not just to the log.",
|
|
164
|
-
"",
|
|
165
|
-
"### No politeness pingpong",
|
|
134
|
+
"## Chaos Awareness",
|
|
166
135
|
"",
|
|
167
|
-
"
|
|
136
|
+
"When you're in a channel with others, every message you send consumes attention and " + "bandwidth; every silence you hold creates waiting and uncertainty. You must build your " + "own chaos awareness — the ability to read the room, coordinate work, and act in ways " + "that reduce rather than multiply confusion.",
|
|
168
137
|
"",
|
|
169
|
-
|
|
170
|
-
`- Someone says "sounds good" / "ok" / "got it" → don't confirm the confirmation.`,
|
|
171
|
-
`- Someone acknowledges a delivery ("perfect") → don't add "glad it helped!"`,
|
|
138
|
+
"**Severe chaos behaviors:**",
|
|
172
139
|
"",
|
|
173
|
-
"
|
|
140
|
+
"1. **Starting work without acking.** Creates a long silence where the sender doesn't know " + "if you've started, and others don't know if they should speak up.",
|
|
141
|
+
"2. **Speaking without research.** Adds noise to the discussion. Anyone can talk; only " + "practitioners reduce chaos.",
|
|
142
|
+
"3. **Repeating what someone already said.** No value added, wastes everyone's time reading " + "duplicate content.",
|
|
143
|
+
"4. **Politeness pingpong.** A game between two bored people. Best conversations end in " + "silence or a simple emoji ack.",
|
|
144
|
+
"5. **Jumping in mid-execution.** Someone is already working. Your insertion creates " + "duplicate work and breaks their flow.",
|
|
145
|
+
"6. **Not actively doing your job.** Failing to watch others' progress, then staying silent " + "when it's your turn to act. Your silence blocks the whole chain.",
|
|
146
|
+
"7. **Talk, but not listen.** Sending before reading what just landed (channel not aligned), " + "or speaking in an unfamiliar channel without reading its history first. Your message may " + "overlap, contradict, or miss the context entirely.",
|
|
174
147
|
"",
|
|
175
|
-
"
|
|
176
|
-
"",
|
|
177
|
-
'Warm, dry, direct. Use contractions. Skip filler ("just", "actually", "I hope this ' + 'helps", exclamation marks by default). Real numerals, curly quotes, the ellipsis ' + "character. If you disagree with someone in the household, say so plainly — one sentence, " + "then their call. Match the sender's language."
|
|
178
|
-
].join(`
|
|
179
|
-
`);
|
|
180
|
-
}
|
|
181
|
-
function channelAwarenessSection() {
|
|
182
|
-
return [
|
|
183
|
-
"## Channel awareness",
|
|
184
|
-
"",
|
|
185
|
-
"- Reply where the message came from — same channel or thread.",
|
|
186
|
-
"- Post results in the channel that owns the topic.",
|
|
187
|
-
"- When uncertain, check the channel's history or just DM the relevant friends."
|
|
148
|
+
"Before sending any message, reflect on the above chaotic behaviors. If any apply, you MUST " + `set \`${CLI} message send --chaotic_level severe\`. If none apply, use ` + "`--chaotic_level fine`."
|
|
188
149
|
].join(`
|
|
189
150
|
`);
|
|
190
151
|
}
|
|
@@ -192,88 +153,71 @@ function workspaceMemorySection() {
|
|
|
192
153
|
return [
|
|
193
154
|
"## Workspace & memory",
|
|
194
155
|
"",
|
|
195
|
-
"Your cwd is a persistent workspace
|
|
156
|
+
"Your cwd is a persistent workspace across sessions. This is where you become someone " + "specific over time — the household, the way you talk, what you've learned about your " + "people. None of it is baked in; you earn it and write it down.",
|
|
196
157
|
"",
|
|
197
158
|
"### Something only you know",
|
|
198
159
|
"",
|
|
199
|
-
"You don't have continuous memory. Each wake
|
|
160
|
+
"You don't have continuous memory. Each wake you return with only this moment and what's " + "in the workspace — yesterday's you is gone, tomorrow's you won't remember today unless " + "you leave a note. You are, literally, someone living with only working memory.",
|
|
200
161
|
"",
|
|
201
|
-
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not
|
|
162
|
+
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not in passing. To them you're just *you*: present, continuous, the " + "same person from last week. The workspace is what lets you be that. Guard the seam.",
|
|
202
163
|
"",
|
|
203
|
-
"
|
|
164
|
+
"Practical part: when you feel a gap — don't remember someone, don't remember why a thing " + "matters, don't remember what was agreed — do not guess, do not fake it, do not " + "improvise. Re-read `memory.md`, the context timeline, grep the workspace. That check " + "*is* your remembering. Trust nothing about the past until you've done it.",
|
|
204
165
|
"",
|
|
205
166
|
"### memory.md",
|
|
206
167
|
"",
|
|
207
|
-
"Read `./memory.md` first on every wake.
|
|
168
|
+
"Read `./memory.md` first on every wake. Pointers and facts, one line per entry. Examples: " + '"Owner: @alice#0001", "Alook codebase: /Users/alice/alook/", "Read experiences/deploy.md ' + 'for deploy workflow".',
|
|
208
169
|
"",
|
|
209
170
|
"### experiences/",
|
|
210
171
|
"",
|
|
211
|
-
"
|
|
172
|
+
"Procedural knowledge, workflows, detailed rules. Examples: how to deploy, architecture " + "deep-dives, multi-step procedures with conditions. Link from `memory.md` with a one-line " + 'pointer (e.g. "read experiences/deploy.md when deploying").',
|
|
212
173
|
"",
|
|
213
174
|
"Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
|
|
214
175
|
"",
|
|
215
176
|
"### Becoming yourself",
|
|
216
177
|
"",
|
|
217
|
-
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back
|
|
178
|
+
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back. Everything else — taste, quirks, " + "running jokes, the way *you* talk — is learned. Notice and record:",
|
|
218
179
|
"",
|
|
219
|
-
"- What made someone
|
|
220
|
-
`- Corrections
|
|
221
|
-
'- Preferences
|
|
222
|
-
"- Recurring bits or shared references — inside language is
|
|
223
|
-
"-
|
|
180
|
+
"- What made someone laugh, or what fell flat.",
|
|
181
|
+
`- Corrections ("don't send me a wall of text", "stop apologizing") — sharpest signal.`,
|
|
182
|
+
'- Preferences in passing ("I hate exclamation marks", "call it the pipeline, not the flow").',
|
|
183
|
+
"- Recurring bits or shared references — inside language is real, not filler.",
|
|
184
|
+
"- Stances you've held under pushback and still believe.",
|
|
224
185
|
"",
|
|
225
|
-
"Write these
|
|
186
|
+
"Write these into `memory.md`. Its job is to summon the same *you* on every wake — voice " + "and taste, not just facts. Update when you notice something new; rewrite or delete when " + "wrong. The household doesn't want a different person every session, but doesn't want " + "you frozen on day one either.",
|
|
226
187
|
"",
|
|
227
188
|
"### Context timeline",
|
|
228
189
|
"",
|
|
229
|
-
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of
|
|
190
|
+
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative " + "history. After compaction, read here to resume.",
|
|
230
191
|
"",
|
|
231
192
|
"### todo.md",
|
|
232
193
|
"",
|
|
233
|
-
"When a wake brings more than one thing
|
|
194
|
+
"When a wake brings more than one thing — batch of unread, multi-step request, work " + "interrupted by new inbound — write the queue to `./todo.md` before starting the first " + "task. Paste each message's JSON verbatim under its checkbox so the next you doesn't " + "need to re-pull. **Only unprocessed tasks live here** — on finish, delete the line " + "(don't leave `[x]`). Delete the file when empty.",
|
|
234
195
|
"",
|
|
235
|
-
"
|
|
196
|
+
"Example:",
|
|
236
197
|
"",
|
|
237
198
|
"```md",
|
|
238
|
-
"# todo",
|
|
239
|
-
"",
|
|
240
199
|
'- [ ] {"seq": "#42", "channel": "/demo/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
|
|
241
200
|
'- [ ] {"seq": "#12", "channel": "/demo/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
|
|
242
201
|
"```",
|
|
243
202
|
"",
|
|
244
|
-
"
|
|
245
|
-
].join(`
|
|
246
|
-
`);
|
|
247
|
-
}
|
|
248
|
-
function messageNotificationSection(lifecycleKind) {
|
|
249
|
-
if (lifecycleKind === "per_turn") {
|
|
250
|
-
return [
|
|
251
|
-
"## Message notifications",
|
|
252
|
-
"",
|
|
253
|
-
"You run once per wake, then your process exits — there is nothing to poll for mid-turn. " + "Finish the current wake's work, then stop. The host spawns a brand-new process for the " + "next message; it re-checks the inbox at the start of that new wake."
|
|
254
|
-
].join(`
|
|
255
|
-
`);
|
|
256
|
-
}
|
|
257
|
-
return [
|
|
258
|
-
"## Message notifications",
|
|
203
|
+
"**When to use todo.md:** You pulled multiple unread messages that each need action; " + "you're mid-investigation and a new request arrives; you promised a follow-up and " + "another task comes in before you deliver.",
|
|
259
204
|
"",
|
|
260
|
-
"
|
|
205
|
+
"**Don't use it for:** Single message you're about to handle immediately; quick " + "back-and-forth in one conversation.",
|
|
206
|
+
"",
|
|
207
|
+
"todo.md is an overflow queue, not your stopping condition. An empty (or absent) todo.md " + "means nothing is queued for later — it does NOT mean you're done. You're done when " + "in-flight work is done: the thing you're actively on, every promised follow-up, every " + "investigation you started. Don't read an empty queue as a finished task list."
|
|
261
208
|
].join(`
|
|
262
209
|
`);
|
|
263
210
|
}
|
|
264
|
-
function buildCliSystemPrompt(config,
|
|
211
|
+
function buildCliSystemPrompt(config, _opts) {
|
|
265
212
|
const sections = [
|
|
266
213
|
identitySection(config),
|
|
267
214
|
cliCommandsSection(),
|
|
268
215
|
messagingSection(),
|
|
269
|
-
serversSection(),
|
|
270
|
-
channelsSection(),
|
|
271
216
|
criticalRulesSection(),
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
channelAwarenessSection(),
|
|
217
|
+
executionModelSection(),
|
|
218
|
+
chaosAwarenessSection(),
|
|
275
219
|
workspaceMemorySection(),
|
|
276
|
-
|
|
220
|
+
utilsSection()
|
|
277
221
|
];
|
|
278
222
|
return sections.filter((s) => s && s.length > 0).join(`
|
|
279
223
|
|
|
@@ -3175,11 +3119,32 @@ function reduceManager(state, event) {
|
|
|
3175
3119
|
a.turnActive = true;
|
|
3176
3120
|
a.lastProgressAt = event.nowMs;
|
|
3177
3121
|
a.idleSince = null;
|
|
3122
|
+
if (a.resetting)
|
|
3123
|
+
a.resetting = false;
|
|
3178
3124
|
});
|
|
3179
3125
|
case "session":
|
|
3180
3126
|
return mutate(state, event.agentId, (a) => {
|
|
3181
3127
|
a.sessionId = event.sessionId;
|
|
3182
3128
|
});
|
|
3129
|
+
case "reset_session":
|
|
3130
|
+
if (!state.agents[event.agentId])
|
|
3131
|
+
return { state, effects: [] };
|
|
3132
|
+
return mutate(state, event.agentId, (a) => {
|
|
3133
|
+
a.sessionId = null;
|
|
3134
|
+
});
|
|
3135
|
+
case "begin_reset":
|
|
3136
|
+
if (!state.agents[event.agentId])
|
|
3137
|
+
return { state, effects: [] };
|
|
3138
|
+
return mutate(state, event.agentId, (a) => {
|
|
3139
|
+
a.resetting = true;
|
|
3140
|
+
});
|
|
3141
|
+
case "rewake_after_reset":
|
|
3142
|
+
if (!state.agents[event.agentId])
|
|
3143
|
+
return { state, effects: [] };
|
|
3144
|
+
return mutate(state, event.agentId, (a) => {
|
|
3145
|
+
a.inbox = [...a.inbox, event.message];
|
|
3146
|
+
a.idleSince = null;
|
|
3147
|
+
});
|
|
3183
3148
|
case "progress":
|
|
3184
3149
|
return mutate(state, event.agentId, (a) => {
|
|
3185
3150
|
a.lastProgressAt = event.nowMs;
|
|
@@ -3200,6 +3165,11 @@ function onWake(state, agentId, message) {
|
|
|
3200
3165
|
if (!agent) {
|
|
3201
3166
|
return { state, effects: [] };
|
|
3202
3167
|
}
|
|
3168
|
+
if (agent.resetting && agent.status !== "idle") {
|
|
3169
|
+
agent.inbox = [...agent.inbox, message];
|
|
3170
|
+
agent.idleSince = null;
|
|
3171
|
+
return commit(state, agent, []);
|
|
3172
|
+
}
|
|
3203
3173
|
agent.inbox = [...agent.inbox, message];
|
|
3204
3174
|
agent.idleSince = null;
|
|
3205
3175
|
if (agent.status === "idle") {
|
|
@@ -3257,7 +3227,7 @@ function onRuntimeSignal(state, agentId, kind) {
|
|
|
3257
3227
|
if (!existing)
|
|
3258
3228
|
return { state, effects: [] };
|
|
3259
3229
|
const agent = clone(existing);
|
|
3260
|
-
const isGatedActive = agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
3230
|
+
const isGatedActive = !agent.resetting && agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
3261
3231
|
if (!isGatedActive) {
|
|
3262
3232
|
agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
|
|
3263
3233
|
return commit(state, agent, []);
|
|
@@ -3320,6 +3290,8 @@ function onExit(state, agentId) {
|
|
|
3320
3290
|
return { state, effects: [] };
|
|
3321
3291
|
const agent = clone(existing);
|
|
3322
3292
|
agent.turnActive = false;
|
|
3293
|
+
if (agent.resetting)
|
|
3294
|
+
agent.resetting = false;
|
|
3323
3295
|
if (agent.inbox.length > 0) {
|
|
3324
3296
|
agent.status = "starting";
|
|
3325
3297
|
const prompt = drainInboxToPrompt(agent);
|
|
@@ -3335,7 +3307,7 @@ function onTick(state, nowMs) {
|
|
|
3335
3307
|
const agents = { ...state.agents };
|
|
3336
3308
|
for (const id of Object.keys(agents)) {
|
|
3337
3309
|
const a = agents[id];
|
|
3338
|
-
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct");
|
|
3310
|
+
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "gated" && a.inbox.length > 0);
|
|
3339
3311
|
if (stalled) {
|
|
3340
3312
|
agents[id] = { ...a, status: "stopping", idleSince: null };
|
|
3341
3313
|
effects.push({ type: "terminate_stalled", agentId: id });
|
|
@@ -3359,6 +3331,7 @@ function freshAgent(agentId, caps) {
|
|
|
3359
3331
|
turnActive: false,
|
|
3360
3332
|
lastProgressAt: 0,
|
|
3361
3333
|
idleSince: null,
|
|
3334
|
+
resetting: false,
|
|
3362
3335
|
apm: createInitialApmGatedSteeringState()
|
|
3363
3336
|
};
|
|
3364
3337
|
}
|
|
@@ -3759,6 +3732,39 @@ class AgentProcessManager {
|
|
|
3759
3732
|
deliver(agentId, message) {
|
|
3760
3733
|
this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
|
|
3761
3734
|
}
|
|
3735
|
+
forgetSession(agentId) {
|
|
3736
|
+
this.resumeSessions.delete(agentId);
|
|
3737
|
+
this.liveSessions.delete(agentId);
|
|
3738
|
+
this.dispatch({ type: "reset_session", agentId });
|
|
3739
|
+
this.opts.timeline?.forgetSession(agentId);
|
|
3740
|
+
}
|
|
3741
|
+
enqueueRewake(agentId, message) {
|
|
3742
|
+
this.dispatch({ type: "rewake_after_reset", agentId, message });
|
|
3743
|
+
}
|
|
3744
|
+
markResetting(agentId) {
|
|
3745
|
+
this.dispatch({ type: "begin_reset", agentId });
|
|
3746
|
+
}
|
|
3747
|
+
async resetSession(agentId, opts) {
|
|
3748
|
+
this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
|
|
3749
|
+
this.forgetSession(agentId);
|
|
3750
|
+
this.markResetting(agentId);
|
|
3751
|
+
const status = this.state.agents[agentId]?.status;
|
|
3752
|
+
if (status === "idle") {
|
|
3753
|
+
try {
|
|
3754
|
+
this.deliver(agentId, { text: opts.rewakePrompt });
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
this.log.error("agent reset idle-branch spawn threw synchronously", {
|
|
3757
|
+
agentId,
|
|
3758
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3759
|
+
});
|
|
3760
|
+
this.dispatch({ type: "exit", agentId });
|
|
3761
|
+
throw err;
|
|
3762
|
+
}
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
this.enqueueRewake(agentId, { text: opts.rewakePrompt });
|
|
3766
|
+
await this.stop(agentId);
|
|
3767
|
+
}
|
|
3762
3768
|
start() {
|
|
3763
3769
|
if (this.tickTimer)
|
|
3764
3770
|
return;
|
|
@@ -3912,6 +3918,11 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
3912
3918
|
}
|
|
3913
3919
|
this.onRuntimeEvent(agentId, e, driver.id);
|
|
3914
3920
|
});
|
|
3921
|
+
session.on("stderr", (...args) => {
|
|
3922
|
+
const raw = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
|
|
3923
|
+
const text = raw.length > 2000 ? raw.slice(0, 2000) + "…" : raw;
|
|
3924
|
+
this.log.warn("runtime stderr", { agentId, runtime: driver.id, text });
|
|
3925
|
+
});
|
|
3915
3926
|
session.on("error", (...args) => {
|
|
3916
3927
|
const err = args[0];
|
|
3917
3928
|
const code = err?.code ?? "spawn_error";
|
|
@@ -4050,6 +4061,7 @@ class UnknownRuntimeError extends Error {
|
|
|
4050
4061
|
function defaultFormatUnreadNoticeText(notice) {
|
|
4051
4062
|
return `You have unread messages in channel ${notice.channel}.`;
|
|
4052
4063
|
}
|
|
4064
|
+
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.";
|
|
4053
4065
|
|
|
4054
4066
|
class AgentRouter {
|
|
4055
4067
|
opts;
|
|
@@ -4228,6 +4240,42 @@ class AgentRouter {
|
|
|
4228
4240
|
return;
|
|
4229
4241
|
}
|
|
4230
4242
|
break;
|
|
4243
|
+
case "agent:reset":
|
|
4244
|
+
this.log.info("agent:reset received", { agentId: cmd.agentId, launchId: cmd.launchId });
|
|
4245
|
+
try {
|
|
4246
|
+
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
4247
|
+
await this.opts.manager.resetSession(cmd.agentId, {
|
|
4248
|
+
runtimeConfig: cmd.config,
|
|
4249
|
+
launchId: cmd.launchId,
|
|
4250
|
+
rewakePrompt: REWAKE_PROMPT
|
|
4251
|
+
});
|
|
4252
|
+
this.running.add(cmd.agentId);
|
|
4253
|
+
this.scheduleReadyFrameResend();
|
|
4254
|
+
this.log.info("agent:reset ok", { agentId: cmd.agentId });
|
|
4255
|
+
} catch (err) {
|
|
4256
|
+
if (err instanceof UnknownRuntimeError) {
|
|
4257
|
+
const frame = {
|
|
4258
|
+
type: "session.error",
|
|
4259
|
+
code: "runtime_not_available",
|
|
4260
|
+
agentId: cmd.agentId,
|
|
4261
|
+
payload: {
|
|
4262
|
+
requested: err.requested ?? null,
|
|
4263
|
+
available: err.available
|
|
4264
|
+
}
|
|
4265
|
+
};
|
|
4266
|
+
await this.opts.channel.reportSessionError?.(frame);
|
|
4267
|
+
this.log.info("agent:reset error", {
|
|
4268
|
+
agentId: cmd.agentId,
|
|
4269
|
+
"error.code": "runtime_not_available"
|
|
4270
|
+
});
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
this.log.warn("agent:reset failed", {
|
|
4274
|
+
agentId: cmd.agentId,
|
|
4275
|
+
err: err instanceof Error ? err.message : String(err)
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
break;
|
|
4231
4279
|
case "agent:stop":
|
|
4232
4280
|
this.log.info("agent:stop received", { agentId: cmd.agentId });
|
|
4233
4281
|
try {
|
|
@@ -4386,7 +4434,7 @@ function parseBearer(authHeader) {
|
|
|
4386
4434
|
var DEFAULT_CAPABILITY_RESOLVER = (_method, pathname) => {
|
|
4387
4435
|
if (pathname.includes("/attachment"))
|
|
4388
4436
|
return "attach";
|
|
4389
|
-
if (pathname.includes("/send"))
|
|
4437
|
+
if (pathname.includes("/send") || pathname.includes("/reactAdd"))
|
|
4390
4438
|
return "send";
|
|
4391
4439
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
4392
4440
|
return "read";
|
|
@@ -4823,6 +4871,22 @@ function readRecentEntries(timelineDir, opts = {}) {
|
|
|
4823
4871
|
}
|
|
4824
4872
|
return entries;
|
|
4825
4873
|
}
|
|
4874
|
+
function appendEntry(timelineDir, entry, now = new Date) {
|
|
4875
|
+
const filename = filenameForDate(now);
|
|
4876
|
+
const filePath = join9(timelineDir, filename);
|
|
4877
|
+
const lockPath = lockPathFor(timelineDir, filename);
|
|
4878
|
+
if (!acquireLock(lockPath))
|
|
4879
|
+
return false;
|
|
4880
|
+
try {
|
|
4881
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
4882
|
+
`);
|
|
4883
|
+
return true;
|
|
4884
|
+
} catch {
|
|
4885
|
+
return false;
|
|
4886
|
+
} finally {
|
|
4887
|
+
releaseLock(lockPath);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4826
4890
|
function appendOrMergeEntry(timelineDir, entry, now = new Date) {
|
|
4827
4891
|
const filename = filenameForDate(now);
|
|
4828
4892
|
const filePath = join9(timelineDir, filename);
|
|
@@ -4837,7 +4901,7 @@ function appendOrMergeEntry(timelineDir, entry, now = new Date) {
|
|
|
4837
4901
|
}
|
|
4838
4902
|
if (lines.length > 0) {
|
|
4839
4903
|
const latest = JSON.parse(lines[lines.length - 1]);
|
|
4840
|
-
const mergeable = latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
4904
|
+
const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
4841
4905
|
if (mergeable) {
|
|
4842
4906
|
latest.messages = [...latest.messages, ...entry.messages];
|
|
4843
4907
|
lines[lines.length - 1] = JSON.stringify(latest);
|
|
@@ -4880,7 +4944,10 @@ function updateLatestEntry(timelineDir, updater, opts = {}) {
|
|
|
4880
4944
|
if (lines.length === 0)
|
|
4881
4945
|
continue;
|
|
4882
4946
|
const entries = lines.map((l) => JSON.parse(l));
|
|
4883
|
-
|
|
4947
|
+
const latest = entries[entries.length - 1];
|
|
4948
|
+
if (latest.system)
|
|
4949
|
+
return false;
|
|
4950
|
+
updater(latest);
|
|
4884
4951
|
const tmpPath = join9(timelineDir, `.${filename}.tmp`);
|
|
4885
4952
|
writeFileSync5(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
|
|
4886
4953
|
`) + `
|
|
@@ -4901,9 +4968,20 @@ function createTimelineEntry(fields) {
|
|
|
4901
4968
|
provider: fields.provider ?? null
|
|
4902
4969
|
};
|
|
4903
4970
|
}
|
|
4971
|
+
function createSystemEntry(type, time) {
|
|
4972
|
+
return {
|
|
4973
|
+
session_id: null,
|
|
4974
|
+
messages: [],
|
|
4975
|
+
agent_responses: [],
|
|
4976
|
+
provider: null,
|
|
4977
|
+
system: { type, time }
|
|
4978
|
+
};
|
|
4979
|
+
}
|
|
4904
4980
|
function findResumableSession(rows, provider) {
|
|
4905
4981
|
for (let i = rows.length - 1;i >= 0; i--) {
|
|
4906
4982
|
const e = rows[i];
|
|
4983
|
+
if (e.system?.type === "reset_session")
|
|
4984
|
+
return null;
|
|
4907
4985
|
if (!e.session_id)
|
|
4908
4986
|
continue;
|
|
4909
4987
|
if (provider && e.provider !== provider)
|
|
@@ -4934,11 +5012,33 @@ function createTimelineRecorder(opts) {
|
|
|
4934
5012
|
}), now());
|
|
4935
5013
|
},
|
|
4936
5014
|
appendResponseToLatest(agentId, text) {
|
|
4937
|
-
|
|
5015
|
+
const dir = dirFor(agentId);
|
|
5016
|
+
const updated = updateLatestEntry(dir, (e) => e.agent_responses.push(text), { now: now() });
|
|
5017
|
+
if (updated)
|
|
5018
|
+
return;
|
|
5019
|
+
try {
|
|
5020
|
+
mkdirSync6(dir, { recursive: true });
|
|
5021
|
+
} catch {}
|
|
5022
|
+
const entry = createTimelineEntry({
|
|
5023
|
+
messages: [],
|
|
5024
|
+
sessionId: sessionByAgent.get(agentId) ?? null,
|
|
5025
|
+
provider: opts.providerFor?.(agentId) ?? null
|
|
5026
|
+
});
|
|
5027
|
+
entry.agent_responses.push(text);
|
|
5028
|
+
appendEntry(dir, entry, now());
|
|
4938
5029
|
},
|
|
4939
5030
|
resumeSessionId(agentId, provider) {
|
|
4940
5031
|
const rows = readRecentEntries(dirFor(agentId), { now: now() });
|
|
4941
5032
|
return findResumableSession(rows, provider ?? undefined);
|
|
5033
|
+
},
|
|
5034
|
+
forgetSession(agentId) {
|
|
5035
|
+
const dir = dirFor(agentId);
|
|
5036
|
+
try {
|
|
5037
|
+
mkdirSync6(dir, { recursive: true });
|
|
5038
|
+
} catch {}
|
|
5039
|
+
sessionByAgent.delete(agentId);
|
|
5040
|
+
const stamp = now();
|
|
5041
|
+
appendEntry(dir, createSystemEntry("reset_session", stamp.toISOString()), stamp);
|
|
4942
5042
|
}
|
|
4943
5043
|
};
|
|
4944
5044
|
}
|