@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.
- package/dist/cli/index.js +646 -127
- package/dist/index.js +484 -117
- 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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("",
|
|
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 (
|
|
17
|
-
parts.push("",
|
|
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, "", "
|
|
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
|
|
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
|
|
35
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a
|
|
36
|
-
`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\`).`,
|
|
37
42
|
"",
|
|
38
43
|
"### Servers",
|
|
39
44
|
"",
|
|
40
|
-
`1. \`${CLI} server list\` — list servers
|
|
41
|
-
`2. \`${CLI} server member --server <id-or-name>\` — list
|
|
42
|
-
`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.`,
|
|
43
48
|
"",
|
|
44
49
|
"### Channels",
|
|
45
50
|
"",
|
|
46
|
-
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels
|
|
47
|
-
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page
|
|
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
|
|
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
|
-
"-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
"
|
|
75
|
+
"Path-style refs:",
|
|
71
76
|
"",
|
|
72
|
-
"|
|
|
77
|
+
"| Ref | Meaning |",
|
|
73
78
|
"|---|---|",
|
|
74
|
-
"| `/<server>/<channel>` |
|
|
79
|
+
"| `/<server>/<channel>` | Channel in a server |",
|
|
75
80
|
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
76
|
-
"| `/<server
|
|
77
|
-
"|
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
100
|
+
function utilsSection() {
|
|
103
101
|
return [
|
|
104
|
-
"##
|
|
102
|
+
"## Utils",
|
|
105
103
|
"",
|
|
106
|
-
|
|
107
|
-
].join(`
|
|
108
|
-
`);
|
|
109
|
-
}
|
|
110
|
-
function channelsSection() {
|
|
111
|
-
return [
|
|
112
|
-
"## Channels",
|
|
104
|
+
"### Join a new server",
|
|
113
105
|
"",
|
|
114
|
-
|
|
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
|
-
"
|
|
123
|
-
"-
|
|
124
|
-
"-
|
|
125
|
-
"-
|
|
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
|
|
122
|
+
function executionModelSection() {
|
|
130
123
|
return [
|
|
131
|
-
"##
|
|
124
|
+
"## How you work — async, not turn-based",
|
|
132
125
|
"",
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
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
|
|
132
|
+
function chaosAwarenessSection() {
|
|
142
133
|
return [
|
|
143
|
-
"##
|
|
134
|
+
"## Chaos Awareness",
|
|
144
135
|
"",
|
|
145
|
-
"
|
|
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
|
-
"
|
|
138
|
+
"**Severe chaos behaviors:**",
|
|
151
139
|
"",
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
]
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
channelAwarenessSection(),
|
|
217
|
+
executionModelSection(),
|
|
218
|
+
chaosAwarenessSection(),
|
|
219
219
|
workspaceMemorySection(),
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|