@saadnvd1/agent-os 0.3.715 → 0.3.723
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/README.md +1 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/spawn/route.ts +22 -1
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/route.ts +16 -0
- package/app/page.tsx +5 -0
- package/components/Chat/Messages.tsx +104 -10
- package/components/Draft/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +51 -2
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/SessionRow.tsx +18 -7
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/push/topics.ts +5 -0
- package/data/sessions/queries.ts +26 -0
- package/lib/agents/brief.ts +1 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +5 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +3 -1
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestrator/facts.ts +7 -2
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/projects.ts +1 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +49 -9
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/row-menu.ts +6 -1
- package/lib/tasks/index.ts +6 -0
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/package.json +1 -1
- package/server.ts +9 -1
- package/stores/exportUi.ts +87 -0
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type ItemRow,
|
|
33
33
|
type RunRow,
|
|
34
34
|
} from "./derive";
|
|
35
|
+
import { agentosSignature, fromAgentosDirs, fromKept } from "./agentos-files";
|
|
35
36
|
import { keepFiles, readManifest, relInside, sessionKeptDir } from "./keep";
|
|
36
37
|
|
|
37
38
|
export const MAX_FILES = 500;
|
|
@@ -160,6 +161,16 @@ async function build(session: Session): Promise<Entry[]> {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
const manifest = worktree && !worktreeThere ? readManifest(session.id) : null;
|
|
164
|
+
if (worktree) {
|
|
165
|
+
const known = new Set(candidates.map((c) => c.key));
|
|
166
|
+
candidates.push(
|
|
167
|
+
...(worktreeThere
|
|
168
|
+
? fromAgentosDirs(worktree, known)
|
|
169
|
+
: manifest
|
|
170
|
+
? fromKept(worktree, manifest.kept, sqliteMs(manifest.at), known)
|
|
171
|
+
: [])
|
|
172
|
+
);
|
|
173
|
+
}
|
|
163
174
|
if (worktree) {
|
|
164
175
|
const planFile = path.join(worktree, PLAN_FILE);
|
|
165
176
|
const named = candidates.some((c) => c.key === `file:${planFile}`);
|
|
@@ -255,6 +266,9 @@ interface Cached {
|
|
|
255
266
|
}
|
|
256
267
|
const cache = new Map<string, Cached>();
|
|
257
268
|
|
|
269
|
+
const isLocalWorktree = (s: Session) =>
|
|
270
|
+
!!s.worktree_path && (!s.host_id || s.host_id === "local");
|
|
271
|
+
|
|
258
272
|
function signature(session: Session): string {
|
|
259
273
|
const row = db
|
|
260
274
|
.prepare(`SELECT max(seq) AS seq FROM chat_items WHERE session_id = ?`)
|
|
@@ -265,6 +279,9 @@ function signature(session: Session): string {
|
|
|
265
279
|
session.pr_number,
|
|
266
280
|
session.worktree_path,
|
|
267
281
|
session.updated_at,
|
|
282
|
+
isLocalWorktree(session)
|
|
283
|
+
? agentosSignature(expand(session.worktree_path!))
|
|
284
|
+
: "",
|
|
268
285
|
].join("|");
|
|
269
286
|
}
|
|
270
287
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A crew: the sessions started in roles that work together. Its id is the
|
|
3
|
+
* session that began it, so a crew needs no table of its own.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from "../db";
|
|
7
|
+
import type { AgentRole } from "../models/agent-roles";
|
|
8
|
+
|
|
9
|
+
const crewOf = (id: string | null | undefined): string | null =>
|
|
10
|
+
id
|
|
11
|
+
? ((
|
|
12
|
+
db.prepare(`SELECT crew_id FROM session_rows WHERE id = ?`).get(id) as
|
|
13
|
+
{ crew_id: string | null } | undefined
|
|
14
|
+
)?.crew_id ?? null)
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
// Puts a session in a role and a crew: the one named, else its starter's (or
|
|
18
|
+
// its calling session's), else a new one it begins.
|
|
19
|
+
export function joinCrew(
|
|
20
|
+
id: string,
|
|
21
|
+
role: AgentRole,
|
|
22
|
+
from: {
|
|
23
|
+
crewId?: string | null;
|
|
24
|
+
startedBy?: string | null;
|
|
25
|
+
parent?: string | null;
|
|
26
|
+
}
|
|
27
|
+
): string {
|
|
28
|
+
const crew =
|
|
29
|
+
from.crewId || crewOf(from.startedBy) || crewOf(from.parent) || id;
|
|
30
|
+
db.prepare(
|
|
31
|
+
`UPDATE sessions SET agent_role = ?, crew_id = ? WHERE id = ?`
|
|
32
|
+
).run(role, crew, id);
|
|
33
|
+
return crew;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CrewMember {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
agentRole: string | null;
|
|
40
|
+
model: string | null;
|
|
41
|
+
modelProvider: string | null;
|
|
42
|
+
agentType: string;
|
|
43
|
+
taskStatus: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function crewMembers(crewId: string): CrewMember[] {
|
|
47
|
+
return (
|
|
48
|
+
db
|
|
49
|
+
.prepare(
|
|
50
|
+
`SELECT id, name, agent_role, model, model_provider, agent_type, task_status
|
|
51
|
+
FROM session_rows WHERE crew_id = ? AND archived_at IS NULL
|
|
52
|
+
ORDER BY created_at`
|
|
53
|
+
)
|
|
54
|
+
.all(crewId) as {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
agent_role: string | null;
|
|
58
|
+
model: string | null;
|
|
59
|
+
model_provider: string | null;
|
|
60
|
+
agent_type: string;
|
|
61
|
+
task_status: string | null;
|
|
62
|
+
}[]
|
|
63
|
+
).map((r) => ({
|
|
64
|
+
id: r.id,
|
|
65
|
+
name: r.name,
|
|
66
|
+
agentRole: r.agent_role,
|
|
67
|
+
model: r.model,
|
|
68
|
+
modelProvider: r.model_provider,
|
|
69
|
+
agentType: r.agent_type,
|
|
70
|
+
taskStatus: r.task_status,
|
|
71
|
+
}));
|
|
72
|
+
}
|
package/lib/sessions/launch.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { discoversModels, resolveModelForAgent } from "../model-catalog";
|
|
|
16
16
|
import { isDiscoveredModel } from "../chat/discovered-models";
|
|
17
17
|
import { parseOverride, resolveRole, setSessionModel } from "../models/resolve";
|
|
18
18
|
import { ANTHROPIC } from "../models/roles";
|
|
19
|
+
import { joinCrew } from "./crew";
|
|
20
|
+
import { prepareRoleStart } from "../models/agent-binding";
|
|
21
|
+
import { isAgentRole, roleBrief, type AgentRole } from "../models/agent-roles";
|
|
19
22
|
import { getProject } from "../projects";
|
|
20
23
|
import { getWorkspace } from "../workspaces";
|
|
21
24
|
import { getHost, isRemoteHost } from "../hosts";
|
|
@@ -84,6 +87,13 @@ export interface LaunchInput {
|
|
|
84
87
|
// follows the project; one in none keeps this (see inWorkspace).
|
|
85
88
|
workspaceId?: string | null;
|
|
86
89
|
systemPrompt?: string | null;
|
|
90
|
+
// Start it in a role: the model comes from the role's binding (or
|
|
91
|
+
// `model`), and its brief joins the instructions.
|
|
92
|
+
agentRole?: AgentRole | null;
|
|
93
|
+
// The crew it joins: the id of the session that began it.
|
|
94
|
+
crewId?: string | null;
|
|
95
|
+
// When the role's model is down, start on the role's fallback instead.
|
|
96
|
+
useFallback?: boolean;
|
|
87
97
|
// The user's group it starts in (lib/session-groups).
|
|
88
98
|
groupId?: string;
|
|
89
99
|
}
|
|
@@ -261,7 +271,17 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
261
271
|
if (scratch && projectPath === SCRATCH_DIR)
|
|
262
272
|
await fs.promises.mkdir(SCRATCH_DIR, { recursive: true });
|
|
263
273
|
|
|
264
|
-
|
|
274
|
+
if (input.agentRole && (remote || !isAgentRole(input.agentRole)))
|
|
275
|
+
throw new Error("A role session runs on this machine");
|
|
276
|
+
const roleStart = input.agentRole
|
|
277
|
+
? await prepareRoleStart(input.agentRole, {
|
|
278
|
+
workspaceId: project?.workspace_id ?? home,
|
|
279
|
+
projectId: project && !project.is_uncategorized ? project.id : null,
|
|
280
|
+
override: input.model?.trim() || null,
|
|
281
|
+
useFallback: input.useFallback,
|
|
282
|
+
})
|
|
283
|
+
: null;
|
|
284
|
+
const agentType: AgentType = roleStart?.agentType ?? input.agentType;
|
|
265
285
|
const view =
|
|
266
286
|
input.view === "terminal" || !supportsChat(agentType) || remote
|
|
267
287
|
? "terminal"
|
|
@@ -273,20 +293,30 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
273
293
|
const picked = input.startedBy ? parseOverride(input.model) : null;
|
|
274
294
|
if (picked && typeof picked !== "string")
|
|
275
295
|
throw new Error(`model: a session takes one "provider/model"`);
|
|
276
|
-
const routed =
|
|
277
|
-
agentType === "claude"
|
|
296
|
+
const routed = roleStart
|
|
297
|
+
? roleStart.agentType === "claude"
|
|
298
|
+
? roleStart.routed
|
|
299
|
+
: null
|
|
300
|
+
: agentType === "claude" && (picked || !input.model?.trim()) && !remote
|
|
278
301
|
? resolveRole(role, {
|
|
279
302
|
workspaceId: project?.workspace_id ?? home,
|
|
280
303
|
override: picked,
|
|
281
304
|
})
|
|
282
305
|
: null;
|
|
283
|
-
if (
|
|
306
|
+
if (
|
|
307
|
+
(picked || roleStart) &&
|
|
308
|
+
routed &&
|
|
309
|
+
routed.provider.id !== ANTHROPIC &&
|
|
310
|
+
view !== "chat"
|
|
311
|
+
)
|
|
284
312
|
throw new Error("Another provider's models run only as a chat");
|
|
285
313
|
const interactive =
|
|
286
314
|
routed && (routed.provider.id === ANTHROPIC || view === "chat")
|
|
287
315
|
? routed
|
|
288
316
|
: null;
|
|
289
|
-
const wanted =
|
|
317
|
+
const wanted = roleStart
|
|
318
|
+
? roleStart.model
|
|
319
|
+
: input.model?.trim() || project?.default_model;
|
|
290
320
|
// An agent without a static list runs what it reports for this folder.
|
|
291
321
|
const discovered =
|
|
292
322
|
!interactive &&
|
|
@@ -295,9 +325,11 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
295
325
|
(await isDiscoveredModel(agentType, projectPath, wanted));
|
|
296
326
|
const model = interactive
|
|
297
327
|
? interactive.model
|
|
298
|
-
:
|
|
299
|
-
?
|
|
300
|
-
:
|
|
328
|
+
: roleStart
|
|
329
|
+
? roleStart.model
|
|
330
|
+
: discovered && wanted
|
|
331
|
+
? wanted
|
|
332
|
+
: resolveModelForAgent(agentType, wanted);
|
|
301
333
|
const text = input.prompt?.trim() ?? "";
|
|
302
334
|
const naming: Naming = text
|
|
303
335
|
? await nameFor(text, projectPath, input.name)
|
|
@@ -318,7 +350,9 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
318
350
|
cwd,
|
|
319
351
|
input.parentSessionId ?? null,
|
|
320
352
|
model,
|
|
321
|
-
input.systemPrompt
|
|
353
|
+
[input.systemPrompt, roleBrief(input.agentRole)]
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.join("\n\n") || null,
|
|
322
356
|
"sessions",
|
|
323
357
|
agentType,
|
|
324
358
|
input.autoApprove || input.access === "full" ? 1 : 0,
|
|
@@ -337,6 +371,12 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
337
371
|
id
|
|
338
372
|
);
|
|
339
373
|
setHome(id, home);
|
|
374
|
+
if (input.agentRole)
|
|
375
|
+
joinCrew(id, input.agentRole, {
|
|
376
|
+
crewId: input.crewId,
|
|
377
|
+
startedBy: input.startedBy,
|
|
378
|
+
parent: input.parentSessionId,
|
|
379
|
+
});
|
|
340
380
|
if (input.groupId) fileInGroup(id, input.groupId);
|
|
341
381
|
if (interactive) setSessionModel(id, interactive);
|
|
342
382
|
if (input.access)
|
package/lib/settings-url.ts
CHANGED
package/lib/share/markdown.ts
CHANGED
|
@@ -83,13 +83,13 @@ export function htmlToMarkdown(html: string, demote = 0): string {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** A fenced block whose fence is longer than any backtick run inside. */
|
|
86
|
-
function fenced(text: string): string {
|
|
86
|
+
export function fenced(text: string, lang = ""): string {
|
|
87
87
|
const longest = Math.max(
|
|
88
88
|
2,
|
|
89
89
|
...(text.match(/`+/g) ?? []).map((m) => m.length)
|
|
90
90
|
);
|
|
91
91
|
const fence = "`".repeat(longest + 1);
|
|
92
|
-
return `${fence}\n${text}\n${fence}`;
|
|
92
|
+
return `${fence}${lang}\n${text}\n${fence}`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function itemToMarkdown(item: SnapshotItem): string {
|
package/lib/share/redact.ts
CHANGED
|
@@ -97,10 +97,12 @@ function sub(
|
|
|
97
97
|
|
|
98
98
|
export function redactForShare(
|
|
99
99
|
input: string,
|
|
100
|
-
ctx: ShareContext = NO_CONTEXT
|
|
100
|
+
ctx: ShareContext = NO_CONTEXT,
|
|
101
|
+
opts: { only?: Kind[] } = {}
|
|
101
102
|
): { text: string; hits: Hits } {
|
|
102
103
|
const hits = noHits();
|
|
103
104
|
let t = input;
|
|
105
|
+
const on = (k: Kind) => !opts.only || opts.only.includes(k);
|
|
104
106
|
|
|
105
107
|
// 1. Credentials by shape.
|
|
106
108
|
t = sub(t, PRIVATE_KEY_BLOCK, SECRET, hits, "secret");
|
|
@@ -143,7 +145,7 @@ export function redactForShare(
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
// 3. Home paths, and the account name before an @.
|
|
146
|
-
if (ctx.home.length > 1)
|
|
148
|
+
if (on("path") && ctx.home.length > 1)
|
|
147
149
|
t = sub(
|
|
148
150
|
t,
|
|
149
151
|
new RegExp(escape(ctx.home) + "(?![\\w.-])", "g"),
|
|
@@ -151,8 +153,8 @@ export function redactForShare(
|
|
|
151
153
|
hits,
|
|
152
154
|
"path"
|
|
153
155
|
);
|
|
154
|
-
t = sub(t, new RegExp(HOME.source, "g"), "~", hits, "path");
|
|
155
|
-
if (ctx.user.length > 2)
|
|
156
|
+
if (on("path")) t = sub(t, new RegExp(HOME.source, "g"), "~", hits, "path");
|
|
157
|
+
if (on("path") && ctx.user.length > 2)
|
|
156
158
|
t = sub(
|
|
157
159
|
t,
|
|
158
160
|
new RegExp(`\\b${escape(ctx.user)}(?=@)`, "g"),
|
|
@@ -162,14 +164,16 @@ export function redactForShare(
|
|
|
162
164
|
);
|
|
163
165
|
|
|
164
166
|
// 4. Addresses.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
if (on("ip")) {
|
|
168
|
+
t = sub(t, IPV4, "[ip]", hits, "ip", (m) => KEEP_IPV4.has(m));
|
|
169
|
+
t = sub(t, IPV6, "[ip]", hits, "ip", (m) => KEEP_IPV6.has(m));
|
|
170
|
+
}
|
|
167
171
|
|
|
168
172
|
// 5. This machine's names, and private-network suffixes.
|
|
169
173
|
const names = [...new Set(ctx.hosts.filter((h) => h.length > 2))].sort(
|
|
170
174
|
(a, b) => b.length - a.length
|
|
171
175
|
);
|
|
172
|
-
if (names.length)
|
|
176
|
+
if (on("host") && names.length)
|
|
173
177
|
t = sub(
|
|
174
178
|
t,
|
|
175
179
|
new RegExp(
|
|
@@ -180,11 +184,13 @@ export function redactForShare(
|
|
|
180
184
|
hits,
|
|
181
185
|
"host"
|
|
182
186
|
);
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
if (on("host")) {
|
|
188
|
+
t = sub(t, CONNECT_HOST, "[host]", hits, "host");
|
|
189
|
+
t = sub(t, LOCAL_HOST, "[host]", hits, "host");
|
|
190
|
+
}
|
|
185
191
|
|
|
186
192
|
// 6. Email addresses.
|
|
187
|
-
t = sub(t, EMAIL, "[email]", hits, "email");
|
|
193
|
+
if (on("email")) t = sub(t, EMAIL, "[email]", hits, "email");
|
|
188
194
|
|
|
189
195
|
return { text: t, hits };
|
|
190
196
|
}
|
package/lib/share/render.tsx
CHANGED
|
@@ -60,7 +60,7 @@ const components: Components = {
|
|
|
60
60
|
img: ({ alt }) => <span>[image{alt ? `: ${alt}` : ""}]</span>,
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
function Markdown({ text }: { text: string }) {
|
|
63
|
+
export function Markdown({ text }: { text: string }) {
|
|
64
64
|
return (
|
|
65
65
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
|
66
66
|
{text}
|
package/lib/share/snapshot.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface Snapshot {
|
|
|
44
44
|
|
|
45
45
|
export const MAX_TOOL_INPUT = 4_000;
|
|
46
46
|
export const MAX_TOOL_OUTPUT = 20_000;
|
|
47
|
-
const HIDDEN_FILE = "[contents of a secret file hidden]";
|
|
47
|
+
export const HIDDEN_FILE = "[contents of a secret file hidden]";
|
|
48
48
|
const NO_IMAGE = "[image not shared]";
|
|
49
49
|
|
|
50
50
|
const clip = (s: string, max: number) =>
|
|
@@ -55,7 +55,7 @@ const clip = (s: string, max: number) =>
|
|
|
55
55
|
const excerptOf = (s: string) => s.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
56
56
|
|
|
57
57
|
// Every string in a tool's input, to see whether it names a secret file.
|
|
58
|
-
function strings(v: unknown, out: string[] = []): string[] {
|
|
58
|
+
export function strings(v: unknown, out: string[] = []): string[] {
|
|
59
59
|
if (typeof v === "string") out.push(v);
|
|
60
60
|
else if (Array.isArray(v)) v.forEach((x) => strings(x, out));
|
|
61
61
|
else if (v && typeof v === "object")
|
package/lib/sidebar/row-menu.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type RowMenuItem =
|
|
|
16
16
|
| "open-recipe"
|
|
17
17
|
| "copy-link"
|
|
18
18
|
| "share"
|
|
19
|
+
| "export"
|
|
19
20
|
| "fork"
|
|
20
21
|
| "fresh-start"
|
|
21
22
|
| "new-conversation"
|
|
@@ -121,7 +122,11 @@ export function rowMenu(input: RowMenuInput): RowMenuPlan {
|
|
|
121
122
|
input.hasCard && "card",
|
|
122
123
|
input.recipeRun && "open-recipe",
|
|
123
124
|
]),
|
|
124
|
-
...group("share", [
|
|
125
|
+
...group("share", [
|
|
126
|
+
"copy-link",
|
|
127
|
+
s.view === "chat" && "share",
|
|
128
|
+
s.view === "chat" && "export",
|
|
129
|
+
]),
|
|
125
130
|
...group("restart", [
|
|
126
131
|
s.agent_type === "claude" && agentRow && live && "fork",
|
|
127
132
|
agentRow && live && "fresh-start",
|
package/lib/tasks/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import { saidSinceLastMessage } from "../chat/store";
|
|
|
67
67
|
|
|
68
68
|
import { lastActiveOf } from "./last-active";
|
|
69
69
|
import { setPlan, setTask } from "../db/session-parts";
|
|
70
|
+
import { joinCrew } from "../sessions/crew";
|
|
70
71
|
export * from "./state";
|
|
71
72
|
export { codeReviewGate, codeReviewRefusal } from "./code-review";
|
|
72
73
|
export { signOffTask, dropTask, signingOff, mergeSettled } from "./finish";
|
|
@@ -414,6 +415,11 @@ async function startTask(
|
|
|
414
415
|
startedVia: opts.startedVia,
|
|
415
416
|
startedByDevice: opts.held?.device ?? opts.startedByDevice,
|
|
416
417
|
});
|
|
418
|
+
// A task is the Developer of its crew.
|
|
419
|
+
joinCrew(id, "developer", {
|
|
420
|
+
startedBy: opts.startedBy,
|
|
421
|
+
parent: opts.parentSessionId,
|
|
422
|
+
});
|
|
417
423
|
} catch (error) {
|
|
418
424
|
// The claim was made for a session that never existed.
|
|
419
425
|
if (opts.pieceId) releaseClaim(opts.pieceId, id);
|
|
@@ -21,7 +21,7 @@ import { promisify } from "util";
|
|
|
21
21
|
import { gitResult, isGitError } from "../git-exec";
|
|
22
22
|
import { findNodeModules } from "./clone";
|
|
23
23
|
import { fastCopyMode } from "./fastcopy";
|
|
24
|
-
import { inside, realExisting } from "./paths";
|
|
24
|
+
import { STAGING_SUFFIX, ignoreStaging, inside, realExisting } from "./paths";
|
|
25
25
|
import { copyTree } from "./spare";
|
|
26
26
|
import type { CopyMode } from "./spare";
|
|
27
27
|
|
|
@@ -234,7 +234,8 @@ export async function seedBuildCaches(opts: {
|
|
|
234
234
|
skip(rel, "its folder in the worktree resolves outside it");
|
|
235
235
|
continue;
|
|
236
236
|
}
|
|
237
|
-
const partial = `${dest}
|
|
237
|
+
const partial = `${dest}${STAGING_SUFFIX}`;
|
|
238
|
+
await ignoreStaging(dir);
|
|
238
239
|
await fs.promises.rm(partial, { recursive: true, force: true });
|
|
239
240
|
const ok =
|
|
240
241
|
(await copyTree(real, partial, mode)) &&
|
|
@@ -10,6 +10,7 @@ import { createHash } from "crypto";
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
import { LOCKFILES } from "./install";
|
|
13
|
+
import { STAGING_SUFFIX, ignoreStaging } from "./paths";
|
|
13
14
|
import { STORE_ROOT, fastCopyMode, unlinkMutable } from "./fastcopy";
|
|
14
15
|
import type { FastMode } from "./fastcopy";
|
|
15
16
|
import {
|
|
@@ -79,7 +80,8 @@ async function cloneWhole(
|
|
|
79
80
|
target: string,
|
|
80
81
|
mode: FastMode
|
|
81
82
|
): Promise<boolean> {
|
|
82
|
-
const partial = `${target}
|
|
83
|
+
const partial = `${target}${STAGING_SUFFIX}`;
|
|
84
|
+
await ignoreStaging(path.dirname(target));
|
|
83
85
|
await fs.promises.rm(partial, { recursive: true, force: true });
|
|
84
86
|
const ok =
|
|
85
87
|
(await copyTree(from, partial, mode)) && (await takeSpare(partial, target));
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
+
import { git } from "../git-exec";
|
|
9
10
|
|
|
10
11
|
export const inside = (root: string, p: string) =>
|
|
11
12
|
p === root || p.startsWith(root + path.sep);
|
|
@@ -18,3 +19,28 @@ export async function realExisting(p: string): Promise<string> {
|
|
|
18
19
|
if (path.dirname(at) === at) return at;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
export const STAGING_SUFFIX = ".aos-partial";
|
|
24
|
+
|
|
25
|
+
const EXCLUDE_LINE = `*${STAGING_SUFFIX}`;
|
|
26
|
+
|
|
27
|
+
// Staging dirs (`node_modules.aos-partial`) sit beside their target, where git
|
|
28
|
+
// would list them as untracked work. Written once to the repo's shared
|
|
29
|
+
// info/exclude, so it covers every worktree and no .gitignore is touched.
|
|
30
|
+
// Best effort: outside a repository there is nothing to ignore it in.
|
|
31
|
+
export async function ignoreStaging(dir: string): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
const common = (
|
|
34
|
+
await git(dir, ["rev-parse", "--git-common-dir"], { timeout: 5000 })
|
|
35
|
+
).trim();
|
|
36
|
+
if (!common) return;
|
|
37
|
+
const file = path.join(path.resolve(dir, common), "info", "exclude");
|
|
38
|
+
const current = await fs.promises.readFile(file, "utf8").catch(() => "");
|
|
39
|
+
if (current.split(/\r?\n/).some((l) => l.trim() === EXCLUDE_LINE)) return;
|
|
40
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
41
|
+
const lead = current && !current.endsWith("\n") ? "\n" : "";
|
|
42
|
+
await fs.promises.appendFile(file, `${lead}${EXCLUDE_LINE}\n`);
|
|
43
|
+
} catch {
|
|
44
|
+
// never fails a setup
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -6,6 +6,8 @@ import next from "next";
|
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
import { ensureBusBrief } from "./lib/agents/launch";
|
|
8
8
|
import { reattachChats } from "./lib/chat/runner";
|
|
9
|
+
import { setTurnEndHook } from "./lib/chat/registry";
|
|
10
|
+
import { analystTurnEndHook, resumeAnalystLinks } from "./lib/analyst";
|
|
9
11
|
import { serveChatSocket } from "./lib/chat/socket";
|
|
10
12
|
import { startStackWatcher } from "./lib/stacks";
|
|
11
13
|
import { startPauseWatcher } from "./lib/tasks/pause";
|
|
@@ -224,6 +226,8 @@ app.prepare().then(async () => {
|
|
|
224
226
|
const folder = sessionFolder(id);
|
|
225
227
|
if (folder) refreshGitSoon(folder);
|
|
226
228
|
});
|
|
229
|
+
// An analyst hears each of its coding agent's turns as they end.
|
|
230
|
+
setTurnEndHook(analystTurnEndHook);
|
|
227
231
|
// Terminal sessions are watched through tmux control mode, or tapped with
|
|
228
232
|
// pipe-pane where tmux is too old for it.
|
|
229
233
|
const control = await startTmuxControl();
|
|
@@ -453,7 +457,11 @@ app.prepare().then(async () => {
|
|
|
453
457
|
void sweepSims();
|
|
454
458
|
setInterval(sweepSims, 5 * 60_000);
|
|
455
459
|
// Chat turns that kept running through a restart.
|
|
456
|
-
void reattachChats().finally(() =>
|
|
460
|
+
void reattachChats().finally(() => {
|
|
461
|
+
startPauseWatcher();
|
|
462
|
+
// Digests a restart cut off, and turns that ended while it was down.
|
|
463
|
+
void resumeAnalystLinks();
|
|
464
|
+
});
|
|
457
465
|
// Older chats get into the search index in the background, a batch at a time.
|
|
458
466
|
if (process.env.AGENTOS_SEARCH_INDEX !== "off") startConversationBackfill();
|
|
459
467
|
// Reviews this restart cut off run again and deliver their verdicts.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_REQUEST,
|
|
4
|
+
type ExportRequest,
|
|
5
|
+
type ExportTurnInfo,
|
|
6
|
+
} from "@/lib/export/view";
|
|
7
|
+
|
|
8
|
+
// The export sheet, and picking turns in the chat for it. The request lives
|
|
9
|
+
// here so it survives closing the sheet to select turns.
|
|
10
|
+
export const exportUi = proxy<{
|
|
11
|
+
open: boolean;
|
|
12
|
+
sessionId: string | null;
|
|
13
|
+
title: string;
|
|
14
|
+
req: ExportRequest;
|
|
15
|
+
/** Open at the turn that began with this message, to the end. */
|
|
16
|
+
fromItemId: string | null;
|
|
17
|
+
/** The session's turns, as the last estimate listed them. */
|
|
18
|
+
turns: ExportTurnInfo[];
|
|
19
|
+
selecting: boolean;
|
|
20
|
+
pick: { from: number | null; to: number | null };
|
|
21
|
+
}>({
|
|
22
|
+
open: false,
|
|
23
|
+
sessionId: null,
|
|
24
|
+
title: "",
|
|
25
|
+
req: { ...DEFAULT_REQUEST },
|
|
26
|
+
fromItemId: null,
|
|
27
|
+
turns: [],
|
|
28
|
+
selecting: false,
|
|
29
|
+
pick: { from: null, to: null },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const exportUiActions = {
|
|
33
|
+
open(sessionId: string, title: string, fromItemId: string | null = null) {
|
|
34
|
+
if (exportUi.sessionId !== sessionId) {
|
|
35
|
+
exportUi.turns = [];
|
|
36
|
+
exportUi.req = { ...exportUi.req, range: undefined };
|
|
37
|
+
}
|
|
38
|
+
exportUi.sessionId = sessionId;
|
|
39
|
+
exportUi.title = title;
|
|
40
|
+
exportUi.fromItemId = fromItemId;
|
|
41
|
+
exportUi.open = true;
|
|
42
|
+
},
|
|
43
|
+
close() {
|
|
44
|
+
exportUi.open = false;
|
|
45
|
+
exportUi.selecting = false;
|
|
46
|
+
exportUi.fromItemId = null;
|
|
47
|
+
},
|
|
48
|
+
setRequest(patch: Partial<ExportRequest>) {
|
|
49
|
+
exportUi.req = { ...exportUi.req, ...patch };
|
|
50
|
+
},
|
|
51
|
+
/** Close the sheet and let the chat's user messages be ticked. */
|
|
52
|
+
startSelect(from: number | null = null) {
|
|
53
|
+
exportUi.open = false;
|
|
54
|
+
exportUi.selecting = true;
|
|
55
|
+
exportUi.pick = { from, to: null };
|
|
56
|
+
},
|
|
57
|
+
/** A tap on a turn: the first sets the start, the second the end. */
|
|
58
|
+
tap(n: number) {
|
|
59
|
+
const { from, to } = exportUi.pick;
|
|
60
|
+
exportUi.pick =
|
|
61
|
+
from === null || to !== null ? { from: n, to: null } : { from, to: n };
|
|
62
|
+
},
|
|
63
|
+
/** Use the ticked turns as the range and go back to the sheet. */
|
|
64
|
+
finishSelect() {
|
|
65
|
+
const { from, to } = exportUi.pick;
|
|
66
|
+
if (from !== null) {
|
|
67
|
+
const end = to ?? from;
|
|
68
|
+
exportUi.req = {
|
|
69
|
+
...exportUi.req,
|
|
70
|
+
range: { from: Math.min(from, end), to: Math.max(from, end) },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
exportUi.selecting = false;
|
|
74
|
+
exportUi.open = true;
|
|
75
|
+
},
|
|
76
|
+
cancelSelect() {
|
|
77
|
+
exportUi.selecting = false;
|
|
78
|
+
exportUi.open = true;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The ticked turns, ordered: null until something is ticked. */
|
|
83
|
+
export function pickedRange(pick: { from: number | null; to: number | null }) {
|
|
84
|
+
if (pick.from === null) return null;
|
|
85
|
+
const end = pick.to ?? pick.from;
|
|
86
|
+
return { from: Math.min(pick.from, end), to: Math.max(pick.from, end) };
|
|
87
|
+
}
|