@saadnvd1/agent-os 0.3.851 → 0.3.859
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/app/api/leads/adopt/route.ts +35 -0
- package/app/api/leads/proposals/route.ts +52 -0
- package/app/api/leads/route.test.ts +95 -0
- package/app/api/leads/route.ts +60 -0
- package/app/api/sessions/[id]/fresh/route.ts +2 -1
- package/app/api/sessions/[id]/route.ts +9 -1
- package/app/api/tmux/kill-all/route.ts +2 -1
- package/app/api/workspaces/[id]/orchestrator/tools/route.test.ts +43 -0
- package/app/api/workspaces/[id]/orchestrator/tools/route.ts +14 -11
- package/components/Analyst/pair.ts +2 -1
- package/components/Chat/ChatPanel.tsx +1 -0
- package/components/Chat/Forks.tsx +24 -2
- package/components/Orchestrator/AskCard.tsx +6 -0
- package/components/Orchestrator/NeedsYouStrip.tsx +7 -2
- package/components/Pane/index.tsx +5 -7
- package/components/Schedules/ScheduleForm.tsx +2 -1
- package/components/SessionList/RowSubtitle.tsx +2 -1
- package/components/SessionList/SessionRow.tsx +2 -1
- package/components/SessionList/useDoneAction.ts +8 -2
- package/components/SessionList/useSidebarData.ts +8 -3
- package/content/whats-new.json +24 -0
- package/data/done/index.ts +7 -1
- package/hooks/useNeedsYou.ts +10 -4
- package/hooks/useOrchestratorHeader.ts +2 -1
- package/lib/analyst/createLink.ts +3 -1
- package/lib/away/report.ts +2 -1
- package/lib/bus/format.ts +4 -1
- package/lib/bus/index.ts +9 -2
- package/lib/bus/read-scope.ts +4 -3
- package/lib/bus/resolve.ts +4 -1
- package/lib/bus/role-alias.ts +2 -1
- package/lib/bus/targets.ts +8 -3
- package/lib/bus/waits/tick.ts +2 -1
- package/lib/chat/compact-context.ts +2 -1
- package/lib/chat/mention-roots.ts +2 -1
- package/lib/chat/runner.ts +3 -2
- package/lib/chat/settings.ts +3 -2
- package/lib/chat/worker/extras.ts +4 -1
- package/lib/chat/worker/host.ts +6 -5
- package/lib/db/migrations/20261102T1944_leads.ts +142 -0
- package/lib/db/migrations/20261102T2300_needs_you_chat_per_session.ts +24 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +10 -2
- package/lib/done/archive.ts +3 -2
- package/lib/done/auto-archive.ts +2 -1
- package/lib/done/bulk.ts +4 -3
- package/lib/done/index.ts +3 -2
- package/lib/done/plan.ts +5 -2
- package/lib/experiments/registry.ts +11 -0
- package/lib/experiments/view.ts +3 -1
- package/lib/fork/brief.ts +2 -2
- package/lib/fork/extras.ts +4 -2
- package/lib/fork/guard.ts +4 -2
- package/lib/fork/scratch.ts +102 -12
- package/lib/fork/settle.ts +31 -7
- package/lib/fork/start.ts +5 -4
- package/lib/fork/tick.ts +19 -0
- package/lib/fork/types.ts +2 -0
- package/lib/fork/view.ts +10 -0
- package/lib/fork/worktree.ts +7 -3
- package/lib/health/env/sendFixNotes.ts +3 -2
- package/lib/hosts/peer-actions.ts +2 -1
- package/lib/hosts/peer-sessions.ts +3 -1
- package/lib/leads/actor.ts +16 -0
- package/lib/leads/adopt.ts +150 -0
- package/lib/leads/area.ts +77 -0
- package/lib/leads/brief-common.ts +122 -0
- package/lib/leads/brief.ts +155 -0
- package/lib/leads/charters.ts +99 -0
- package/lib/leads/deliver.ts +24 -0
- package/lib/leads/digest.ts +166 -0
- package/lib/leads/direction.ts +202 -0
- package/lib/leads/home.ts +196 -0
- package/lib/leads/index.ts +8 -0
- package/lib/leads/memos.ts +70 -0
- package/lib/leads/messages.ts +112 -0
- package/lib/leads/profiles.ts +83 -0
- package/lib/leads/roles.ts +114 -0
- package/lib/leads/settings.ts +55 -0
- package/lib/leads/templates.ts +51 -0
- package/lib/leads/tools.ts +159 -0
- package/lib/leads/types.ts +88 -0
- package/lib/leads/usage.ts +46 -0
- package/lib/learning-cards/input.ts +2 -1
- package/lib/live/drive.ts +2 -1
- package/lib/models/launch.ts +2 -1
- package/lib/models/session-role.ts +2 -1
- package/lib/needs-you.ts +12 -6
- package/lib/notify/index.ts +2 -1
- package/lib/orchestrator/act.ts +3 -1
- package/lib/orchestrator/ask-tool.ts +10 -0
- package/lib/orchestrator/ask-view.ts +2 -0
- package/lib/orchestrator/asks.ts +48 -8
- package/lib/orchestrator/brief.ts +32 -67
- package/lib/orchestrator/digest.ts +3 -2
- package/lib/orchestrator/facts.ts +2 -1
- package/lib/orchestrator/full-access.ts +2 -1
- package/lib/orchestrator/home.ts +8 -7
- package/lib/orchestrator/mcp-servers.ts +7 -6
- package/lib/orchestrator/notes.ts +15 -4
- package/lib/orchestrator/overview.ts +3 -1
- package/lib/orchestrator/recipe-tools.ts +2 -1
- package/lib/orchestrator/reminders.ts +2 -1
- package/lib/orchestrator/tools.ts +43 -13
- package/lib/orchestrator/worker.ts +45 -13
- package/lib/orchestrator/write-guard.ts +2 -2
- package/lib/projects.ts +3 -2
- package/lib/resurrect/find.ts +2 -1
- package/lib/resurrect/index.ts +3 -2
- package/lib/schedules/index.ts +3 -2
- package/lib/schedules/start.ts +2 -1
- package/lib/schedules/store.ts +3 -2
- package/lib/session-audit/sources.ts +4 -3
- package/lib/session-groups/workspace.ts +2 -1
- package/lib/sessions/launch.ts +2 -1
- package/lib/sidebar/machines.ts +2 -1
- package/lib/sidebar/row-actions.ts +2 -1
- package/lib/sidebar/row-badge.ts +3 -1
- package/lib/sidebar/row-menu.ts +6 -3
- package/lib/sidebar/shelves.ts +3 -3
- package/lib/status/collect.ts +4 -3
- package/lib/tasks/held-replies.ts +2 -1
- package/lib/tasks/move-targets.ts +2 -1
- package/lib/tasks/pause.ts +2 -1
- package/lib/tasks/starter.ts +3 -2
- package/lib/workspaces.ts +4 -3
- package/lib/worktree-sweep/findSettled.ts +2 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isStandingRole, isTechRole } from "@/lib/leads/roles";
|
|
3
4
|
import { useCallback, useEffect, useMemo } from "react";
|
|
4
5
|
import { useSnapshot } from "valtio";
|
|
5
6
|
import type { Session } from "@/lib/db";
|
|
@@ -92,7 +93,7 @@ export function useSidebarData(
|
|
|
92
93
|
const taskStates: Record<string, TaskState> = {};
|
|
93
94
|
for (const t of tasks) taskStates[t.id] = t.state;
|
|
94
95
|
const projectName = (s: Session) =>
|
|
95
|
-
s.role
|
|
96
|
+
isTechRole(s.role)
|
|
96
97
|
? `Orchestrator · ${workspaceName.get(s.workspace_id ?? "") ?? ""}`
|
|
97
98
|
: (byId.get(s.project_id ?? "")?.name ?? "");
|
|
98
99
|
const common = {
|
|
@@ -167,7 +168,11 @@ export function useSidebarData(
|
|
|
167
168
|
const counts = new Map<string, number>();
|
|
168
169
|
for (const s of sessions) {
|
|
169
170
|
const ws = byId.get(s.project_id ?? "");
|
|
170
|
-
if (
|
|
171
|
+
if (
|
|
172
|
+
isStandingRole(s.role) ||
|
|
173
|
+
!ws ||
|
|
174
|
+
sessionStatuses?.[s.id]?.status !== "running"
|
|
175
|
+
)
|
|
171
176
|
continue;
|
|
172
177
|
counts.set(ws, (counts.get(ws) ?? 0) + 1);
|
|
173
178
|
}
|
|
@@ -226,7 +231,7 @@ export function useSidebarData(
|
|
|
226
231
|
// Orchestrator session id -> the workspace it serves, for tags and chips.
|
|
227
232
|
const orchestratorKey = JSON.stringify(
|
|
228
233
|
sessions
|
|
229
|
-
.filter((s) => s.role
|
|
234
|
+
.filter((s) => isTechRole(s.role))
|
|
230
235
|
.map((s) => [
|
|
231
236
|
s.id,
|
|
232
237
|
workspaces.find((w) => w.id === s.workspace_id)?.name ?? s.name,
|
package/content/whats-new.json
CHANGED
|
@@ -2033,6 +2033,30 @@
|
|
|
2033
2033
|
"area": "Updates",
|
|
2034
2034
|
"text": "You can update AgentOS by hand again on machines that deploy themselves, and Update now skips the hourly wait.",
|
|
2035
2035
|
"url": "https://github.com/saadnvd1/agent-os/pull/571"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
"id": 573,
|
|
2039
|
+
"date": "2026-11-02",
|
|
2040
|
+
"kind": "new",
|
|
2041
|
+
"area": "AgentOS",
|
|
2042
|
+
"text": "Mark a fork as done straight from the Forks list above the chat box.",
|
|
2043
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/573"
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
"id": 574,
|
|
2047
|
+
"date": "2026-11-02",
|
|
2048
|
+
"kind": "fixed",
|
|
2049
|
+
"area": "AgentOS",
|
|
2050
|
+
"text": "Files a forked session writes are no longer deleted when it finishes.",
|
|
2051
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/574"
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
"id": 576,
|
|
2055
|
+
"date": "2026-11-02",
|
|
2056
|
+
"kind": "improved",
|
|
2057
|
+
"area": "AgentOS",
|
|
2058
|
+
"text": "Heavy builds and test runs on Linux machines are now memory-capped, so one runaway command can no longer freeze the whole machine.",
|
|
2059
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/576"
|
|
2036
2060
|
}
|
|
2037
2061
|
]
|
|
2038
2062
|
}
|
package/data/done/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ArchivedView } from "@/lib/done/archive";
|
|
|
5
5
|
import type { ResurrectOutcome } from "@/lib/resurrect";
|
|
6
6
|
import { sessionKeys } from "../sessions/keys";
|
|
7
7
|
import { taskKeys } from "../tasks";
|
|
8
|
+
import { forkKeys } from "../fork";
|
|
8
9
|
|
|
9
10
|
export const archivedKeys = {
|
|
10
11
|
all: ["archived-sessions"] as const,
|
|
@@ -52,7 +53,12 @@ function useDoneMutation<V, R>(fn: (v: V) => Promise<R>) {
|
|
|
52
53
|
return useMutation({
|
|
53
54
|
mutationFn: fn,
|
|
54
55
|
onSettled: () => {
|
|
55
|
-
for (const queryKey of [
|
|
56
|
+
for (const queryKey of [
|
|
57
|
+
sessionKeys.all,
|
|
58
|
+
taskKeys.all,
|
|
59
|
+
archivedKeys.all,
|
|
60
|
+
forkKeys.all,
|
|
61
|
+
])
|
|
56
62
|
queryClient.invalidateQueries({ queryKey });
|
|
57
63
|
},
|
|
58
64
|
});
|
package/hooks/useNeedsYou.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isStandingRole } from "@/lib/leads/roles";
|
|
3
4
|
import { useMemo } from "react";
|
|
4
5
|
import { useQuery } from "@tanstack/react-query";
|
|
5
6
|
import { useSessionsQuery } from "@/data/sessions/queries";
|
|
@@ -15,9 +16,11 @@ import {
|
|
|
15
16
|
} from "@/lib/orchestrator/needs-you-items";
|
|
16
17
|
|
|
17
18
|
// Everything waiting on Saad in a workspace: its open asks and questions, the
|
|
18
|
-
// tasks the sidebar marks as needing him, and
|
|
19
|
+
// tasks the sidebar marks as needing him, and this chat waiting on an answer
|
|
20
|
+
// (keyed by the chat, so one lead's waiting chat never hides another's).
|
|
19
21
|
export function useNeedsYou(
|
|
20
22
|
workspaceId: string,
|
|
23
|
+
sessionId: string,
|
|
21
24
|
chatWaiting: boolean
|
|
22
25
|
): NeedsYouItem[] {
|
|
23
26
|
const overview = useOrchestratorOverview(workspaceId);
|
|
@@ -35,7 +38,10 @@ export function useNeedsYou(
|
|
|
35
38
|
);
|
|
36
39
|
return (sessions?.sessions ?? []).flatMap((s): TaskNeed[] => {
|
|
37
40
|
const need = statuses?.statuses[s.id]?.need;
|
|
38
|
-
return !s.role
|
|
41
|
+
return !isStandingRole(s.role) &&
|
|
42
|
+
s.project_id &&
|
|
43
|
+
members.has(s.project_id) &&
|
|
44
|
+
need
|
|
39
45
|
? [{ sessionId: s.id, name: s.name, need }]
|
|
40
46
|
: [];
|
|
41
47
|
});
|
|
@@ -49,8 +55,8 @@ export function useNeedsYou(
|
|
|
49
55
|
tasks,
|
|
50
56
|
cards: cardsOn ? (overview?.cards ?? []) : [],
|
|
51
57
|
chatWaiting,
|
|
52
|
-
chatKey: `approval:${
|
|
58
|
+
chatKey: `approval:${sessionId}`,
|
|
53
59
|
}),
|
|
54
|
-
[overview, tasks, cardsOn, chatWaiting,
|
|
60
|
+
[overview, tasks, cardsOn, chatWaiting, sessionId]
|
|
55
61
|
);
|
|
56
62
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isStandingRole } from "@/lib/leads/roles";
|
|
3
4
|
import { useQuery } from "@tanstack/react-query";
|
|
4
5
|
import { useSessionsQuery } from "@/data/sessions/queries";
|
|
5
6
|
import { useProjectsQuery } from "@/data/projects/queries";
|
|
@@ -23,7 +24,7 @@ export function useWorkspaceRunning(workspaceId: string): number {
|
|
|
23
24
|
);
|
|
24
25
|
return (sessions?.sessions ?? []).filter(
|
|
25
26
|
(s) =>
|
|
26
|
-
!s.role &&
|
|
27
|
+
!isStandingRole(s.role) &&
|
|
27
28
|
s.project_id &&
|
|
28
29
|
members.has(s.project_id) &&
|
|
29
30
|
statuses?.statuses[s.id]?.status === "running"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole, RoleLabel } from "../leads/roles";
|
|
1
2
|
import { db, type Session } from "../db";
|
|
2
3
|
import { lastItems } from "../chat/store";
|
|
3
4
|
import { launchSession } from "../sessions/launch";
|
|
@@ -18,7 +19,8 @@ const sessionRow = (id: string) =>
|
|
|
18
19
|
export function coderRefusal(s: Session | undefined): string | null {
|
|
19
20
|
if (!s) return "Session not found";
|
|
20
21
|
if (s.archived_at) return `${s.name} is archived`;
|
|
21
|
-
if (s.role
|
|
22
|
+
if (isStandingRole(s.role))
|
|
23
|
+
return `${RoleLabel(s.role)} can't have an analyst`;
|
|
22
24
|
if (s.view !== "chat")
|
|
23
25
|
return "Only a chat session can have an analyst: a terminal has no turns to review";
|
|
24
26
|
if ((s.host_id || "local") !== "local")
|
package/lib/away/report.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* as Away ends and kept with the period, so it reads the same later.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { sqlNoRole } from "../leads/roles";
|
|
7
8
|
import { db } from "../db";
|
|
8
9
|
import { listWorkspaces } from "../workspaces";
|
|
9
10
|
import { HARD_LINES, openAsks } from "../orchestrator/asks";
|
|
@@ -178,7 +179,7 @@ function inFlight(workspaceId: string, awayId: number): InFlightItem[] {
|
|
|
178
179
|
`SELECT s.id, s.name, p.name AS project, s.pr_number, s.pr_url,
|
|
179
180
|
s.merge_armed_at AS armed_at
|
|
180
181
|
FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
181
|
-
WHERE p.workspace_id = ? AND s.role
|
|
182
|
+
WHERE p.workspace_id = ? AND ${sqlNoRole("s.role")} AND s.archived_at IS NULL
|
|
182
183
|
AND s.task_status = 'running'
|
|
183
184
|
AND EXISTS (SELECT 1 FROM away_actions a WHERE a.away_id = ?
|
|
184
185
|
AND a.subject = s.id AND a.kind IN (${TOUCHED}))
|
package/lib/bus/format.ts
CHANGED
|
@@ -81,6 +81,8 @@ export function matchesRef(
|
|
|
81
81
|
// orchestrator: "<workspace>/<name>" and "<workspace>/orchestrator".
|
|
82
82
|
workspaceName?: string | null;
|
|
83
83
|
orchestrator?: boolean;
|
|
84
|
+
// A lead's department: "<workspace>/<kind>".
|
|
85
|
+
leadKind?: string | null;
|
|
84
86
|
}
|
|
85
87
|
): boolean {
|
|
86
88
|
const r = ref.trim().toLowerCase();
|
|
@@ -92,6 +94,7 @@ export function matchesRef(
|
|
|
92
94
|
r === name ||
|
|
93
95
|
(!!s.projectName && r === `${s.projectName.toLowerCase()}/${name}`) ||
|
|
94
96
|
(!!ws && r === `${ws}/${name}`) ||
|
|
95
|
-
(!!ws && !!s.orchestrator && r === `${ws}/orchestrator`)
|
|
97
|
+
(!!ws && !!s.orchestrator && r === `${ws}/orchestrator`) ||
|
|
98
|
+
(!!ws && !!s.leadKind && r === `${ws}/${s.leadKind}`)
|
|
96
99
|
);
|
|
97
100
|
}
|
package/lib/bus/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the recipient's tmux pane, which works for any agent CLI.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isTechRole } from "../leads/roles";
|
|
7
8
|
import { db, type Session } from "../db";
|
|
8
9
|
import { statusDetector } from "../status-detector";
|
|
9
10
|
import { chatState, sendChatConfirmed } from "../chat/runner";
|
|
@@ -63,6 +64,8 @@ export interface Peer {
|
|
|
63
64
|
orchestratorOf: string | null;
|
|
64
65
|
// The asking session's own workspace orchestrator.
|
|
65
66
|
yours?: boolean;
|
|
67
|
+
// A lead's address, "<workspace>/<kind>", once its workspace has leads.
|
|
68
|
+
lead?: string;
|
|
66
69
|
// Names of the sessions it waits on, and of those waiting on it.
|
|
67
70
|
waitingOn: string[];
|
|
68
71
|
waitedBy: string[];
|
|
@@ -93,7 +96,7 @@ async function peerState(
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
const orchestratorOf = (s: Session) =>
|
|
96
|
-
s.role
|
|
99
|
+
isTechRole(s.role) && s.workspace_id
|
|
97
100
|
? (getWorkspace(s.workspace_id)?.name ?? "a workspace")
|
|
98
101
|
: null;
|
|
99
102
|
|
|
@@ -121,6 +124,10 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
121
124
|
running: info.running,
|
|
122
125
|
doing: info.subtitle,
|
|
123
126
|
orchestratorOf: orchestratorOf(s),
|
|
127
|
+
...(named[i].leadKind &&
|
|
128
|
+
named[i].workspaceName && {
|
|
129
|
+
lead: `${named[i].workspaceName}/${named[i].leadKind}`,
|
|
130
|
+
}),
|
|
124
131
|
waitingOn: badges.get(s.id)?.on.map((w) => w.name) ?? [],
|
|
125
132
|
waitedBy: badges.get(s.id)?.by.map((w) => w.name) ?? [],
|
|
126
133
|
...(s.agent_role && { agentRole: s.agent_role }),
|
|
@@ -130,7 +137,7 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
130
137
|
group: groups.get(s.session_group_id),
|
|
131
138
|
}),
|
|
132
139
|
...(callerWorkspace &&
|
|
133
|
-
s.role
|
|
140
|
+
isTechRole(s.role) &&
|
|
134
141
|
s.workspace_id === callerWorkspace && { yours: true }),
|
|
135
142
|
};
|
|
136
143
|
})
|
package/lib/bus/read-scope.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole, roleLabel } from "../leads/roles";
|
|
1
2
|
import { db } from "../db";
|
|
2
3
|
import type { Session } from "../db";
|
|
3
4
|
import { getProject } from "../projects";
|
|
@@ -58,7 +59,7 @@ function candidates(rows: ReadableSession[]): Candidate[] {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function inScope(s: ReadableSession, scope: ReaderScope): boolean {
|
|
61
|
-
if (s.role) return false;
|
|
62
|
+
if (isStandingRole(s.role)) return false;
|
|
62
63
|
if (scope.workspaceId) {
|
|
63
64
|
const p = s.project_id ? getProject(s.project_id) : null;
|
|
64
65
|
// A session in no project is in the workspace it was started in.
|
|
@@ -113,8 +114,8 @@ export function resolveReadable(callerId: string, ref: string): Readable {
|
|
|
113
114
|
return {
|
|
114
115
|
ok: false,
|
|
115
116
|
external: false,
|
|
116
|
-
error: s.role
|
|
117
|
-
? `${s.name} is
|
|
117
|
+
error: isStandingRole(s.role)
|
|
118
|
+
? `${s.name} is ${roleLabel(s.role)}: it isn't readable. Message it with aos send`
|
|
118
119
|
: `"${ref}" isn't in your ${scope.label}; aos read reaches only sessions there`,
|
|
119
120
|
};
|
|
120
121
|
}
|
package/lib/bus/resolve.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Candidate {
|
|
|
13
13
|
previousNames: string[];
|
|
14
14
|
workspaceName?: string | null;
|
|
15
15
|
orchestrator?: boolean;
|
|
16
|
+
// The department it heads, when its workspace has leads: "<workspace>/<kind>".
|
|
17
|
+
leadKind?: string | null;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type Resolution =
|
|
@@ -23,7 +25,7 @@ export const MIN_ID_PREFIX = 4;
|
|
|
23
25
|
|
|
24
26
|
const label = (c: Candidate) =>
|
|
25
27
|
`${
|
|
26
|
-
c.orchestrator && c.workspaceName
|
|
28
|
+
(c.orchestrator || c.leadKind) && c.workspaceName
|
|
27
29
|
? `${c.workspaceName}/`
|
|
28
30
|
: c.projectName
|
|
29
31
|
? `${c.projectName}/`
|
|
@@ -46,6 +48,7 @@ const matchesOld = (r: string, c: Candidate) =>
|
|
|
46
48
|
tmuxName: "",
|
|
47
49
|
name: old,
|
|
48
50
|
orchestrator: false,
|
|
51
|
+
leadKind: null,
|
|
49
52
|
})
|
|
50
53
|
);
|
|
51
54
|
|
package/lib/bus/role-alias.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* refuses and lists them, like any other name.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isTechRole } from "../leads/roles";
|
|
8
9
|
import type { Session } from "../db";
|
|
9
10
|
import { sessionWorkspace } from "../models/resolve";
|
|
10
11
|
|
|
@@ -60,7 +61,7 @@ export function resolveRoleAlias<T extends RoleSession>(
|
|
|
60
61
|
const workspace = caller ? sessionWorkspace(caller) : null;
|
|
61
62
|
if (role === "supervisor" && workspace) {
|
|
62
63
|
const orchestrator = others.find(
|
|
63
|
-
(s) => s.role
|
|
64
|
+
(s) => isTechRole(s.role) && s.workspace_id === workspace
|
|
64
65
|
);
|
|
65
66
|
if (orchestrator) return { ok: true, session: orchestrator };
|
|
66
67
|
}
|
package/lib/bus/targets.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Which live session a name someone typed reaches, across workspaces.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isTechRole, leadKindOf } from "../leads/roles";
|
|
6
|
+
import { hasLeads } from "../leads/settings";
|
|
5
7
|
import { db, type Session } from "../db";
|
|
6
8
|
import { getProject } from "../projects";
|
|
7
9
|
import { getWorkspace } from "../workspaces";
|
|
@@ -37,7 +39,10 @@ export function candidates(sessions: Session[]): Candidate[] {
|
|
|
37
39
|
tmuxName: s.tmux_name,
|
|
38
40
|
previousNames: old.get(s.id) ?? [],
|
|
39
41
|
workspaceName: workspaceName(s),
|
|
40
|
-
orchestrator: s.role
|
|
42
|
+
orchestrator: isTechRole(s.role),
|
|
43
|
+
// The orchestrator answers to "tech" only once its workspace has leads.
|
|
44
|
+
leadKind:
|
|
45
|
+
isTechRole(s.role) && !hasLeads(s.workspace_id) ? null : leadKindOf(s),
|
|
41
46
|
}));
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -77,7 +82,7 @@ function ownOrchestrator(
|
|
|
77
82
|
const workspace = caller ? sessionWorkspace(caller) : null;
|
|
78
83
|
if (!workspace) return undefined;
|
|
79
84
|
return sessions.find(
|
|
80
|
-
(s) => s.role
|
|
85
|
+
(s) => isTechRole(s.role) && s.workspace_id === workspace
|
|
81
86
|
);
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -95,7 +100,7 @@ export function tryResolveTarget(
|
|
|
95
100
|
const r = resolveRef(ref, candidates(sessions));
|
|
96
101
|
if (!r.ok) {
|
|
97
102
|
const orchestrators = /^(my )?orchestrator$/i.test(ref.trim())
|
|
98
|
-
? sessions.filter((s) => s.role
|
|
103
|
+
? sessions.filter((s) => isTechRole(s.role))
|
|
99
104
|
: [];
|
|
100
105
|
if (orchestrators.length)
|
|
101
106
|
return {
|
package/lib/bus/waits/tick.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* reads status through statusOf, the same idle test the orchestrator uses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../../leads/roles";
|
|
8
9
|
import type { Session } from "../../db";
|
|
9
10
|
import { db } from "../../db";
|
|
10
11
|
import { statusDetector } from "../../status-detector";
|
|
@@ -128,7 +129,7 @@ async function settle(w: WaitRow, waiter: Session, now: number): Promise<void> {
|
|
|
128
129
|
timeoutLine(target, w.kind, w.deadline - w.created_at, w.request, is)
|
|
129
130
|
);
|
|
130
131
|
const workspace = sessionWorkspace(waiter);
|
|
131
|
-
if (workspace && waiter.role
|
|
132
|
+
if (workspace && !isStandingRole(waiter.role))
|
|
132
133
|
queueEventOnce(
|
|
133
134
|
workspace,
|
|
134
135
|
`wait-timeout:${w.id}`,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* deploy restarted the worker too.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../leads/roles";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import { statusPorcelain } from "../git-exec";
|
|
10
11
|
import { db, type Session } from "../db";
|
|
@@ -33,7 +34,7 @@ export async function compactContext(sessionId: string): Promise<string> {
|
|
|
33
34
|
.get(sessionId) as
|
|
34
35
|
Pick<Session, "working_directory" | "task_prompt" | "role"> | undefined;
|
|
35
36
|
// An orchestrator starts fresh instead of compacting.
|
|
36
|
-
if (!session || session.role
|
|
37
|
+
if (!session || isStandingRole(session.role)) return "";
|
|
37
38
|
const tools = lastItems(sessionId, 400, "tool").filter(
|
|
38
39
|
(i) => i.kind === "tool"
|
|
39
40
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStandingRole } from "../leads/roles";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
@@ -35,7 +36,7 @@ export function mentionRoots(sessionId: string): MentionRoot[] | null {
|
|
|
35
36
|
working_directory: string;
|
|
36
37
|
}
|
|
37
38
|
| undefined;
|
|
38
|
-
if (session?.role
|
|
39
|
+
if (!isStandingRole(session?.role)) return null;
|
|
39
40
|
|
|
40
41
|
// Never the home folder or the whole disk: a project pointed there would
|
|
41
42
|
// list everything under it, dotfiles and keys included.
|
package/lib/chat/runner.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* events to whoever is watching. Restarting the server leaves turns running.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isStandingRole } from "../leads/roles";
|
|
8
9
|
import { pushPlanReady } from "../notify/push/events";
|
|
9
10
|
import {
|
|
10
11
|
handoffAfterPlan,
|
|
@@ -638,7 +639,7 @@ async function resumeCutOff(sessionId: string): Promise<void> {
|
|
|
638
639
|
const task =
|
|
639
640
|
row.workspace_id &&
|
|
640
641
|
row.task_status === "running" &&
|
|
641
|
-
row.role
|
|
642
|
+
!isStandingRole(row.role)
|
|
642
643
|
? row.workspace_id
|
|
643
644
|
: null;
|
|
644
645
|
try {
|
|
@@ -923,7 +924,7 @@ export async function freshStartChat(
|
|
|
923
924
|
id: `note-refresh-${Date.now()}`,
|
|
924
925
|
kind: "note",
|
|
925
926
|
tone: "note",
|
|
926
|
-
text: `Fresh start: the agent starts a new conversation with its brief${session.role
|
|
927
|
+
text: `Fresh start: the agent starts a new conversation with its brief${isStandingRole(session.role) ? " and the current state from AgentOS" : ""} at its next message.`,
|
|
927
928
|
createdAt: Date.now(),
|
|
928
929
|
};
|
|
929
930
|
saveItem(sessionId, item);
|
package/lib/chat/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* per-session settings that go with them: the model and the access level.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isStandingRole } from "../leads/roles";
|
|
6
7
|
import { effectiveChatAccess, noApprovalsOn } from "../approvals/no-approvals";
|
|
7
8
|
import os from "os";
|
|
8
9
|
import { agentLabel } from "../agent-options";
|
|
@@ -330,7 +331,7 @@ export async function setChatAccess(
|
|
|
330
331
|
): Promise<void> {
|
|
331
332
|
if (!CHAT_ACCESS.includes(access)) return;
|
|
332
333
|
// An orchestrator's access is fixed by its role.
|
|
333
|
-
if (getSession(sessionId).role
|
|
334
|
+
if (isStandingRole(getSession(sessionId).role)) return;
|
|
334
335
|
db.prepare(`UPDATE sessions SET chat_access = ? WHERE id = ?`).run(
|
|
335
336
|
access,
|
|
336
337
|
sessionId
|
|
@@ -341,7 +342,7 @@ export async function setChatAccess(
|
|
|
341
342
|
|
|
342
343
|
// An orchestrator's permission mode is fixed by its role.
|
|
343
344
|
const canPlan = (s: Session) =>
|
|
344
|
-
s.role
|
|
345
|
+
!isStandingRole(s.role) && chatDriverFor(s.agent_type)?.plan !== false;
|
|
345
346
|
|
|
346
347
|
// Plan mode: the agent reads and plans, and changes nothing until the plan
|
|
347
348
|
// is carried out. Now if a conversation is live, and for every next start.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLeadRole } from "../../leads/roles";
|
|
1
2
|
import type { Session } from "../../db";
|
|
2
3
|
import type { ChatStartOptions } from "../driver";
|
|
3
4
|
import { refreshSettings } from "../refresh";
|
|
@@ -40,7 +41,9 @@ export async function roleExtras(session: Session): Promise<RoleExtras> {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async function ownExtras(session: Session): Promise<RoleExtras> {
|
|
43
|
-
|
|
44
|
+
// The orchestrator and the other leads run on the orchestrator's worker,
|
|
45
|
+
// each with its own profile.
|
|
46
|
+
if (isLeadRole(session.role))
|
|
44
47
|
return (await import("../../orchestrator/worker")).orchestratorExtras(
|
|
45
48
|
session
|
|
46
49
|
);
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* events to whoever is connected.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isStandingRole } from "../../leads/roles";
|
|
7
8
|
import os from "os";
|
|
8
9
|
import { db, type Session } from "../../db";
|
|
9
10
|
import { agentEnv } from "../../agents/launch";
|
|
@@ -216,7 +217,7 @@ export class ChatHost {
|
|
|
216
217
|
this.atRest = driver.atRest !== false;
|
|
217
218
|
const cwd = session.working_directory.replace(/^~/, os.homedir());
|
|
218
219
|
const env = { ...agentEnv(session.id), ...extras.env };
|
|
219
|
-
const orchestrator = session.role
|
|
220
|
+
const orchestrator = isStandingRole(session.role);
|
|
220
221
|
// Every chat can show visuals, asked for like any tool under its access
|
|
221
222
|
// setting (they read files and reach the web). An orchestrator reads
|
|
222
223
|
// other sessions' text, so it gets a shorter brief that says what stays
|
|
@@ -289,7 +290,7 @@ export class ChatHost {
|
|
|
289
290
|
if (session.chat_refreshed)
|
|
290
291
|
this.refreshedNote = refreshedNote(
|
|
291
292
|
extras.refresh?.role ??
|
|
292
|
-
(session.role
|
|
293
|
+
(isStandingRole(session.role) ? "orchestrator" : "task"),
|
|
293
294
|
session.chat_refreshed
|
|
294
295
|
);
|
|
295
296
|
// Told only an orchestrator whose tools changed since its last worker:
|
|
@@ -964,7 +965,7 @@ export class ChatHost {
|
|
|
964
965
|
const stale = this.staleNow(cmd.compact);
|
|
965
966
|
// An orchestrator starts over at its own threshold (lib/chat/refresh);
|
|
966
967
|
// only one gone stale does it early.
|
|
967
|
-
if (!stale || (this.session.role
|
|
968
|
+
if (!stale || (isStandingRole(this.session.role) && !stale.stale))
|
|
968
969
|
return false;
|
|
969
970
|
try {
|
|
970
971
|
enqueue(this.session.id, cmd);
|
|
@@ -978,7 +979,7 @@ export class ChatHost {
|
|
|
978
979
|
// from tasks and CI without its untrusted fence. It starts a fresh
|
|
979
980
|
// conversation from its brief and the database instead, as at its
|
|
980
981
|
// threshold, and the next worker sends the message.
|
|
981
|
-
if (this.session.role
|
|
982
|
+
if (isStandingRole(this.session.role)) {
|
|
982
983
|
this.staleRefresh = stale;
|
|
983
984
|
this.refreshAt = stale.tokens;
|
|
984
985
|
this.retiring = true;
|
|
@@ -1085,7 +1086,7 @@ export class ChatHost {
|
|
|
1085
1086
|
// line saying where it started over.
|
|
1086
1087
|
private endConversation(): void {
|
|
1087
1088
|
releaseConversation(this.session.id, this.refreshAt);
|
|
1088
|
-
const brief = `its brief${this.session.role
|
|
1089
|
+
const brief = `its brief${isStandingRole(this.session.role) ? " and the current state from AgentOS" : ""}`;
|
|
1089
1090
|
const stale = this.staleRefresh;
|
|
1090
1091
|
const requested = this.refreshAt === REQUESTED_FRESH;
|
|
1091
1092
|
this.record({
|