Skip to content
161 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1import { test } from "node:test";
2import assert from "node:assert/strict";
3
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.4import { MAX_HOPS, type RecentMessage, addressedAgents, chainFor, deliveries, delivery, handOffPlace, handOffRefusal, personalAgentRefusal, wakeable } from "./delivery.ts";
Chat and workspace agents: channels, DMs and named agents you talk to5
6const ship = { id: "a1", handle: "ship" };
7const docs = { id: "a2", handle: "docs" };
8
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.9test("a person's direct message with nothing said yet wakes every agent in it, at hop 0", () => {
Chat and workspace agents: channels, DMs and named agents you talk to10 assert.deepEqual(deliveries({ author: "user:u", hops: 3, channelKind: "dm", agents: [ship, docs], mentioned: [] }), [
11 { agent_id: "a1", hops: 0 },
12 { agent_id: "a2", hops: 0 },
13 ]);
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.14 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: [], recent: [] }), [
15 { agent_id: "a1", hops: 0 },
16 { agent_id: "a2", hops: 0 },
17 ]);
18 // With one agent there is nobody else it could be for.
19 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship], mentioned: [], recent: [{ author: "user:u", mentioned: ["docs"] }] }), [{ agent_id: "a1", hops: 0 }]);
20});
21
22const mike = { id: "a_mike", handle: "mike" };
23const g1t = { id: "a_g1t", handle: "g1t" };
24const says = (author: string, ...mentioned: string[]): RecentMessage => ({ author, mentioned });
25
26test("in a group DM, an unaddressed message goes to the agent the person is talking with", () => {
27 // The conversation as it happened: Chase, Mike the recruiter and @g1t in one group message.
28 const transcript = [
29 says("user:chase"), // "Are you able to generate PDFs now?"
30 says("agent:a_mike"),
31 says("agent:a_g1t"),
32 says("user:chase", "mike"), // "@Mike Make a PDF of that example resume for me"
33 says("agent:a_mike"),
34 ];
35 // "Let's take that template and make a fake resume for some fake person": Mike was helping.
36 assert.deepEqual(deliveries({ author: "user:chase", hops: 0, channelKind: "dm", agents: [mike, g1t], mentioned: [], recent: transcript }), [{ agent_id: "a_mike", hops: 0 }]);
37 // The first question, with nothing before it, went to both; so does a follow-up to both their answers.
38 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", []), [mike, g1t]);
39 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", transcript.slice(0, 3)), [mike, g1t]);
40 // A mention still goes only to those named, whoever was talking.
41 assert.deepEqual(deliveries({ author: "user:chase", hops: 0, channelKind: "dm", agents: [mike, g1t], mentioned: ["g1t"], recent: transcript }), [{ agent_id: "a_g1t", hops: 0 }]);
42});
43
44test("who is addressed: the last exchange, else the person's last mention, else everyone", () => {
45 // Mentioned but not yet answered: the mention stands, through the person's own follow-ups.
46 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "Mike"), says("user:chase")]), [mike]);
47 // Answered by a colleague after the mention (a hand-off here, say): the colleague has the conversation now.
48 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike", "g1t"), says("user:chase"), says("agent:a_g1t")]), [g1t]);
49 // Both answered the last message: both are in the exchange, in the conversation's order.
50 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "g1t", "mike"), says("agent:a_g1t"), says("agent:a_mike")]), [mike, g1t]);
51 // A thank-you in between changes nothing: the person's last mention still says who.
52 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike"), says("user:chase"), says("user:chase")]), [mike]);
53 // Another person's message ends the exchange without deciding it; only this person's mentions do.
54 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "mike"), says("agent:a_mike"), says("user:bo", "g1t")]), [mike]);
55 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:bo", "g1t"), says("agent:a_g1t")]), [g1t], "whoever answered last, whoever they answered");
56 // A second message before anyone answers the first goes where the first did.
57 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:bo", "g1t"), says("agent:a_g1t"), says("user:chase")]), [g1t]);
58 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase"), says("agent:a_mike"), says("user:chase")]), [mike], "only Mike answered the first");
59 // Mentions of agents that aren't here, and messages of agents that left, decide nothing.
60 assert.deepEqual(addressedAgents([mike, g1t], "user:chase", [says("user:chase", "sam"), says("agent:a_gone")]), [mike, g1t]);
61 assert.deepEqual(addressedAgents([mike], "user:chase", [says("user:chase", "g1t")]), [mike], "one agent: nothing to decide");
Chat and workspace agents: channels, DMs and named agents you talk to62});
63
Merge branch 'worktree-agent-a1398e81ad1a64c5f'64test("in a group direct message, a person who mentions some of its agents wakes only those", () => {
65 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["docs"] }), [{ agent_id: "a2", hops: 0 }]);
66 // Mentioning someone who isn't in it is no mention of its agents: all of them answer.
67 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["mike"] }), [
68 { agent_id: "a1", hops: 0 },
69 { agent_id: "a2", hops: 0 },
70 ]);
71});
72
Chat and workspace agents: channels, DMs and named agents you talk to73test("in a channel, a person wakes only the agent members they mention", () => {
74 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, docs], mentioned: [] }), []);
75 assert.deepEqual(
76 deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, docs], mentioned: ["Ship", "nobody"] }),
77 [{ agent_id: "a1", hops: 0 }],
78 );
79});
80
Merge branch 'worktree-agent-a1398e81ad1a64c5f'81test("an agent's message wakes nobody, even when it @mentions an agent member", () => {
Chat and workspace agents: channels, DMs and named agents you talk to82 assert.deepEqual(deliveries({ author: "agent:a1", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: [] }), []);
Merge branch 'worktree-agent-a1398e81ad1a64c5f'83 assert.deepEqual(deliveries({ author: "agent:a1", hops: 0, channelKind: "dm", agents: [ship, docs], mentioned: ["docs"] }), []);
84 assert.deepEqual(deliveries({ author: "agent:a1", hops: 2, channelKind: "channel", agents: [ship, docs], mentioned: ["docs", "ship"] }), []);
Chat and workspace agents: channels, DMs and named agents you talk to85});
86
87test("a delivery carries the chain's asker on g1t's own chat, and the wake's hops", () => {
88 const asker = { username: "ada", role: "member", can_write: false };
89 const place = { workspace: "acme", workspace_id: "w1", channel_id: "c1", channel_kind: "channel", channel_name: "ops" };
90 assert.deepEqual(
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)91 delivery(place, { agent_id: "a2", hops: 3 }, { id: "m1", thread_root: "m0" }, { hops: 2, asked_by: "u1", asker, chain: ["a1"] }),
92 { ...place, agent_id: "a2", message_id: "m1", thread_root: "m0", asked_by: "u1", asker, chain: ["a1"], hops: 3, surface: "g1t" },
Chat and workspace agents: channels, DMs and named agents you talk to93 );
94 // An agent that passed no asker back hands on none.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)95 assert.equal(delivery(place, { agent_id: "a2", hops: 1 }, { id: "m1", thread_root: null }, { hops: 0, asked_by: "u1", asker: null, chain: [] }).asker, null);
96});
97
98test("mentioning @g1t in a channel brings it in once; never into a DM", async () => {
99 const { addsOrchestrator } = await import("./delivery.ts");
100 assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["G1T"], orchestratorIsMember: false }), true);
101 assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["g1t"], orchestratorIsMember: true }), false, "already in");
102 assert.equal(addsOrchestrator({ channelKind: "channel", mentioned: ["ship"], orchestratorIsMember: false }), false);
103 assert.equal(addsOrchestrator({ channelKind: "dm", mentioned: ["g1t"], orchestratorIsMember: false }), false);
104 // Once in, it is woken like any mentioned agent member.
105 const g1t = { id: "a9", handle: "g1t" };
106 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents: [ship, g1t], mentioned: ["g1t"] }), [{ agent_id: "a9", hops: 0 }]);
107});
108
Merge branch 'worktree-agent-a1398e81ad1a64c5f'109test("a chain is the agents that handled a request, with the poster added", () => {
110 assert.deepEqual(chainFor(["a9"], "a1"), ["a9", "a1"]);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)111 assert.deepEqual(chainFor("nonsense", "a1"), ["a1"]);
112});
113
Merge branch 'worktree-agent-a1398e81ad1a64c5f'114test("a hand-off goes here when the colleague is in this channel or group DM, else to a group DM", () => {
115 assert.equal(handOffPlace({ channelKind: "channel", members: 40, colleagueHere: true }), "here");
116 assert.equal(handOffPlace({ channelKind: "dm", members: 3, colleagueHere: true }), "here");
117 assert.equal(handOffPlace({ channelKind: "channel", members: 40, colleagueHere: false }), "group_dm");
118 // The 1:1 DM the user saw: @g1t and the person, Mike not in it.
119 assert.equal(handOffPlace({ channelKind: "dm", members: 2, colleagueHere: false }), "group_dm");
120});
121
122test("a hand-off is refused to itself, to @g1t, back along the chain, and past the hop limit", () => {
123 const ok = { agent: "a9", colleague: { id: "a1" }, chain: [], hops: 0 };
124 assert.equal(handOffRefusal(ok), null);
125 assert.match(handOffRefusal({ ...ok, colleague: { id: "a9" } })!, /itself/);
126 assert.match(handOffRefusal({ ...ok, agent: "a1", colleague: { id: "a9", builtin: true } })!, /@g1t/);
127 assert.match(handOffRefusal({ ...ok, agent: "a2", chain: ["a1", "a2"] })!, /already handled/);
128 assert.equal(handOffRefusal({ ...ok, hops: MAX_HOPS - 1 }), null);
129 assert.match(handOffRefusal({ ...ok, hops: MAX_HOPS })!, /too many times/);
Chat and workspace agents: channels, DMs and named agents you talk to130});
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.131
132test("a personal agent is only in the direct message of it and the person it belongs to", () => {
133 const mine = { id: "a9", handle: "pip", scope: "personal", personal_owner_id: "u1" };
134 assert.equal(personalAgentRefusal(mine, { kind: "dm", members: ["user:u1", "agent:a9"] }), null);
135 assert.match(personalAgentRefusal(mine, { kind: "dm", members: ["user:u2", "agent:a9"] }) ?? "", /personal agent/);
136 assert.match(personalAgentRefusal(mine, { kind: "dm", members: ["user:u1", "user:u2", "agent:a9"] }) ?? "", /personal agent/, "not a group DM");
137 assert.match(personalAgentRefusal(mine, { kind: "channel", members: null }) ?? "", /personal agent/, "never invited to a channel");
138 assert.match(personalAgentRefusal(mine, { kind: "hand_off", members: null }) ?? "", /personal agent/, "never handed work");
139 assert.equal(personalAgentRefusal({ id: "a1", handle: "ship", scope: "workspace" }, { kind: "channel", members: null }), null);
140 assert.equal(personalAgentRefusal({ id: "a1", handle: "ship" }, { kind: "hand_off", members: null }), null, "agents from before personal ones are the workspace's");
141});
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.142
143test("an archived agent stays a member but is never woken; one the agents service no longer knows, or of another workspace, neither", () => {
144 const live = { id: "a1", handle: "ship", workspace_id: "wsp_1", archived_at: null };
145 const archived = { id: "a2", handle: "docs", workspace_id: "wsp_1", archived_at: "2026-10-09T10:00:00Z" };
146 const elsewhere = { id: "a3", handle: "ops", workspace_id: "wsp_2", archived_at: null };
147 const found = new Map<string, typeof live | null>([
148 ["a1", live],
149 ["a2", archived],
150 ["a3", elsewhere],
151 // Deleted for good: chat still has its membership row, the agents service has nothing.
152 ["a4", null],
153 ]);
154 assert.deepEqual(wakeable(found.values(), "wsp_1"), [live]);
155 // In a direct message with the two of them, an unaddressed message goes to the live one alone.
156 const agents = wakeable(found.values(), "wsp_1").map((a) => ({ id: a.id, handle: a.handle }));
157 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "dm", agents, mentioned: [] }), [{ agent_id: "a1", hops: 0 }]);
158 // Mentioning the archived one by handle wakes nobody: it is not among the wakeable.
159 assert.deepEqual(deliveries({ author: "user:u", hops: 0, channelKind: "channel", agents, mentioned: ["docs"] }), []);
160 assert.deepEqual(wakeable([], "wsp_1"), []);
161});

This file's history is long; its oldest lines are credited to the oldest commit read.