@saadnvd1/agent-os 0.3.819 → 0.3.821

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 (63) hide show
  1. package/app/api/client-config/route.test.ts +3 -1
  2. package/app/api/owner/route.test.ts +27 -0
  3. package/app/api/owner/route.ts +17 -0
  4. package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +2 -2
  5. package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +1 -1
  6. package/app/api/workspaces/[id]/orchestrator/rules/route.ts +2 -2
  7. package/app/api/workspaces/[id]/orchestrator/tools/route.ts +6 -1
  8. package/app/api/worktree-sweep/route.ts +2 -2
  9. package/components/Chat/EventRow.tsx +5 -2
  10. package/components/Chat/Messages.tsx +9 -2
  11. package/components/SessionList/statusDot.test.ts +1 -1
  12. package/components/Settings/OwnerNameRow.tsx +55 -0
  13. package/components/Settings/SetupSection.tsx +2 -0
  14. package/content/whats-new.json +8 -0
  15. package/data/client-config/queries.ts +3 -0
  16. package/data/owner/mutations.ts +19 -0
  17. package/lib/approvals/enable.ts +2 -1
  18. package/lib/artifacts/tools.ts +2 -1
  19. package/lib/away/brief.ts +2 -1
  20. package/lib/away/run.ts +5 -4
  21. package/lib/away/sweep.ts +2 -1
  22. package/lib/chat/runner.ts +2 -1
  23. package/lib/chat/turns.ts +1 -1
  24. package/lib/config/client-config-types.ts +2 -0
  25. package/lib/config/client-config.ts +2 -0
  26. package/lib/learning-cards/analyze.ts +2 -1
  27. package/lib/models/plan-step.ts +2 -1
  28. package/lib/orchestrator/approval-carry.ts +2 -1
  29. package/lib/orchestrator/armed.ts +9 -8
  30. package/lib/orchestrator/ask-heads.ts +2 -1
  31. package/lib/orchestrator/ask-tool.ts +6 -5
  32. package/lib/orchestrator/asks.ts +6 -4
  33. package/lib/orchestrator/brakes.ts +3 -2
  34. package/lib/orchestrator/brief.ts +61 -50
  35. package/lib/orchestrator/cross.ts +2 -1
  36. package/lib/orchestrator/describe.ts +5 -2
  37. package/lib/orchestrator/digest.ts +4 -3
  38. package/lib/orchestrator/escalate.ts +7 -6
  39. package/lib/orchestrator/external-gates.ts +5 -4
  40. package/lib/orchestrator/held-asks.ts +2 -2
  41. package/lib/orchestrator/held.ts +3 -2
  42. package/lib/orchestrator/mcp-servers.ts +1 -1
  43. package/lib/orchestrator/pause.ts +4 -3
  44. package/lib/orchestrator/questions.ts +2 -1
  45. package/lib/orchestrator/recipe-tools.ts +5 -4
  46. package/lib/orchestrator/review.ts +4 -3
  47. package/lib/orchestrator/rules.ts +2 -1
  48. package/lib/orchestrator/serve.ts +1 -1
  49. package/lib/orchestrator/shell-guard.ts +4 -4
  50. package/lib/orchestrator/signoff.test-helpers.ts +1 -1
  51. package/lib/orchestrator/signoff.ts +10 -7
  52. package/lib/orchestrator/sweep-arm.ts +2 -1
  53. package/lib/orchestrator/tool-names.ts +1 -1
  54. package/lib/orchestrator/tool-schemas.ts +13 -13
  55. package/lib/orchestrator/tools.ts +21 -21
  56. package/lib/owner-label.ts +25 -0
  57. package/lib/owner.ts +21 -0
  58. package/lib/project-add/index.ts +2 -1
  59. package/lib/schedules/index.ts +2 -1
  60. package/lib/session-logs/gather.ts +3 -2
  61. package/lib/tasks/queue.ts +3 -1
  62. package/lib/tasks/start.ts +3 -1
  63. package/package.json +1 -1
@@ -6,6 +6,8 @@ describe("GET /api/client-config", () => {
6
6
  const res = GET();
7
7
  expect(res.status).toBe(200);
8
8
  expect(res.headers.get("cache-control")).toBe("no-store");
9
- expect((await res.json()).v).toBe(1);
9
+ const body = await res.json();
10
+ expect(body.v).toBe(1);
11
+ expect(body).toHaveProperty("ownerName");
10
12
  });
11
13
  });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { GET, PUT } from "./route";
3
+
4
+ const put = (body: unknown) =>
5
+ PUT(
6
+ new Request("http://localhost/api/owner", {
7
+ method: "PUT",
8
+ body: JSON.stringify(body),
9
+ })
10
+ );
11
+
12
+ describe("/api/owner", () => {
13
+ it("sets, reads and clears the owner's name", async () => {
14
+ expect(await (await GET()).json()).toEqual({ name: null });
15
+ expect(await (await put({ name: " Alex " })).json()).toEqual({
16
+ name: "Alex",
17
+ });
18
+ expect(await (await GET()).json()).toEqual({ name: "Alex" });
19
+ expect(await (await put({ name: null })).json()).toEqual({ name: null });
20
+ expect(await (await GET()).json()).toEqual({ name: null });
21
+ });
22
+
23
+ it("refuses a name that isn't text", async () => {
24
+ expect((await put({ name: 5 })).status).toBe(400);
25
+ expect((await put({})).status).toBe(400);
26
+ });
27
+ });
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from "next/server";
2
+ import { ownerName, setOwnerName } from "@/lib/owner";
3
+
4
+ // GET /api/owner - the name the owner gave, or null.
5
+ export function GET() {
6
+ return NextResponse.json({ name: ownerName() });
7
+ }
8
+
9
+ // PUT /api/owner { name } - set it; blank or null clears it.
10
+ export async function PUT(request: Request) {
11
+ const body = (await request.json().catch(() => null)) as {
12
+ name?: unknown;
13
+ } | null;
14
+ if (!body || (body.name !== null && typeof body.name !== "string"))
15
+ return NextResponse.json({ error: "name must be text" }, { status: 400 });
16
+ return NextResponse.json({ name: setOwnerName(body.name) });
17
+ }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import type { PresenceProof } from "@/lib/security/presence-http";
3
3
  import { removeRule, updateRule } from "@/lib/orchestrator/rules";
4
- import { provenAsSaad } from "../presence";
4
+ import { provenAsOwner } from "../presence";
5
5
 
6
6
  type RouteParams = { params: Promise<{ id: string; ruleId: string }> };
7
7
 
@@ -21,7 +21,7 @@ async function target(
21
21
  response: NextResponse.json({ error: "Bad rule id" }, { status: 400 }),
22
22
  };
23
23
  const body = (await request.json().catch(() => ({}))) as Body;
24
- const refused = await provenAsSaad(request, id, body);
24
+ const refused = await provenAsOwner(request, id, body);
25
25
  if (refused) return { response: refused };
26
26
  return { workspaceId: id, ruleId: n, body };
27
27
  }
@@ -10,7 +10,7 @@ import { rulesPresence } from "@/lib/orchestrator/presence-binding";
10
10
  // A rule written here is Saad's, shown to the orchestrator as his word and
11
11
  // removable only by him: so it takes his passkey, not just a trusted place
12
12
  // (any process on this machine reaches the route).
13
- export async function provenAsSaad(
13
+ export async function provenAsOwner(
14
14
  request: NextRequest,
15
15
  workspaceId: string,
16
16
  proof: PresenceProof | undefined
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import type { PresenceProof } from "@/lib/security/presence-http";
3
3
  import { getWorkspace } from "@/lib/workspaces";
4
4
  import { addRule, listRules } from "@/lib/orchestrator/rules";
5
- import { provenAsSaad } from "./presence";
5
+ import { provenAsOwner } from "./presence";
6
6
 
7
7
  type RouteParams = { params: Promise<{ id: string }> };
8
8
 
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
25
25
  const { text } = body;
26
26
  if (typeof text !== "string")
27
27
  return NextResponse.json({ error: "text is required" }, { status: 400 });
28
- const refused = await provenAsSaad(request, id, body);
28
+ const refused = await provenAsOwner(request, id, body);
29
29
  if (refused) return refused;
30
30
  try {
31
31
  return NextResponse.json({ rule: addRule(id, text, "saad") });
@@ -5,6 +5,8 @@ import { TOKEN_HEADER } from "@/lib/orchestrator/tools";
5
5
 
6
6
  type RouteParams = { params: Promise<{ id: string }> };
7
7
 
8
+ const LEGACY_ASK = "ask_saad";
9
+
8
10
  // The orchestrator chat's tools call here from its worker: plain text back.
9
11
  // Only that workspace's orchestrator worker holds the secret: other local
10
12
  // sessions are trusted by the network, not to act as the orchestrator.
@@ -12,7 +14,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
12
14
  const { id } = await params;
13
15
  if (!isOrchestratorToken(id, request.headers.get(TOKEN_HEADER)))
14
16
  return new NextResponse("Not the orchestrator", { status: 403 });
15
- const { tool, args } = await request.json().catch(() => ({}));
17
+ const body = await request.json().catch(() => ({}));
18
+ const { args } = body;
19
+ // A worker started before the rename still calls ask_saad.
20
+ const tool = body.tool === LEGACY_ASK ? "ask_owner" : body.tool;
16
21
  if (!isToolName(tool))
17
22
  return NextResponse.json({ error: "Unknown tool" }, { status: 400 });
18
23
  try {
@@ -10,6 +10,7 @@ import { requireLocalTrust } from "@/lib/security/route-guard";
10
10
  import { db, type Session } from "@/lib/db";
11
11
  import { orchestratorFullAccess } from "@/lib/orchestrator/full-access";
12
12
  import { sweepState } from "@/lib/worktree-sweep/state";
13
+ import { owner } from "@/lib/owner";
13
14
 
14
15
  const body = z.union([
15
16
  z.object({
@@ -53,8 +54,7 @@ export async function PUT(request: NextRequest) {
53
54
  if (!s || !orchestratorFullAccess(s))
54
55
  return NextResponse.json(
55
56
  {
56
- error:
57
- "a session can't turn the full clean-up on: Saad does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to",
57
+ error: `a session can't turn the full clean-up on: ${owner()} does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to`,
58
58
  },
59
59
  { status: 403 }
60
60
  );
@@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
18
18
  import { UserMessage } from "./Messages";
19
19
  import { openSessionLink as openSession } from "./session-link";
20
20
  import { useRowState } from "./rowState";
21
+ import { useOwnerName } from "@/data/client-config/queries";
22
+ import { ownerInUi } from "@/lib/owner-label";
21
23
 
22
24
  type UserItem = Extract<ChatItem, { kind: "user" }>;
23
25
 
@@ -158,6 +160,7 @@ export function EventRow({
158
160
  origin: ChatOrigin;
159
161
  }) {
160
162
  const { body, decided } = eventParts(item, origin);
163
+ const decidedLabel = `${ownerInUi(useOwnerName(), true)} decided`;
161
164
  const decision = origin.kind === "decision";
162
165
  return (
163
166
  <div className="flex flex-col gap-1">
@@ -167,7 +170,7 @@ export function EventRow({
167
170
  id={`${item.id}:${i}`}
168
171
  kind="decision"
169
172
  tone="decision"
170
- head={<span className="truncate font-medium">Saad decided</span>}
173
+ head={<span className="truncate font-medium">{decidedLabel}</span>}
171
174
  body={d}
172
175
  at={item.createdAt}
173
176
  />
@@ -179,7 +182,7 @@ export function EventRow({
179
182
  tone={decision ? "decision" : undefined}
180
183
  head={
181
184
  decision ? (
182
- <span className="truncate font-medium">Saad decided</span>
185
+ <span className="truncate font-medium">{decidedLabel}</span>
183
186
  ) : origin.kind === "guest" ? (
184
187
  <span className="truncate font-medium">
185
188
  Suggested by {origin.label}
@@ -39,6 +39,8 @@ import {
39
39
  TurnBookmark,
40
40
  BookmarkPress,
41
41
  } from "@/components/Bookmarks/BookmarkButton";
42
+ import { useOwnerName } from "@/data/client-config/queries";
43
+ import { ownerVerb } from "@/lib/owner-label";
42
44
 
43
45
  type Of<K extends ChatItem["kind"]> = Extract<ChatItem, { kind: K }>;
44
46
 
@@ -473,13 +475,18 @@ export function CommandOutput({ item }: { item: Of<"command_output"> }) {
473
475
  const NOTE_LABEL = {
474
476
  note: "Note",
475
477
  brake: "Brake",
476
- escalation: "Saad decides",
478
+ escalation: "",
477
479
  ask: "Ask",
478
480
  pause: "Pause",
479
481
  away: "Away",
480
482
  } as const;
481
483
 
482
484
  export function NoteLine({ item }: { item: Of<"note"> }) {
485
+ const name = useOwnerName();
486
+ const label =
487
+ item.tone === "escalation"
488
+ ? ownerVerb(name, "decides", "decide")
489
+ : NOTE_LABEL[item.tone];
483
490
  return (
484
491
  <div
485
492
  className={cn(
@@ -490,7 +497,7 @@ export function NoteLine({ item }: { item: Of<"note"> }) {
490
497
  )}
491
498
  >
492
499
  <span className="text-muted-foreground mr-2 font-mono text-[11px] tracking-wide uppercase">
493
- {NOTE_LABEL[item.tone]}
500
+ {label}
494
501
  </span>
495
502
  {item.text}
496
503
  </div>
@@ -35,7 +35,7 @@ describe("statusDot", () => {
35
35
  expect(d.title).toContain("Load 4.2 on 10 cores");
36
36
  expect(d.opensHealth).toBe(true);
37
37
  });
38
- it("goes red when a health issue needs Saad", () => {
38
+ it("goes red when a health issue needs the owner", () => {
39
39
  const d = statusDot(load("green"), { open: 2, needsYou: 2 })!;
40
40
  expect(d.level).toBe("red");
41
41
  expect(d.label).toBe("Load normal · 2 health issues need you");
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { useClientConfig } from "@/data/client-config/queries";
7
+ import { useSetOwnerName } from "@/data/owner/mutations";
8
+
9
+ // The name AgentOS and the orchestrators use when they refer to you.
10
+ export function OwnerNameRow() {
11
+ const saved = useClientConfig().data?.ownerName ?? "";
12
+ // null: show what is saved.
13
+ const [draft, setDraft] = useState<string | null>(null);
14
+ const value = draft ?? saved;
15
+ const save = useSetOwnerName();
16
+ return (
17
+ <form
18
+ className="bg-muted/40 flex flex-col gap-2 rounded-lg px-3 py-2.5 sm:flex-row sm:items-center"
19
+ onSubmit={(e) => {
20
+ e.preventDefault();
21
+ save.mutate(value.trim() || null, { onSuccess: () => setDraft(null) });
22
+ }}
23
+ >
24
+ <div className="min-w-0 flex-1">
25
+ <label htmlFor="owner-name" className="text-sm font-medium">
26
+ Your name
27
+ </label>
28
+ <p className="text-muted-foreground text-xs">
29
+ Used where AgentOS and your orchestrators refer to you. Orchestrators
30
+ pick it up when they next start.
31
+ </p>
32
+ {save.error && (
33
+ <p className="text-destructive text-xs">{save.error.message}</p>
34
+ )}
35
+ </div>
36
+ <Input
37
+ id="owner-name"
38
+ value={value}
39
+ maxLength={40}
40
+ placeholder="Not set"
41
+ onChange={(e) => setDraft(e.target.value)}
42
+ className="min-h-11 sm:min-h-8 sm:w-44"
43
+ />
44
+ <Button
45
+ type="submit"
46
+ size="sm"
47
+ variant="outline"
48
+ className="min-h-11 sm:min-h-8"
49
+ disabled={save.isPending || value.trim() === saved}
50
+ >
51
+ Save
52
+ </Button>
53
+ </form>
54
+ );
55
+ }
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
4
4
  import { HealthList } from "@/components/Welcome/HealthList";
5
5
  import { AgentList } from "@/components/Welcome/AgentList";
6
6
  import { ReachRow } from "@/components/Reach/ReachRow";
7
+ import { OwnerNameRow } from "./OwnerNameRow";
7
8
  import { settingsUiActions } from "@/stores/settingsUi";
8
9
  import { welcomeUiActions } from "@/stores/welcomeUi";
9
10
 
@@ -11,6 +12,7 @@ import { welcomeUiActions } from "@/stores/welcomeUi";
11
12
  export function SetupSection() {
12
13
  return (
13
14
  <div className="flex flex-col gap-4">
15
+ <OwnerNameRow />
14
16
  <div className="bg-muted/40 flex items-center gap-3 rounded-lg px-3 py-2.5">
15
17
  <div className="min-w-0 flex-1">
16
18
  <p className="text-sm font-medium">First-run setup</p>
@@ -1913,6 +1913,14 @@
1913
1913
  "area": "AgentOS",
1914
1914
  "text": "Files shared into the app are now checked against the same rules as chat attachments.",
1915
1915
  "url": "https://github.com/saadnvd1/agent-os/pull/549"
1916
+ },
1917
+ {
1918
+ "id": 550,
1919
+ "date": "2026-11-01",
1920
+ "kind": "new",
1921
+ "area": "AgentOS",
1922
+ "text": "You can set your name in Settings, and AgentOS uses it in chats and notes instead of a fixed name.",
1923
+ "url": "https://github.com/saadnvd1/agent-os/pull/550"
1916
1924
  }
1917
1925
  ]
1918
1926
  }
@@ -16,3 +16,6 @@ export const clientConfigQuery = queryOptions({
16
16
  });
17
17
 
18
18
  export const useClientConfig = () => useQuery(clientConfigQuery);
19
+
20
+ /** The owner's own name, or null until they set one (see lib/owner-label). */
21
+ export const useOwnerName = () => useClientConfig().data?.ownerName ?? null;
@@ -0,0 +1,19 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { clientConfigKeys } from "@/data/client-config/queries";
3
+
4
+ export function useSetOwnerName() {
5
+ const qc = useQueryClient();
6
+ return useMutation({
7
+ mutationFn: async (name: string | null) => {
8
+ const res = await fetch("/api/owner", {
9
+ method: "PUT",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify({ name }),
12
+ });
13
+ const data = await res.json();
14
+ if (!res.ok) throw new Error(data.error || "Couldn't save the name");
15
+ return data.name as string | null;
16
+ },
17
+ onSuccess: () => qc.invalidateQueries({ queryKey: clientConfigKeys.all }),
18
+ });
19
+ }
@@ -11,6 +11,7 @@ import {
11
11
  recordAutoApproval,
12
12
  setNoApprovals,
13
13
  } from "./no-approvals";
14
+ import { ownerUi } from "../owner";
14
15
 
15
16
  // Everything already waiting when the mode goes on is approved: open asks,
16
17
  // tool cards in live chats.
@@ -42,7 +43,7 @@ export async function switchNoApprovals(on: boolean): Promise<void> {
42
43
  o.workspace_id,
43
44
  on
44
45
  ? "No approvals mode is on: every ask is approved as it's raised, and agents run without permission prompts."
45
- : "No approvals mode is off: asks wait for Saad again.",
46
+ : `No approvals mode is off: asks wait for ${ownerUi()} again.`,
46
47
  "ask"
47
48
  );
48
49
  }
@@ -21,6 +21,7 @@ import {
21
21
  } from "./store";
22
22
  import { followLatest } from "../share/follow";
23
23
  import { previewHtml } from "./preview";
24
+ import { owner } from "../owner";
24
25
 
25
26
  export const VISUALS_SERVER = "visuals";
26
27
 
@@ -218,4 +219,4 @@ When a chart, table, diagram or mockup would say more than prose, build a self-c
218
219
  // is told what to keep out of a page.
219
220
  export const ORCHESTRATOR_VISUALS_BRIEF = `## Pages
220
221
 
221
- \`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for Saad to read or share; check it with \`html_preview\` first. Put only what you wrote in it: no secrets, and nothing from a session's text loaded from a URL.`;
222
+ \`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for ${owner()} to read or share; check it with \`html_preview\` first. Put only what you wrote in it: no secrets, and nothing from a session's text loaded from a URL.`;
package/lib/away/brief.ts CHANGED
@@ -12,9 +12,10 @@ import {
12
12
  } from "./state";
13
13
  import { SWEEP_EVERY_MS } from "./sweep";
14
14
  import { awayWindowMinutes } from "./activity";
15
+ import { owner, Owner } from "../owner";
15
16
 
16
17
  export function awayOnLine(row: AwayRow, now = Date.now()): string {
17
- return `Saad is away (Away mode, since ${clockTime(fromSql(row.started_at), now)}, ${untilText(row, now)}). Follow work through rather than only reacting to events: keep each task moving to merged and deployed, sign off what passes its gates, and pick up what stalled. Every ${SWEEP_EVERY_MS / 60000} minutes AgentOS sweeps this workspace without messaging any session: it runs lost reviews again, checks the deploy after each merge, and sends you one digest of what's new (stalled tasks, idle ones with uncommitted work and no PR, Saad's own sessions idle with uncommitted work). You decide what needs work and act with your tools. Only sessions with real activity in the last ${awayWindowMinutes()} minutes are listed: leave the rest alone, since a message wakes an agent and re-reads its whole context. Interactive sessions (not tasks) are Saad's: ask a listed one at most once to wrap up, never drive them further. Hard lines still go to ask_saad; while he's away his asks reach his phone in one batch per sweep, so raise them as usual. \`aos notify\` is held for that batch too, so don't send one per item.`;
18
+ return `${Owner()} is away (Away mode, since ${clockTime(fromSql(row.started_at), now)}, ${untilText(row, now)}). Follow work through rather than only reacting to events: keep each task moving to merged and deployed, sign off what passes its gates, and pick up what stalled. Every ${SWEEP_EVERY_MS / 60000} minutes AgentOS sweeps this workspace without messaging any session: it runs lost reviews again, checks the deploy after each merge, and sends you one digest of what's new (stalled tasks, idle ones with uncommitted work and no PR, ${owner()}'s own sessions idle with uncommitted work). You decide what needs work and act with your tools. Only sessions with real activity in the last ${awayWindowMinutes()} minutes are listed: leave the rest alone, since a message wakes an agent and re-reads its whole context. Interactive sessions (not tasks) are ${owner()}'s: ask a listed one at most once to wrap up, never drive them further. Hard lines still go to ask_owner; while he's away his asks reach his phone in one batch per sweep, so raise them as usual. \`aos notify\` is held for that batch too, so don't send one per item.`;
18
19
  }
19
20
 
20
21
  // For the digest: the same line whenever Away is on, messaged or not, so an
package/lib/away/run.ts CHANGED
@@ -56,6 +56,7 @@ import { checkDeploy } from "./deploy";
56
56
  import { buildAwayReport, phoneSummary, reportMarkdown } from "./report";
57
57
  import { awayOnLine } from "./brief";
58
58
  import { shutFinishedTaskSimulators } from "../simulator";
59
+ import { owner, Owner, ownerUi } from "../owner";
59
60
 
60
61
  const PHONE_SOURCE = "away";
61
62
  const PHONE_LINES = 8;
@@ -116,7 +117,7 @@ function tellAway(
116
117
  ).run(away.id, workspaceId, orchestratorId, toldKey(workspaceId));
117
118
  addNote(
118
119
  workspaceId,
119
- `Saad turned Away on, ${untilText(away, now)}.`,
120
+ `${ownerUi(true)} turned Away on, ${untilText(away, now)}.`,
120
121
  "away"
121
122
  );
122
123
  }
@@ -181,13 +182,13 @@ export function turnAwayOff(
181
182
  }
182
183
  const line =
183
184
  by === "expired"
184
- ? "Away mode ran out: Saad may be back. Go back to reacting to events; don't drive interactive sessions."
185
- : "Saad is back: Away mode is off. Go back to reacting to events.";
185
+ ? `Away mode ran out: ${owner()} may be back. Go back to reacting to events; don't drive interactive sessions.`
186
+ : `${Owner()} is back: Away mode is off. Go back to reacting to events.`;
186
187
  for (const workspaceId of reachedWorkspaces(ended.id)) {
187
188
  queueEvent(workspaceId, `away-back:${ended.id}`, null, line, now);
188
189
  addNote(
189
190
  workspaceId,
190
- by === "expired" ? "Away ran out." : "Saad turned Away off.",
191
+ by === "expired" ? "Away ran out." : `${ownerUi(true)} turned Away off.`,
191
192
  "away"
192
193
  );
193
194
  }
package/lib/away/sweep.ts CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { isFinished } from "../tasks/state";
23
23
  import type { SessionFacts } from "../orchestrator/facts";
24
+ import { owner } from "../owner";
24
25
 
25
26
  export const SWEEP_EVERY_MS = 10 * 60 * 1000;
26
27
  export const STALL_MS = 30 * 60 * 1000;
@@ -199,7 +200,7 @@ function wrapupCandidate(f: SessionFacts, v: SweepView): Candidate | null {
199
200
  kind: "wrapup",
200
201
  sessionId: f.id,
201
202
  key,
202
- line: `${f.name} (${f.id}): Saad's own session, idle ${minutes(v.now - turnOf(f, v))}m with uncommitted work.`,
203
+ line: `${f.name} (${f.id}): ${owner()}'s own session, idle ${minutes(v.now - turnOf(f, v))}m with uncommitted work.`,
203
204
  };
204
205
  }
205
206
 
@@ -86,6 +86,7 @@ import { DEMO_REFUSAL, demoMode } from "../security/demo";
86
86
  import { chatHold, chatRefusal } from "./hold";
87
87
  import { getHost } from "../hosts";
88
88
  import { hostLink } from "../hosts/remote-api";
89
+ import { ownerUi } from "../owner";
89
90
 
90
91
  export {
91
92
  refreshCapabilities,
@@ -119,7 +120,7 @@ export async function carryOutPlan(
119
120
  {
120
121
  id: `user-carry-${planId}`,
121
122
  text: CARRY_OUT,
122
- origin: { kind: "decision", label: "Saad" },
123
+ origin: { kind: "decision", label: ownerUi(true) },
123
124
  host: true,
124
125
  },
125
126
  timeoutMs
package/lib/chat/turns.ts CHANGED
@@ -38,7 +38,7 @@ export const isQuestionTool = (item: ChatItem): boolean =>
38
38
  item.kind === "tool" && /(^|__)question$/.test(item.name);
39
39
 
40
40
  export const isAskTool = (item: ChatItem): boolean =>
41
- item.kind === "tool" && /(^|__)ask_saad$/.test(item.name);
41
+ item.kind === "tool" && /(^|__)ask_(owner|saad)$/.test(item.name);
42
42
 
43
43
  // The assistant message a question was asked in (its turn's last words
44
44
  // before the call), else the call itself.
@@ -30,6 +30,8 @@ export interface ClientConfig {
30
30
  intake: IntakeLimits;
31
31
  attachments: AttachmentConfig;
32
32
  voice: VoiceLimits;
33
+ /** The owner's own name for text that refers to them; null until set. */
34
+ ownerName: string | null;
33
35
  }
34
36
 
35
37
  // What a chat accepts, served from lib/attachments/policy.ts. Sizes in bytes.
@@ -1,3 +1,4 @@
1
+ import { ownerName } from "../owner";
1
2
  import { ATTACHMENT_LIMITS } from "../attachments/policy";
2
3
  import { ACCESS_LEVELS, DEFAULT_NEW_CHAT_ACCESS } from "../chat/access-levels";
3
4
  import { CHAT_ACCESS } from "../chat/events";
@@ -42,5 +43,6 @@ export function clientConfig(): ClientConfig {
42
43
  intake: INTAKE_LIMITS,
43
44
  attachments: ATTACHMENT_LIMITS,
44
45
  voice: VOICE_LIMITS,
46
+ ownerName: ownerName(),
45
47
  };
46
48
  }
@@ -24,6 +24,7 @@ import {
24
24
  type Pair,
25
25
  type SessionInput,
26
26
  } from "./types";
27
+ import { owner } from "../owner";
27
28
 
28
29
  const ID = "learning-cards" as const;
29
30
  const LESSON_MAX = 200;
@@ -227,7 +228,7 @@ export async function analyzeSession(
227
228
  input.workspaceId,
228
229
  `learning-card:${card.id}`,
229
230
  sessionId,
230
- `Learning card on "${input.name.replace(/\s+/g, " ")}", waiting on Saad's Needs-you strip: ${untrusted("learning card", cardTitle(card))}`
231
+ `Learning card on "${input.name.replace(/\s+/g, " ")}", waiting on ${owner()}'s Needs-you strip: ${untrusted("learning card", cardTitle(card))}`
231
232
  );
232
233
  } finally {
233
234
  inFlight.delete(sessionId);
@@ -35,6 +35,7 @@ import {
35
35
  splitOf,
36
36
  } from "../plans/features";
37
37
  import { advancePlan, setPlan } from "../db/session-parts";
38
+ import { Owner } from "../owner";
38
39
 
39
40
  export const PLAN_FILE = ".agentos/plan.md";
40
41
 
@@ -533,7 +534,7 @@ export async function recheckPlanning(
533
534
  export type ApprovedBy = "saad" | "orchestrator" | "no-approvals";
534
535
 
535
536
  const APPROVAL_TEXT: Record<ApprovedBy, string> = {
536
- saad: "Saad approved the plan; implement it as planned.",
537
+ saad: `${Owner()} approved the plan; implement it as planned.`,
537
538
  orchestrator: "The orchestrator approved the plan; implement it as planned.",
538
539
  "no-approvals":
539
540
  "The plan was approved automatically (no-approvals mode); implement it as planned.",
@@ -11,6 +11,7 @@ import { addNote } from "./notes";
11
11
  import { comparePatches } from "./patch";
12
12
  import { fetchRefs } from "./repo";
13
13
  import type { Session } from "../db";
14
+ import { Owner } from "../owner";
14
15
 
15
16
  const short = (sha: string) => sha.slice(0, 7);
16
17
 
@@ -41,7 +42,7 @@ export async function carryApproval(
41
42
  if (rebindApproval(approval.id, approval.sha, head, line))
42
43
  addNote(
43
44
  workspaceId,
44
- `Saad's approval of ${work.name} at ${short(approval.sha)} carries to ${short(head)}: same patch, rebased onto ${base}.`
45
+ `${Owner()}'s approval of ${work.name} at ${short(approval.sha)} carries to ${short(head)}: same patch, rebased onto ${base}.`
45
46
  );
46
47
  const now = getAsk(workspaceId, approval.id);
47
48
  return { approval: now?.sha === head && !now.used_at ? now : null, note };
@@ -31,6 +31,7 @@ import { workspaceTask } from "./targets";
31
31
  import { markStatusDirty } from "../status/dirty";
32
32
  import { setPr } from "../db/session-parts";
33
33
  import { noteAutoArmed } from "./fast-lane-state";
34
+ import { owner } from "../owner";
34
35
 
35
36
  // Judged again at least this often, for what the fingerprint can't see.
36
37
  export const ARMED_RECHECK_MS = 5 * 60 * 1000;
@@ -94,7 +95,7 @@ export async function arm(workspaceId: string, ref: string): Promise<string> {
94
95
  const task = workspaceTask(workspaceId, ref);
95
96
  armTask(task);
96
97
  if (isFast(task)) return armedNow(workspaceId, task);
97
- return `Armed ${task.name}: AgentOS merges its PR the moment every sign_off gate passes at its head, and tells you with one event. A gate failure also comes as one event; it stays armed for the fix, unless the failure goes to Saad. No need to call sign_off.`;
98
+ return `Armed ${task.name}: AgentOS merges its PR the moment every sign_off gate passes at its head, and tells you with one event. A gate failure also comes as one event; it stays armed for the fix, unless the failure goes to ${owner()}. No need to call sign_off.`;
98
99
  }
99
100
 
100
101
  // Arms a task, or says why it can't be: only a running task, in a
@@ -108,11 +109,11 @@ export function armTask(task: Session): void {
108
109
  const held = approved ? [] : holdingEscalations(task.id);
109
110
  if (held.length)
110
111
  throw new Error(
111
- `${task.name} is with Saad (${held.map((h) => h.gate).join(", ")}); only he merges it`
112
+ `${task.name} is with ${owner()} (${held.map((h) => h.gate).join(", ")}); only he merges it`
112
113
  );
113
114
  if (w && !approved && openBySubject(w, workSubject(task.id)))
114
115
  throw new Error(
115
- `${task.name} has an open ask with Saad; it merges on his answer`
116
+ `${task.name} has an open ask with ${owner()}; it merges on his answer`
116
117
  );
117
118
  if (task.peer_mirror)
118
119
  throw new Error(`${task.name} runs on a linked machine; arm it there`);
@@ -255,7 +256,7 @@ async function mergeOne(
255
256
  event(
256
257
  workspaceId,
257
258
  task,
258
- `task ${task.name}: disarmed, it's with Saad (${held.join(", ")})`
259
+ `task ${task.name}: disarmed, it's with ${owner()} (${held.join(", ")})`
259
260
  );
260
261
  return;
261
262
  }
@@ -280,14 +281,14 @@ async function mergeOne(
280
281
  if (!stillArmed(workspaceId, task.id)) return;
281
282
  const counted = await deps.judge(workspaceId, task);
282
283
  if (counted.ok) return merge(workspaceId, task, counted, deps);
283
- const toSaad = holdingEscalations(task.id).length > 0;
284
- if (toSaad) setArmed(task.id, false);
284
+ const toOwner = holdingEscalations(task.id).length > 0;
285
+ if (toOwner) setArmed(task.id, false);
285
286
  else setPr(db, task.id, { merge_armed_seen: seen });
286
287
  event(
287
288
  workspaceId,
288
289
  task,
289
- toSaad
290
- ? `task ${task.name}: armed merge stopped and disarmed, it went to Saad.\n${counted.text}`
290
+ toOwner
291
+ ? `task ${task.name}: armed merge stopped and disarmed, it went to ${owner()}.\n${counted.text}`
291
292
  : `task ${task.name}: armed, not merged yet; it stays armed and merges once this is fixed.\n${counted.text}`
292
293
  );
293
294
  }
@@ -15,6 +15,7 @@ import type { SessionFacts } from "./facts";
15
15
  import { addNote } from "./notes";
16
16
  import { comparePatches } from "./patch";
17
17
  import { fetchRefs, repoOf } from "./repo";
18
+ import { Owner } from "../owner";
18
19
 
19
20
  const short = (sha: string) => sha.slice(0, 7);
20
21
 
@@ -49,7 +50,7 @@ export async function followAskHeads(
49
50
  if (moveAskHead(ask.id, ask.sha, pr.head, line))
50
51
  addNote(
51
52
  workspaceId,
52
- `Saad's ask on ${task.name} now names ${short(pr.head)} (${same ? "same patch" : "patch changed"}).`,
53
+ `${Owner()}'s ask on ${task.name} now names ${short(pr.head)} (${same ? "same patch" : "patch changed"}).`,
53
54
  "ask"
54
55
  );
55
56
  } catch (error) {