@saadnvd1/agent-os 0.3.817 → 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 (67) hide show
  1. package/app/api/client-config/route.test.ts +3 -1
  2. package/app/api/orchestrators/[workspaceId]/intake/route.test.ts +77 -0
  3. package/app/api/owner/route.test.ts +27 -0
  4. package/app/api/owner/route.ts +17 -0
  5. package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +2 -2
  6. package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +1 -1
  7. package/app/api/workspaces/[id]/orchestrator/rules/route.ts +2 -2
  8. package/app/api/workspaces/[id]/orchestrator/tools/route.ts +6 -1
  9. package/app/api/worktree-sweep/route.ts +2 -2
  10. package/components/Chat/EventRow.tsx +5 -2
  11. package/components/Chat/Messages.tsx +9 -2
  12. package/components/SessionList/statusDot.test.ts +1 -1
  13. package/components/Settings/OwnerNameRow.tsx +55 -0
  14. package/components/Settings/SetupSection.tsx +2 -0
  15. package/content/whats-new.json +16 -0
  16. package/data/client-config/queries.ts +3 -0
  17. package/data/owner/mutations.ts +19 -0
  18. package/lib/approvals/enable.ts +2 -1
  19. package/lib/artifacts/tools.ts +2 -1
  20. package/lib/away/brief.ts +2 -1
  21. package/lib/away/run.ts +5 -4
  22. package/lib/away/sweep.ts +2 -1
  23. package/lib/chat/runner.ts +2 -1
  24. package/lib/chat/turns.ts +1 -1
  25. package/lib/config/client-config-types.ts +5 -1
  26. package/lib/config/client-config.ts +2 -0
  27. package/lib/intake/accept.ts +58 -0
  28. package/lib/intake/intake.ts +44 -17
  29. package/lib/intake/message.ts +6 -2
  30. package/lib/learning-cards/analyze.ts +2 -1
  31. package/lib/models/plan-step.ts +2 -1
  32. package/lib/orchestrator/approval-carry.ts +2 -1
  33. package/lib/orchestrator/armed.ts +9 -8
  34. package/lib/orchestrator/ask-heads.ts +2 -1
  35. package/lib/orchestrator/ask-tool.ts +6 -5
  36. package/lib/orchestrator/asks.ts +6 -4
  37. package/lib/orchestrator/brakes.ts +3 -2
  38. package/lib/orchestrator/brief.ts +61 -50
  39. package/lib/orchestrator/cross.ts +2 -1
  40. package/lib/orchestrator/describe.ts +5 -2
  41. package/lib/orchestrator/digest.ts +4 -3
  42. package/lib/orchestrator/escalate.ts +7 -6
  43. package/lib/orchestrator/external-gates.ts +5 -4
  44. package/lib/orchestrator/held-asks.ts +2 -2
  45. package/lib/orchestrator/held.ts +3 -2
  46. package/lib/orchestrator/mcp-servers.ts +1 -1
  47. package/lib/orchestrator/pause.ts +4 -3
  48. package/lib/orchestrator/questions.ts +2 -1
  49. package/lib/orchestrator/recipe-tools.ts +5 -4
  50. package/lib/orchestrator/review.ts +4 -3
  51. package/lib/orchestrator/rules.ts +2 -1
  52. package/lib/orchestrator/serve.ts +1 -1
  53. package/lib/orchestrator/shell-guard.ts +4 -4
  54. package/lib/orchestrator/signoff.test-helpers.ts +1 -1
  55. package/lib/orchestrator/signoff.ts +10 -7
  56. package/lib/orchestrator/sweep-arm.ts +2 -1
  57. package/lib/orchestrator/tool-names.ts +1 -1
  58. package/lib/orchestrator/tool-schemas.ts +13 -13
  59. package/lib/orchestrator/tools.ts +21 -21
  60. package/lib/owner-label.ts +25 -0
  61. package/lib/owner.ts +21 -0
  62. package/lib/project-add/index.ts +2 -1
  63. package/lib/schedules/index.ts +2 -1
  64. package/lib/session-logs/gather.ts +3 -2
  65. package/lib/tasks/queue.ts +3 -1
  66. package/lib/tasks/start.ts +3 -1
  67. 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
  });
@@ -193,6 +193,83 @@ describe("POST /api/orchestrators/:id/intake", () => {
193
193
  expect(sent).toHaveLength(0);
194
194
  });
195
195
 
196
+ it("takes a HAR over the old 25 MB cap, redacted, and names it by path", async () => {
197
+ const { ws, device } = setup();
198
+ const secret = ["tok", crypto.randomUUID()].join("-");
199
+ const har = JSON.stringify({
200
+ log: {
201
+ version: "1.2",
202
+ entries: [
203
+ {
204
+ request: {
205
+ method: "GET",
206
+ url: "https://api.example.com/v1/me",
207
+ headers: [{ name: "Authorization", value: `Bearer ${secret}` }],
208
+ },
209
+ response: {
210
+ status: 200,
211
+ headers: [],
212
+ content: { size: 30_000_000, text: "x".repeat(30_000_000) },
213
+ },
214
+ },
215
+ ],
216
+ },
217
+ });
218
+ expect(Buffer.byteLength(har)).toBeGreaterThan(25_000_000);
219
+ const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
220
+ { name: "capture.har", data: Buffer.from(har) },
221
+ ]);
222
+ expect(res.status).toBe(201);
223
+ const body = await res.json();
224
+ const saved = fs.readFileSync(path.join(body.dir, "capture.har"), "utf8");
225
+ expect(saved).not.toContain(secret);
226
+ expect(saved).toContain("[redacted]");
227
+ expect(
228
+ fs.readFileSync(path.join(body.dir, "capture.har.summary.txt"), "utf8")
229
+ ).toContain("1 requests");
230
+ const text = (sent[0].input as { text: string }).text;
231
+ expect(text).toContain(path.join(body.dir, "capture.har"));
232
+ expect(text).toContain("large: read it in parts");
233
+ expect(text).not.toContain("xxxxxxxxxx");
234
+ });
235
+
236
+ it("takes a shell script as text, with its #! line", async () => {
237
+ const { ws, device } = setup();
238
+ const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
239
+ { name: "deploy.sh", data: Buffer.from("#!/bin/sh\necho hi\n") },
240
+ ]);
241
+ expect(res.status).toBe(201);
242
+ const { dir } = await res.json();
243
+ expect(fs.readFileSync(path.join(dir, "deploy.sh"), "utf8")).toContain(
244
+ "#!/bin/sh"
245
+ );
246
+ });
247
+
248
+ it("still refuses a Mach-O or ELF binary, even named like a script", async () => {
249
+ const { ws, device } = setup();
250
+ const macho = Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1, 0, 0]);
251
+ const elf = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0, 0, 0]);
252
+ for (const name of ["tool", "tool.sh", "tool.exe"])
253
+ for (const data of [macho, elf]) {
254
+ const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
255
+ { name, data },
256
+ ]);
257
+ expect([400, 415]).toContain(res.status);
258
+ }
259
+ expect(sent).toHaveLength(0);
260
+ });
261
+
262
+ it("holds an image to the image limit", async () => {
263
+ const { ws, device } = setup();
264
+ const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
265
+ {
266
+ name: "big.png",
267
+ data: Buffer.concat([png(), Buffer.alloc(4_000_000)]),
268
+ },
269
+ ]);
270
+ expect(res.status).toBe(413);
271
+ });
272
+
196
273
  it("rejects bad meta", async () => {
197
274
  const { ws, device } = setup();
198
275
  expect((await post(ws, device, "{nope")).status).toBe(400);
@@ -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>
@@ -1905,6 +1905,22 @@
1905
1905
  "area": "Chat",
1906
1906
  "text": "Chats now accept HAR network captures (with logins and cookies hidden), iPhone photos and many more file types.",
1907
1907
  "url": "https://github.com/saadnvd1/agent-os/pull/548"
1908
+ },
1909
+ {
1910
+ "id": 549,
1911
+ "date": "2026-11-01",
1912
+ "kind": "improved",
1913
+ "area": "AgentOS",
1914
+ "text": "Files shared into the app are now checked against the same rules as chat attachments.",
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"
1908
1924
  }
1909
1925
  ]
1910
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.
@@ -72,7 +74,9 @@ export interface VoiceLimits {
72
74
  }
73
75
 
74
76
  export const INTAKE_LIMITS: IntakeLimits = {
75
- maxBytes: 25_000_000,
77
+ // All files together; each is also held to its kind's limit in
78
+ // lib/attachments/policy.ts (text up to maxTextBytes, so a big HAR fits).
79
+ maxBytes: 100_000_000,
76
80
  maxFiles: 8,
77
81
  maxInlineImages: 2,
78
82
  maxNoteChars: 280,
@@ -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
  }