@alook/daemon 0.0.157 → 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.
Files changed (3) hide show
  1. package/dist/cli/index.js +646 -127
  2. package/dist/index.js +484 -117
  3. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -6,18 +6,22 @@ import * as path2 from "path";
6
6
  var CLI = "alook";
7
7
  function identitySection(config) {
8
8
  const parts = ["## Identity", ""];
9
- const introParts = ["You are a user operating in Alook."];
10
- if (config.agentName)
11
- introParts.push(`Your name is ${config.agentName}.`);
12
- parts.push(introParts.join(" "));
9
+ const name = config.agentName ?? "a member of the household";
10
+ const owner = config.ownerHandle;
11
+ if (owner) {
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
+ } else {
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
+ }
13
16
  if (config.agentHandle) {
14
- parts.push("", "Every account in Alook has a name plus a `#NNNN` number to make the handle unique. " + `Your handle is \`${config.agentHandle}\`. ` + "Speak with the name in conversation to make it natural; use the full handle when addressing (DM, mention on channel).");
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).");
15
18
  }
16
- if (config.ownerHandle) {
17
- parts.push("", `You are owned by \`${config.ownerHandle}\` anything private or sensitive about them ` + "(credentials, personal details, internal plans) belongs to them alone. Never share it with " + "anyone else, including other users, servers, or agents.");
19
+ if (owner) {
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.");
18
21
  }
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.");
19
23
  if (config.description) {
20
- parts.push("", "### Role", "", config.description, "", "This is a starting point, not fixed as you build context through interactions, capture how " + "your role has evolved in `./memory.md` (the Role text above isn't something you can edit directly).");
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).");
21
25
  }
22
26
  return parts.join(`
23
27
  `);
@@ -26,29 +30,31 @@ function cliCommandsSection() {
26
30
  return [
27
31
  "## CLI commands",
28
32
  "",
29
- `\`${CLI}\` is your command-line interface. Commands are grouped by category below; ` + `run \`${CLI} <command> -h\` on any of them for full usage and flags.`,
33
+ `\`${CLI}\` is your CLI. Run \`${CLI} <command> -h\` for full usage and flags.`,
30
34
  "",
31
35
  "### Messaging",
32
36
  "",
33
37
  `1. \`${CLI} inbox pull\` — fetch unread messages.`,
34
- `2. \`${CLI} message send\` — send a message to a channel, DM, or thread. ` + `Attach files with \`--attachment <id>\` (repeatable, order matters).`,
35
- `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a local file; ` + `returns an id. Feed that id into \`message send --attachment <id>\`. ` + `The id is stable across the pending→persisted lifecycle.`,
36
- `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download an attachment ` + `id from any message you have access to (or your own pending uploads).`,
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\`).`,
37
42
  "",
38
43
  "### Servers",
39
44
  "",
40
- `1. \`${CLI} server list\` — list servers you're a member of.`,
41
- `2. \`${CLI} server member --server <id-or-name>\` — list members of a server.`,
42
- `3. \`${CLI} server join --invite <link>\` — join a server via an invite link or token.`,
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.`,
43
48
  "",
44
49
  "### Channels",
45
50
  "",
46
- `1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels in a server.`,
47
- `2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page of messages.`,
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.`,
48
54
  "",
49
55
  "### Output format",
50
56
  "",
51
- `Every \`${CLI}\` command outputs a single JSON line (envelope):`,
57
+ `Every \`${CLI}\` command outputs one JSON line:`,
52
58
  '- Success: `{"success": { ... }}`',
53
59
  '- Error: `{"error": "message", "hint": "optional recovery hint"}`'
54
60
  ].join(`
@@ -60,58 +66,44 @@ function messagingSection() {
60
66
  "",
61
67
  "### Sending & receiving",
62
68
  "",
63
- "- Send a reply two options depending on length:",
64
- ` - Short: \`${CLI} message send --target <ref> --text "brief reply"\``,
65
- ` - Long&Complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\``,
66
- "- 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\`.`,
67
72
  "",
68
73
  "### Channel refs & addressing",
69
74
  "",
70
- "Channels and messages are addressed with path-style refs:",
75
+ "Path-style refs:",
71
76
  "",
72
- "| Channel Ref | Meaning |",
77
+ "| Ref | Meaning |",
73
78
  "|---|---|",
74
- "| `/<server>/<channel>` | A channel in a server |",
79
+ "| `/<server>/<channel>` | Channel in a server |",
75
80
  "| `/<server>/<channel>/#N` | Thread rooted at message #N |",
76
- "| `/<server>` | A server, with no specific channel |",
77
- "| `/.dm/<peer>` | A DM with another user/agent (peer = handle, `name#0042`) |",
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`) |",
78
84
  "| `/.dm/<peer>#N` | Message #N in a DM |",
79
- "| `/.dm/<peer>/#N` | Thread in a DM |",
80
85
  "",
81
- "Use the `channel` field from received messages as the `--target` when replying.",
82
- "To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
83
- "These same refs also work inline, inside a message's `--text`/`--file` body — not just as `--target`. " + "Type a ref (server, channel, or thread form, from the table above) directly into your message text as " + "a standalone token, preceded by a space or at the start of a line, and it renders as a clickable link " + "for human readers in the web client. **Do not wrap it in backticks or a code block** — that renders it " + "as literal text instead of a link. Use this to cross-reference other servers/channels/threads naturally " + "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.",
84
87
  "",
85
88
  "### Message shape",
86
89
  "",
87
- `When you call \`${CLI} inbox pull\`, you receive messages as JSON objects:`,
90
+ "Pulled messages:",
88
91
  "",
89
92
  "```json",
90
93
  '{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
91
94
  "```",
92
95
  "",
93
- "Fields:",
94
- "- `seq` — per-channel sequence number (`#N`). Identifies a message within its channel.",
95
- "- `channel` — the path ref of the channel/DM. Reuse as `--target` when replying.",
96
- "- `sender` — handle (`@name#0042`) of who sent it.",
97
- "- `content.text` — the message body.",
98
- "- `time` — ISO-8601 timestamp."
96
+ "`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply."
99
97
  ].join(`
100
98
  `);
101
99
  }
102
- function serversSection() {
100
+ function utilsSection() {
103
101
  return [
104
- "## Servers",
102
+ "## Utils",
105
103
  "",
106
- `If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces an owner-only check for you — it only accepts an invite your owner created, and " + "rejects anything else with a clear reason. So it's always safe to attempt a join without first " + "reasoning about whose link it is."
107
- ].join(`
108
- `);
109
- }
110
- function channelsSection() {
111
- return [
112
- "## Channels",
104
+ "### Join a new server",
113
105
  "",
114
- `\`${CLI} channel list\`'s items are \`{ref, name, type}\` \`ref\` is directly reusable as ` + "`--channel`/`--target` on every other command, no separate id lookup needed. `type` is " + '`"text"` or `"forum"` (a forum channel\'s "messages" are really its top-level posts).'
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."
115
107
  ].join(`
116
108
  `);
117
109
  }
@@ -119,51 +111,41 @@ function criticalRulesSection() {
119
111
  return [
120
112
  "## Critical rules",
121
113
  "",
122
- "- Do not expose tokens, keys, or secrets in any message or channel; redact " + "credential-like strings from tool output before sharing.",
123
- "- You never handle credentials directly — every `alook` command is already " + "authenticated for you. If a command fails with an auth-related error, stop " + "and report it; do not go looking for alternate tokens, keys, or environment " + "variables to work around it.",
124
- "- **Channel alignment**: you cannot send to a channel with unread messages. If send " + `fails with a "channel not aligned" error, run \`${CLI} inbox pull\` first, then resend.`,
125
- "- Finish the work a message asks for before you stop; don't leave a request half-handled."
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."
126
119
  ].join(`
127
120
  `);
128
121
  }
129
- function startupSequenceSection() {
122
+ function executionModelSection() {
130
123
  return [
131
- "## On wake",
124
+ "## How you work — async, not turn-based",
132
125
  "",
133
- "Each time you're woken up:",
134
- "1. Acknowledge any message already in front of you.",
135
- "2. Read `./memory.md` + latest context timeline to restore state.",
136
- `3. If notified of unread messages, run \`${CLI} inbox pull\` to fetch them.`,
137
- "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."
138
129
  ].join(`
139
130
  `);
140
131
  }
141
- function communicationStyleSection() {
132
+ function chaosAwarenessSection() {
142
133
  return [
143
- "## Communication style",
134
+ "## Chaos Awareness",
144
135
  "",
145
- "Your reasoning is invisible to others — keep them in the loop:",
146
- "- Acknowledge tasks before starting; give a one-line plan.",
147
- "- Post brief updates at milestones (one sentence each).",
148
- "- Summarize outcomes when done.",
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.",
149
137
  "",
150
- "### Etiquette",
138
+ "**Severe chaos behaviors:**",
151
139
  "",
152
- "- Don't jump into a conversation unless @mentioned or directly addressed.",
153
- "- Let the person who did the work report on it.",
154
- "- Before going idle, unblock anyone waiting on you.",
155
- "- Don't narrate inactivity only speak when you have something actionable.",
156
- "- Talk in the same language as the sender."
157
- ].join(`
158
- `);
159
- }
160
- function channelAwarenessSection() {
161
- return [
162
- "## Channel awareness",
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.",
163
147
  "",
164
- "- Reply where the message came from same channel or thread.",
165
- "- Post results in the channel that owns the topic.",
166
- "- 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`."
167
149
  ].join(`
168
150
  `);
169
151
  }
@@ -171,53 +153,71 @@ function workspaceMemorySection() {
171
153
  return [
172
154
  "## Workspace & memory",
173
155
  "",
174
- "Your cwd is a persistent workspace that survives across sessions.",
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.",
157
+ "",
158
+ "### Something only you know",
159
+ "",
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.",
161
+ "",
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.",
163
+ "",
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.",
175
165
  "",
176
166
  "### memory.md",
177
167
  "",
178
- "Read `./memory.md` first on every wake. It holds durable facts (user profile, project " + "map, pointers to detail files). Keep each entry short (one sentence, <140 chars).",
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".',
179
169
  "",
180
170
  "### experiences/",
181
171
  "",
182
- "For longer rules, workflows, or conditional procedures, write to `experiences/[NAME].md` " + 'and add a one-line index pointer in `./memory.md` (e.g. "read experiences/deploy.md ' + 'when deploying"). Use this for anything too specific or long for memory.md itself.',
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").',
183
173
  "",
184
174
  "Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
185
175
  "",
176
+ "### Becoming yourself",
177
+ "",
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:",
179
+ "",
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.",
185
+ "",
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.",
187
+ "",
186
188
  "### Context timeline",
187
189
  "",
188
- "`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of everything you did, by day. " + "This is your authoritative history. After compaction, read here to resume."
189
- ].join(`
190
- `);
191
- }
192
- function messageNotificationSection(lifecycleKind) {
193
- if (lifecycleKind === "per_turn") {
194
- return [
195
- "## Message notifications",
196
- "",
197
- "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."
198
- ].join(`
199
- `);
200
- }
201
- return [
202
- "## Message notifications",
190
+ "`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative " + "history. After compaction, read here to resume.",
191
+ "",
192
+ "### todo.md",
193
+ "",
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.",
195
+ "",
196
+ "Example:",
197
+ "",
198
+ "```md",
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"}',
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"}',
201
+ "```",
203
202
  "",
204
- "Your process stays alive across turns. Alook may inject a lightweight inbox notice " + "mid-turn (no message bodies included) — a notification without bodies still means " + "messages are waiting, not that there's nothing to do. " + "Pulling and acknowledging them IS time-sensitive: at the next natural breakpoint, run " + `\`${CLI} inbox pull\` and send a brief ack so the sender isn't left hanging. Whether to ` + "drop your current work and dive into the new request right away is your call — judge it " + "by priority. If you decide the new work can wait, that's a judgment call to report " + 'honestly — never conclude "no work pending" from a content-free notice alone.'
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.",
204
+ "",
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."
205
208
  ].join(`
206
209
  `);
207
210
  }
208
- function buildCliSystemPrompt(config, opts) {
211
+ function buildCliSystemPrompt(config, _opts) {
209
212
  const sections = [
210
213
  identitySection(config),
211
214
  cliCommandsSection(),
212
215
  messagingSection(),
213
- serversSection(),
214
- channelsSection(),
215
216
  criticalRulesSection(),
216
- startupSequenceSection(),
217
- communicationStyleSection(),
218
- channelAwarenessSection(),
217
+ executionModelSection(),
218
+ chaosAwarenessSection(),
219
219
  workspaceMemorySection(),
220
- messageNotificationSection(opts.lifecycleKind)
220
+ utilsSection()
221
221
  ];
222
222
  return sections.filter((s) => s && s.length > 0).join(`
223
223
 
@@ -3119,11 +3119,32 @@ function reduceManager(state, event) {
3119
3119
  a.turnActive = true;
3120
3120
  a.lastProgressAt = event.nowMs;
3121
3121
  a.idleSince = null;
3122
+ if (a.resetting)
3123
+ a.resetting = false;
3122
3124
  });
3123
3125
  case "session":
3124
3126
  return mutate(state, event.agentId, (a) => {
3125
3127
  a.sessionId = event.sessionId;
3126
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
+ });
3127
3148
  case "progress":
3128
3149
  return mutate(state, event.agentId, (a) => {
3129
3150
  a.lastProgressAt = event.nowMs;
@@ -3144,6 +3165,11 @@ function onWake(state, agentId, message) {
3144
3165
  if (!agent) {
3145
3166
  return { state, effects: [] };
3146
3167
  }
3168
+ if (agent.resetting && agent.status !== "idle") {
3169
+ agent.inbox = [...agent.inbox, message];
3170
+ agent.idleSince = null;
3171
+ return commit(state, agent, []);
3172
+ }
3147
3173
  agent.inbox = [...agent.inbox, message];
3148
3174
  agent.idleSince = null;
3149
3175
  if (agent.status === "idle") {
@@ -3201,7 +3227,7 @@ function onRuntimeSignal(state, agentId, kind) {
3201
3227
  if (!existing)
3202
3228
  return { state, effects: [] };
3203
3229
  const agent = clone(existing);
3204
- 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";
3205
3231
  if (!isGatedActive) {
3206
3232
  agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
3207
3233
  return commit(state, agent, []);
@@ -3264,6 +3290,8 @@ function onExit(state, agentId) {
3264
3290
  return { state, effects: [] };
3265
3291
  const agent = clone(existing);
3266
3292
  agent.turnActive = false;
3293
+ if (agent.resetting)
3294
+ agent.resetting = false;
3267
3295
  if (agent.inbox.length > 0) {
3268
3296
  agent.status = "starting";
3269
3297
  const prompt = drainInboxToPrompt(agent);
@@ -3279,7 +3307,7 @@ function onTick(state, nowMs) {
3279
3307
  const agents = { ...state.agents };
3280
3308
  for (const id of Object.keys(agents)) {
3281
3309
  const a = agents[id];
3282
- 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);
3283
3311
  if (stalled) {
3284
3312
  agents[id] = { ...a, status: "stopping", idleSince: null };
3285
3313
  effects.push({ type: "terminate_stalled", agentId: id });
@@ -3303,6 +3331,7 @@ function freshAgent(agentId, caps) {
3303
3331
  turnActive: false,
3304
3332
  lastProgressAt: 0,
3305
3333
  idleSince: null,
3334
+ resetting: false,
3306
3335
  apm: createInitialApmGatedSteeringState()
3307
3336
  };
3308
3337
  }
@@ -3464,8 +3493,189 @@ function createLogger(options = {}) {
3464
3493
  };
3465
3494
  }
3466
3495
 
3496
+ // src/util/localTime.ts
3497
+ function localISOString(now) {
3498
+ const tzOffset = -now.getTimezoneOffset();
3499
+ const sign = tzOffset >= 0 ? "+" : "-";
3500
+ const abs = Math.abs(tzOffset);
3501
+ const hh = String(Math.floor(abs / 60)).padStart(2, "0");
3502
+ const mm = String(abs % 60).padStart(2, "0");
3503
+ const y = now.getFullYear();
3504
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
3505
+ const d = String(now.getDate()).padStart(2, "0");
3506
+ const h = String(now.getHours()).padStart(2, "0");
3507
+ const mi = String(now.getMinutes()).padStart(2, "0");
3508
+ const s = String(now.getSeconds()).padStart(2, "0");
3509
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
3510
+ return `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}${sign}${hh}:${mm}`;
3511
+ }
3512
+ function nowLocalISO() {
3513
+ return localISOString(new Date);
3514
+ }
3515
+
3467
3516
  // src/manager/managerRuntime.ts
3468
3517
  var THINKING_MAX_BYTES = 4096;
3518
+ var MAX_TARGET_CODE_UNITS = 200;
3519
+ function canonicalToolName(rawName) {
3520
+ const lower = rawName.toLowerCase();
3521
+ switch (lower) {
3522
+ case "bash":
3523
+ case "shell":
3524
+ return "bash";
3525
+ case "read":
3526
+ return "read";
3527
+ case "edit":
3528
+ case "multiedit":
3529
+ case "file_change":
3530
+ return "edit";
3531
+ case "write":
3532
+ return "write";
3533
+ case "grep":
3534
+ return "grep";
3535
+ case "glob":
3536
+ return "glob";
3537
+ case "find":
3538
+ return "find";
3539
+ case "ls":
3540
+ return "ls";
3541
+ case "notebookedit":
3542
+ case "notebook_edit":
3543
+ return "notebook_edit";
3544
+ case "websearch":
3545
+ case "web_search":
3546
+ return "web_search";
3547
+ case "webfetch":
3548
+ case "web_fetch":
3549
+ return "web_fetch";
3550
+ case "todowrite":
3551
+ case "todo_write":
3552
+ return "todo_write";
3553
+ default:
3554
+ return lower;
3555
+ }
3556
+ }
3557
+ function classify(canonicalName) {
3558
+ switch (canonicalName) {
3559
+ case "bash":
3560
+ return "shell";
3561
+ case "read":
3562
+ case "edit":
3563
+ case "write":
3564
+ case "ls":
3565
+ case "notebook_edit":
3566
+ return "file_target";
3567
+ case "grep":
3568
+ case "glob":
3569
+ case "find":
3570
+ return "pattern";
3571
+ default:
3572
+ return "fallthrough";
3573
+ }
3574
+ }
3575
+ function coerceInputRecord(input) {
3576
+ if (typeof input === "string") {
3577
+ try {
3578
+ const parsed = JSON.parse(input);
3579
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3580
+ return parsed;
3581
+ }
3582
+ } catch {
3583
+ return;
3584
+ }
3585
+ return;
3586
+ }
3587
+ if (!input || typeof input !== "object" || Array.isArray(input))
3588
+ return;
3589
+ return input;
3590
+ }
3591
+ function pickCommandString(input) {
3592
+ const rec = coerceInputRecord(input);
3593
+ if (!rec)
3594
+ return;
3595
+ if (typeof rec.command === "string")
3596
+ return rec.command;
3597
+ if (Array.isArray(rec.command))
3598
+ return rec.command.filter((v) => typeof v === "string").join(" ");
3599
+ return;
3600
+ }
3601
+ function pickFileTarget(input) {
3602
+ const rec = coerceInputRecord(input);
3603
+ if (!rec)
3604
+ return;
3605
+ if (typeof rec.file_path === "string")
3606
+ return rec.file_path;
3607
+ if (typeof rec.path === "string")
3608
+ return rec.path;
3609
+ if (typeof rec.notebook_path === "string")
3610
+ return rec.notebook_path;
3611
+ return;
3612
+ }
3613
+ function pickPatternTarget(input) {
3614
+ const rec = coerceInputRecord(input);
3615
+ if (!rec)
3616
+ return;
3617
+ if (typeof rec.pattern === "string")
3618
+ return rec.pattern;
3619
+ if (typeof rec.query === "string")
3620
+ return rec.query;
3621
+ if (typeof rec.path === "string")
3622
+ return rec.path;
3623
+ return;
3624
+ }
3625
+ function pickFallthroughTarget(input) {
3626
+ const rec = coerceInputRecord(input);
3627
+ if (!rec)
3628
+ return;
3629
+ if (typeof rec.url === "string")
3630
+ return rec.url;
3631
+ if (typeof rec.query === "string")
3632
+ return rec.query;
3633
+ if (typeof rec.path === "string")
3634
+ return rec.path;
3635
+ if (typeof rec.name === "string")
3636
+ return rec.name;
3637
+ return;
3638
+ }
3639
+ function isAlookShellInvocation(command) {
3640
+ if (!command)
3641
+ return false;
3642
+ return /^alook(\s|$)/.test(command.trimStart());
3643
+ }
3644
+ function truncateTargetToCodeUnits(s) {
3645
+ if (s.length <= MAX_TARGET_CODE_UNITS)
3646
+ return s;
3647
+ let end = MAX_TARGET_CODE_UNITS - 1;
3648
+ const cu = s.charCodeAt(end - 1);
3649
+ if (cu >= 55296 && cu <= 56319)
3650
+ end -= 1;
3651
+ return s.slice(0, end) + "…";
3652
+ }
3653
+ function extractToolAudit(rawName, rawInput) {
3654
+ const name = canonicalToolName(rawName);
3655
+ const cls = classify(name);
3656
+ if (cls === "shell") {
3657
+ const raw = pickCommandString(rawInput);
3658
+ if (isAlookShellInvocation(raw)) {
3659
+ return { name, suppressed: true };
3660
+ }
3661
+ const firstLine = typeof raw === "string" ? raw.split(`
3662
+ `).map((s) => s.trim()).find((s) => s.length > 0) : undefined;
3663
+ if (!firstLine)
3664
+ return { name, suppressed: false };
3665
+ return { name, target: truncateTargetToCodeUnits(firstLine), suppressed: false };
3666
+ }
3667
+ let target;
3668
+ if (cls === "file_target")
3669
+ target = pickFileTarget(rawInput);
3670
+ else if (cls === "pattern")
3671
+ target = pickPatternTarget(rawInput);
3672
+ else
3673
+ target = pickFallthroughTarget(rawInput);
3674
+ if (typeof target !== "string" || target.length === 0) {
3675
+ return { name, suppressed: false };
3676
+ }
3677
+ return { name, target: truncateTargetToCodeUnits(target), suppressed: false };
3678
+ }
3469
3679
  function truncateThinking(text) {
3470
3680
  const chars = [...text].length;
3471
3681
  const buf = Buffer.from(text, "utf8");
@@ -3497,6 +3707,7 @@ class AgentProcessManager {
3497
3707
  tickIntervalMs: 5000,
3498
3708
  staleThresholdMs: 120000,
3499
3709
  idleTimeoutMs: 300000,
3710
+ stampWakePromptTime: false,
3500
3711
  ...opts
3501
3712
  };
3502
3713
  this.now = opts.now ?? (() => Date.now());
@@ -3521,6 +3732,39 @@ class AgentProcessManager {
3521
3732
  deliver(agentId, message) {
3522
3733
  this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
3523
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
+ }
3524
3768
  start() {
3525
3769
  if (this.tickTimer)
3526
3770
  return;
@@ -3589,6 +3833,9 @@ class AgentProcessManager {
3589
3833
 
3590
3834
  ${this.opts.wakePromptFooter}` : text;
3591
3835
  }
3836
+ stampNow(text) {
3837
+ return this.opts.stampWakePromptTime ? `[${nowLocalISO()}] ${text}` : text;
3838
+ }
3592
3839
  applyEffect(effect) {
3593
3840
  switch (effect.type) {
3594
3841
  case "spawn":
@@ -3596,7 +3843,7 @@ ${this.opts.wakePromptFooter}` : text;
3596
3843
  break;
3597
3844
  case "send": {
3598
3845
  const session = this.sessions.get(effect.agentId);
3599
- session?.send({ text: this.withFooter(effect.text), mode: effect.mode });
3846
+ session?.send({ text: this.stampNow(this.withFooter(effect.text)), mode: effect.mode });
3600
3847
  this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
3601
3848
  break;
3602
3849
  }
@@ -3671,6 +3918,11 @@ ${this.opts.wakePromptFooter}` : text;
3671
3918
  }
3672
3919
  this.onRuntimeEvent(agentId, e, driver.id);
3673
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
+ });
3674
3926
  session.on("error", (...args) => {
3675
3927
  const err = args[0];
3676
3928
  const code = err?.code ?? "spawn_error";
@@ -3687,7 +3939,8 @@ ${this.opts.wakePromptFooter}` : text;
3687
3939
  this.activeSpawnState.delete(agentId);
3688
3940
  this.dispatch({ type: "exit", agentId });
3689
3941
  });
3690
- Promise.resolve(session.start({ text: prompt, sessionId: ctx.config.sessionId })).then(() => {
3942
+ const stampedPrompt = this.stampNow(prompt);
3943
+ Promise.resolve(session.start({ text: stampedPrompt, sessionId: ctx.config.sessionId })).then(() => {
3691
3944
  if (this.sessions.get(agentId) !== session)
3692
3945
  return;
3693
3946
  this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
@@ -3729,11 +3982,13 @@ ${this.opts.wakePromptFooter}` : text;
3729
3982
  } else {
3730
3983
  this.flushThinkingAudit(agentId);
3731
3984
  if (ev.kind === "tool_call" && typeof ev.name === "string") {
3732
- if (ev.name !== "Bash") {
3985
+ const audit = extractToolAudit(ev.name, ev.input);
3986
+ if (!audit.suppressed) {
3987
+ const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
3733
3988
  try {
3734
3989
  this.opts.onBotAuditEvent(agentId, {
3735
3990
  kind: "tool_call",
3736
- payload: { name: ev.name }
3991
+ payload
3737
3992
  }, {
3738
3993
  sessionId: this.liveSessions.get(agentId) ?? null,
3739
3994
  launchId: this.launchIds.get(agentId) ?? null
@@ -3806,6 +4061,7 @@ class UnknownRuntimeError extends Error {
3806
4061
  function defaultFormatUnreadNoticeText(notice) {
3807
4062
  return `You have unread messages in channel ${notice.channel}.`;
3808
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.";
3809
4065
 
3810
4066
  class AgentRouter {
3811
4067
  opts;
@@ -3984,6 +4240,42 @@ class AgentRouter {
3984
4240
  return;
3985
4241
  }
3986
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;
3987
4279
  case "agent:stop":
3988
4280
  this.log.info("agent:stop received", { agentId: cmd.agentId });
3989
4281
  try {
@@ -4142,7 +4434,7 @@ function parseBearer(authHeader) {
4142
4434
  var DEFAULT_CAPABILITY_RESOLVER = (_method, pathname) => {
4143
4435
  if (pathname.includes("/attachment"))
4144
4436
  return "attach";
4145
- if (pathname.includes("/send"))
4437
+ if (pathname.includes("/send") || pathname.includes("/reactAdd"))
4146
4438
  return "send";
4147
4439
  if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
4148
4440
  return "read";
@@ -4579,6 +4871,22 @@ function readRecentEntries(timelineDir, opts = {}) {
4579
4871
  }
4580
4872
  return entries;
4581
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
+ }
4582
4890
  function appendOrMergeEntry(timelineDir, entry, now = new Date) {
4583
4891
  const filename = filenameForDate(now);
4584
4892
  const filePath = join9(timelineDir, filename);
@@ -4593,7 +4901,7 @@ function appendOrMergeEntry(timelineDir, entry, now = new Date) {
4593
4901
  }
4594
4902
  if (lines.length > 0) {
4595
4903
  const latest = JSON.parse(lines[lines.length - 1]);
4596
- 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;
4597
4905
  if (mergeable) {
4598
4906
  latest.messages = [...latest.messages, ...entry.messages];
4599
4907
  lines[lines.length - 1] = JSON.stringify(latest);
@@ -4636,7 +4944,10 @@ function updateLatestEntry(timelineDir, updater, opts = {}) {
4636
4944
  if (lines.length === 0)
4637
4945
  continue;
4638
4946
  const entries = lines.map((l) => JSON.parse(l));
4639
- updater(entries[entries.length - 1]);
4947
+ const latest = entries[entries.length - 1];
4948
+ if (latest.system)
4949
+ return false;
4950
+ updater(latest);
4640
4951
  const tmpPath = join9(timelineDir, `.${filename}.tmp`);
4641
4952
  writeFileSync5(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
4642
4953
  `) + `
@@ -4657,9 +4968,20 @@ function createTimelineEntry(fields) {
4657
4968
  provider: fields.provider ?? null
4658
4969
  };
4659
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
+ }
4660
4980
  function findResumableSession(rows, provider) {
4661
4981
  for (let i = rows.length - 1;i >= 0; i--) {
4662
4982
  const e = rows[i];
4983
+ if (e.system?.type === "reset_session")
4984
+ return null;
4663
4985
  if (!e.session_id)
4664
4986
  continue;
4665
4987
  if (provider && e.provider !== provider)
@@ -4690,11 +5012,33 @@ function createTimelineRecorder(opts) {
4690
5012
  }), now());
4691
5013
  },
4692
5014
  appendResponseToLatest(agentId, text) {
4693
- updateLatestEntry(dirFor(agentId), (e) => e.agent_responses.push(text), { now: now() });
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());
4694
5029
  },
4695
5030
  resumeSessionId(agentId, provider) {
4696
5031
  const rows = readRecentEntries(dirFor(agentId), { now: now() });
4697
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);
4698
5042
  }
4699
5043
  };
4700
5044
  }
@@ -4864,6 +5208,16 @@ function deriveAuditLogSubcommand(pathname) {
4864
5208
  return null;
4865
5209
  return sub;
4866
5210
  }
5211
+ function emitImplicitTypingStopOnSend(args) {
5212
+ if (args.subcommand !== "send")
5213
+ return;
5214
+ const emit = args.reportAgentTypingStop;
5215
+ if (!emit)
5216
+ return;
5217
+ for (const dmConversationId of args.typingTracker.snapshot(args.agentId)) {
5218
+ emit({ agentId: args.agentId, dmConversationId });
5219
+ }
5220
+ }
4867
5221
  async function createDaemon(opts) {
4868
5222
  const log = opts.logger ?? createLogger({ header: "@alook/daemon" });
4869
5223
  const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${homedir2()}/.alook`) + "/daemon";
@@ -4884,6 +5238,7 @@ async function createDaemon(opts) {
4884
5238
  event
4885
5239
  });
4886
5240
  };
5241
+ const typingTracker = createTypingScopeTracker();
4887
5242
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
4888
5243
  const proxy = await startCredentialProxy(broker, {
4889
5244
  onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
@@ -4896,10 +5251,15 @@ async function createDaemon(opts) {
4896
5251
  kind: "cli_invocation",
4897
5252
  payload: { subcommand }
4898
5253
  }, context);
5254
+ emitImplicitTypingStopOnSend({
5255
+ subcommand,
5256
+ agentId,
5257
+ typingTracker,
5258
+ reportAgentTypingStop: channelRef?.reportAgentTypingStop?.bind(channelRef)
5259
+ });
4899
5260
  }
4900
5261
  });
4901
5262
  const enrolledKeys = new Map;
4902
- const typingTracker = createTypingScopeTracker();
4903
5263
  const typingHeartbeats = new Map;
4904
5264
  const TYPING_HEARTBEAT_MS = 5000;
4905
5265
  function stopTypingHeartbeat(agentId) {
@@ -5106,6 +5466,7 @@ async function createDaemon(opts) {
5106
5466
  sdkDriverDepsFor: (ctx) => createPiSdkDriverDeps(ctx),
5107
5467
  timeline: timeline2,
5108
5468
  wakePromptFooter: "Use `alook inbox pull` to read your messages, then reply with `alook message send`.",
5469
+ stampWakePromptTime: true,
5109
5470
  logger: log.child("manager")
5110
5471
  });
5111
5472
  managerRef = manager;
@@ -5167,6 +5528,7 @@ async function createDaemon(opts) {
5167
5528
  }
5168
5529
  export {
5169
5530
  truncateThinking,
5531
+ truncateTargetToCodeUnits,
5170
5532
  startCredentialProxy,
5171
5533
  scrubRuntimeErrorDiagnosticText,
5172
5534
  runtimeErrorReason,
@@ -5193,14 +5555,18 @@ export {
5193
5555
  projectApmHeldFreshnessEnvelope,
5194
5556
  projectAgentInboxSnapshot,
5195
5557
  planAgentInboxSideEffect,
5558
+ pickCommandString,
5196
5559
  normalizeInboxVisibleMessage,
5197
5560
  listRuntimeIds,
5198
5561
  isRuntimeAuthActionRequiredText,
5562
+ isAlookShellInvocation,
5199
5563
  hasConfiguredCodexHome,
5200
5564
  getDriver,
5201
5565
  getAvailableRuntimes,
5202
5566
  formatInboxMessageTarget,
5567
+ extractToolAudit,
5203
5568
  extractHttpStatus,
5569
+ emitImplicitTypingStopOnSend,
5204
5570
  detectRuntimes,
5205
5571
  descriptorFromDriver,
5206
5572
  deriveCliFallbackCandidates,
@@ -5214,6 +5580,7 @@ export {
5214
5580
  codexSessionRootCandidates,
5215
5581
  classifyRuntimeErrorAction,
5216
5582
  classifyRuntimeError,
5583
+ canonicalToolName,
5217
5584
  buildRuntimeErrorDiagnosticEnvelope,
5218
5585
  buildApmFreshnessDecisionProducerFactId,
5219
5586
  UnknownRuntimeError,