@saadnvd1/agent-os 0.3.660 → 0.3.664
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/sessions/[id]/route.ts +3 -3
- package/app/api/sessions/[id]/summarize/route.ts +3 -2
- package/app/api/worktree-sweep/route.test.ts +19 -1
- package/app/api/worktree-sweep/route.ts +25 -6
- package/components/Health/SweepRuns.tsx +76 -1
- package/data/worktree-sweep/index.ts +15 -0
- package/lib/agentos-home.ts +11 -0
- package/lib/agents/launch.ts +6 -9
- package/lib/agents/server-env.ts +24 -0
- package/lib/artifacts/store.ts +2 -5
- package/lib/chat/drivers/claude.ts +3 -2
- package/lib/chat/drivers/codex.ts +3 -2
- package/lib/chat/drivers/copilot.ts +2 -1
- package/lib/chat/drivers/opencode.ts +3 -2
- package/lib/chat/drivers/pi-approvals.ts +2 -2
- package/lib/chat/drivers/pi.ts +3 -2
- package/lib/chat/worker/client.ts +18 -13
- package/lib/chat/worker/protocol.ts +2 -2
- package/lib/connect/config.ts +2 -3
- package/lib/db/backup.ts +2 -2
- package/lib/db/migrations/20261018T1600_health_boot_deploys.ts +13 -0
- package/lib/dev-instance/processInfo.ts +48 -27
- package/lib/dev-instance/startDevServer.ts +1 -1
- package/lib/dev-instance/stopDevServer.ts +20 -17
- package/lib/dev-servers.ts +2 -1
- package/lib/done/index.ts +2 -2
- package/lib/health/boots.ts +37 -0
- package/lib/health/ci.ts +23 -2
- package/lib/health/detectors.ts +2 -1
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +3 -3
- package/lib/health/fixtures.ts +1 -0
- package/lib/health/tick.ts +6 -1
- package/lib/health/types.ts +2 -0
- package/lib/hosts/attach.ts +8 -1
- package/lib/orchestration.ts +3 -1
- package/lib/orchestrator/home.ts +2 -5
- package/lib/orchestrator/shell-guard.ts +2 -0
- package/lib/orchestrator/write-guard.ts +3 -2
- package/lib/plans/features.ts +2 -2
- package/lib/pr-generation.ts +2 -1
- package/lib/program-status/claude-hooks.ts +2 -2
- package/lib/program-status/tap.ts +2 -2
- package/lib/project-config/database.ts +71 -16
- package/lib/project-config/session.ts +18 -0
- package/lib/security/tailnet-https.ts +2 -2
- package/lib/session-artifacts/collect.ts +18 -1
- package/lib/session-artifacts/keep.ts +2 -4
- package/lib/session-audit/store.ts +2 -5
- package/lib/session-logs/folder.ts +2 -3
- package/lib/sessions/launch.ts +2 -2
- package/lib/sessions/leftovers.ts +61 -0
- package/lib/simulator/agent-device.ts +2 -2
- package/lib/tasks/finish.ts +2 -2
- package/lib/tasks/import.ts +3 -5
- package/lib/tasks/move.ts +2 -2
- package/lib/tasks/moved-away.ts +2 -0
- package/lib/tasks/state.ts +7 -3
- package/lib/tasks/ui-brief.ts +2 -2
- package/lib/tmux/exec.ts +3 -1
- package/lib/worktree-deps/fastcopy.ts +2 -2
- package/lib/worktree-deps/spare.ts +2 -2
- package/lib/worktree-sweep/caches.ts +123 -0
- package/lib/worktree-sweep/databases.ts +222 -0
- package/lib/worktree-sweep/du.ts +13 -0
- package/lib/worktree-sweep/extras.ts +188 -0
- package/lib/worktree-sweep/findOrphans.ts +19 -8
- package/lib/worktree-sweep/report.ts +85 -0
- package/lib/worktree-sweep/runSweep.ts +46 -13
- package/lib/worktree-sweep/settings.ts +24 -0
- package/lib/worktree-sweep/state.ts +5 -0
- package/lib/worktree-sweep/store.ts +11 -0
- package/lib/worktree-sweep/types.ts +44 -1
- package/lib/worktrees.ts +3 -0
- package/package.json +2 -1
- package/scripts/disk-dry-run.ts +57 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
mainCheckoutOf,
|
|
8
8
|
} from "@/lib/worktrees";
|
|
9
9
|
import { releasePort } from "@/lib/ports";
|
|
10
|
-
import {
|
|
10
|
+
import { releaseSessionLeftovers } from "@/lib/sessions/leftovers";
|
|
11
11
|
import { killWorker } from "@/lib/orchestration";
|
|
12
12
|
import { endSessionAgent } from "@/lib/sessions/end";
|
|
13
13
|
import { endTmuxSession } from "@/lib/tmux/end";
|
|
@@ -261,7 +261,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
261
261
|
} catch (error) {
|
|
262
262
|
console.error(`Failed to kill worker ${worker.id}:`, error);
|
|
263
263
|
}
|
|
264
|
-
void
|
|
264
|
+
void releaseSessionLeftovers(worker);
|
|
265
265
|
queries.deleteSession(db).run(worker.id);
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -271,7 +271,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
// Its private Postgres copy: the row is read now, the drop runs on.
|
|
274
|
-
void
|
|
274
|
+
void releaseSessionLeftovers(existing);
|
|
275
275
|
|
|
276
276
|
// Delete from database immediately for instant UI feedback
|
|
277
277
|
// The agent's tmux and processes go on in the background; the worktree
|
|
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
|
|
|
8
8
|
import { readFile } from "fs/promises";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { pasteText, tmux } from "@/lib/tmux/exec";
|
|
11
|
+
import { sessionBaseEnv, UNSET_SERVER_ENV } from "@/lib/agents/server-env";
|
|
11
12
|
|
|
12
13
|
// Get Claude session ID from tmux environment
|
|
13
14
|
async function getClaudeSessionId(tmuxSession: string): Promise<string | null> {
|
|
@@ -135,7 +136,7 @@ async function generateSummary(conversation: string): Promise<string> {
|
|
|
135
136
|
const routed = await routedRun("helper");
|
|
136
137
|
return new Promise((resolve, reject) => {
|
|
137
138
|
const claude = spawn("claude", ["-p", prompt, "--model", routed.model], {
|
|
138
|
-
env: { ...
|
|
139
|
+
env: { ...sessionBaseEnv(), ...routed.env },
|
|
139
140
|
stdio: ["pipe", "pipe", "pipe"],
|
|
140
141
|
});
|
|
141
142
|
|
|
@@ -309,7 +310,7 @@ export async function POST(
|
|
|
309
310
|
|
|
310
311
|
// Start new tmux session with Claude directly
|
|
311
312
|
const isRoot = process.getuid?.() === 0;
|
|
312
|
-
const envPrefix = isRoot ? "IS_SANDBOX=1 " : "";
|
|
313
|
+
const envPrefix = UNSET_SERVER_ENV + (isRoot ? "IS_SANDBOX=1 " : "");
|
|
313
314
|
const claudeCmd = effectiveAutoApprove(session.auto_approve)
|
|
314
315
|
? `${envPrefix}claude --dangerously-skip-permissions`
|
|
315
316
|
: "claude";
|
|
@@ -5,10 +5,11 @@ import { NextRequest } from "next/server";
|
|
|
5
5
|
const { GET, PUT } = await import("./route");
|
|
6
6
|
const { createProject } = await import("@/lib/projects");
|
|
7
7
|
|
|
8
|
-
const put = (body: unknown) =>
|
|
8
|
+
const put = (body: unknown, trust: string | null = "loopback") =>
|
|
9
9
|
PUT(
|
|
10
10
|
new NextRequest("http://localhost/api/worktree-sweep", {
|
|
11
11
|
method: "PUT",
|
|
12
|
+
headers: trust ? { "x-agentos-trust": trust } : {},
|
|
12
13
|
body: JSON.stringify(body),
|
|
13
14
|
})
|
|
14
15
|
);
|
|
@@ -30,6 +31,23 @@ describe("/api/worktree-sweep", () => {
|
|
|
30
31
|
expect((await (await put({ days: null })).json()).days).toBeNull();
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
it("turns the full clean-up on and off, off by default", async () => {
|
|
35
|
+
expect((await (await GET()).json()).armed).toBe(false);
|
|
36
|
+
expect((await (await put({ armed: true })).json()).armed).toBe(true);
|
|
37
|
+
expect((await (await GET()).json()).armed).toBe(true);
|
|
38
|
+
expect((await (await put({ armed: false })).json()).armed).toBe(false);
|
|
39
|
+
expect((await put({ armed: "yes" })).status).toBe(400);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("turns the full clean-up on only from this machine or the tailnet, off from anywhere", async () => {
|
|
43
|
+
expect((await put({ armed: true }, null)).status).toBe(403);
|
|
44
|
+
expect((await put({ armed: true }, "device")).status).toBe(403);
|
|
45
|
+
expect((await (await GET()).json()).armed).toBe(false);
|
|
46
|
+
expect((await put({ armed: true }, "tailnet")).status).toBe(200);
|
|
47
|
+
expect((await put({ armed: false }, null)).status).toBe(200);
|
|
48
|
+
expect((await (await GET()).json()).armed).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
33
51
|
it("refuses a number that isn't a whole 0-365", async () => {
|
|
34
52
|
for (const days of [-1, 1.5, 400, "7", undefined]) {
|
|
35
53
|
const res = await put({ days });
|
|
@@ -2,16 +2,21 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
setGlobalSweepDays,
|
|
5
|
+
setReaperArmed,
|
|
5
6
|
setProjectSweepDays,
|
|
6
7
|
sweepDays,
|
|
7
8
|
} from "@/lib/worktree-sweep/settings";
|
|
9
|
+
import { requireLocalTrust } from "@/lib/security/route-guard";
|
|
8
10
|
import { sweepState } from "@/lib/worktree-sweep/state";
|
|
9
11
|
|
|
10
|
-
const body = z.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const body = z.union([
|
|
13
|
+
z.object({
|
|
14
|
+
// Null clears the setting: the global one, then the default, applies.
|
|
15
|
+
days: sweepDays.nullable(),
|
|
16
|
+
projectId: z.string().min(1).optional(),
|
|
17
|
+
}),
|
|
18
|
+
z.object({ armed: z.boolean() }),
|
|
19
|
+
]);
|
|
15
20
|
|
|
16
21
|
export async function GET() {
|
|
17
22
|
return NextResponse.json(sweepState());
|
|
@@ -21,9 +26,23 @@ export async function PUT(request: NextRequest) {
|
|
|
21
26
|
const got = body.safeParse(await request.json().catch(() => null));
|
|
22
27
|
if (!got.success)
|
|
23
28
|
return NextResponse.json(
|
|
24
|
-
{
|
|
29
|
+
{
|
|
30
|
+
error:
|
|
31
|
+
"days must be a whole number from 0 to 365, or null; or armed true or false",
|
|
32
|
+
},
|
|
25
33
|
{ status: 400 }
|
|
26
34
|
);
|
|
35
|
+
if ("armed" in got.data) {
|
|
36
|
+
// Turning the removal on is a person's call, made on this machine or
|
|
37
|
+
// over Tailscale: a stolen device token must not be able to. Turning it
|
|
38
|
+
// off stays on the ordinary gate.
|
|
39
|
+
if (got.data.armed) {
|
|
40
|
+
const refused = requireLocalTrust(request);
|
|
41
|
+
if (refused) return refused;
|
|
42
|
+
}
|
|
43
|
+
setReaperArmed(got.data.armed);
|
|
44
|
+
return NextResponse.json(sweepState());
|
|
45
|
+
}
|
|
27
46
|
const { days, projectId } = got.data;
|
|
28
47
|
if (projectId) setProjectSweepDays(projectId, days);
|
|
29
48
|
else setGlobalSweepDays(days);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useRunSweep,
|
|
6
|
+
useSetReaperArmed,
|
|
7
|
+
useWorktreeSweep,
|
|
8
|
+
} from "@/data/worktree-sweep";
|
|
5
9
|
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
6
10
|
import { formatBytes } from "@/lib/share/view";
|
|
7
11
|
import type { SweepRun } from "@/lib/worktree-sweep/types";
|
|
@@ -10,8 +14,34 @@ const KIND: Record<string, string> = {
|
|
|
10
14
|
settled: "settled session",
|
|
11
15
|
orphan: "orphan folder",
|
|
12
16
|
"test-leak": "test leftover",
|
|
17
|
+
database: "task database",
|
|
18
|
+
spare: "dependency spare",
|
|
19
|
+
temp: "temp folder",
|
|
20
|
+
"derived-data": "Xcode build folder",
|
|
13
21
|
};
|
|
14
22
|
|
|
23
|
+
// "18.2 GB in 3,876 worktrees · 12 task databases (270 MB) · ..." from the
|
|
24
|
+
// latest run that measured, and what the last real run freed.
|
|
25
|
+
export function diskSummary(runs: SweepRun[]): string | null {
|
|
26
|
+
const totals = runs.find((r) => r.details?.totals)?.details?.totals;
|
|
27
|
+
if (!totals) return null;
|
|
28
|
+
const part = (n: number, bytes: number, what: string) =>
|
|
29
|
+
`${n.toLocaleString()} ${what}${bytes ? ` (${formatBytes(bytes)})` : ""}`;
|
|
30
|
+
const last = runs.find((r) => !r.dryRun && r.finishedAt);
|
|
31
|
+
return [
|
|
32
|
+
`Task leftovers: ${part(totals.worktrees.count, totals.worktrees.bytes, "worktrees")}`,
|
|
33
|
+
part(
|
|
34
|
+
totals.databases.count,
|
|
35
|
+
totals.databases.bytes,
|
|
36
|
+
"stale task databases"
|
|
37
|
+
),
|
|
38
|
+
part(totals.spares.count, totals.spares.bytes, "dependency spares"),
|
|
39
|
+
last
|
|
40
|
+
? `last clean-up freed ${formatBytes(last.freedBytes)}, ${compactTimeAgo(fromSqliteTime(last.finishedAt!))}`
|
|
41
|
+
: "no clean-up has run yet",
|
|
42
|
+
].join(" · ");
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
const name = (p: string) => p.split("/").pop();
|
|
16
46
|
|
|
17
47
|
function Run({ run }: { run: SweepRun }) {
|
|
@@ -51,6 +81,22 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
51
81
|
</ul>
|
|
52
82
|
</div>
|
|
53
83
|
)}
|
|
84
|
+
{(run.details?.pending?.length ?? 0) > 0 && (
|
|
85
|
+
<div className="mt-2 space-y-1 pb-2">
|
|
86
|
+
<p className="text-sm font-medium">
|
|
87
|
+
Waiting for the full clean-up to be turned on (
|
|
88
|
+
{run.details?.pendingTotal?.count ?? 0},{" "}
|
|
89
|
+
{formatBytes(run.details?.pendingTotal?.bytes ?? 0)})
|
|
90
|
+
</p>
|
|
91
|
+
<ul className="text-muted-foreground space-y-1 text-xs">
|
|
92
|
+
{run.details?.pending?.slice(0, 100).map((p) => (
|
|
93
|
+
<li key={p.path} className="break-all">
|
|
94
|
+
{name(p.path)} ({KIND[p.kind]}, {formatBytes(p.bytes)})
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
54
100
|
{removed.length > 0 && (
|
|
55
101
|
<div className="mt-2 space-y-1 pb-2">
|
|
56
102
|
<p className="text-sm font-medium">Removed</p>
|
|
@@ -72,8 +118,37 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
72
118
|
export function SweepRuns() {
|
|
73
119
|
const { data, isPending, error } = useWorktreeSweep();
|
|
74
120
|
const run = useRunSweep();
|
|
121
|
+
const arm = useSetReaperArmed();
|
|
122
|
+
const summary = data ? diskSummary(data.runs) : null;
|
|
75
123
|
return (
|
|
76
124
|
<div className="space-y-3">
|
|
125
|
+
{summary && (
|
|
126
|
+
<p
|
|
127
|
+
className="text-sm font-medium tabular-nums"
|
|
128
|
+
data-testid="disk-summary"
|
|
129
|
+
>
|
|
130
|
+
{summary}
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
{data && (
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
135
|
+
<p className="text-muted-foreground min-w-0 flex-1 text-sm">
|
|
136
|
+
Full clean-up is {data.armed ? "on" : "off"}: test leftovers, stale
|
|
137
|
+
task databases, spares, temp folders and build caches{" "}
|
|
138
|
+
{data.armed
|
|
139
|
+
? "are removed with each run."
|
|
140
|
+
: "are only listed until you turn it on. Read a dry run first."}
|
|
141
|
+
</p>
|
|
142
|
+
<Button
|
|
143
|
+
variant="outline"
|
|
144
|
+
className="min-h-11 md:min-h-8"
|
|
145
|
+
disabled={arm.isPending}
|
|
146
|
+
onClick={() => arm.mutate(!data.armed)}
|
|
147
|
+
>
|
|
148
|
+
{data.armed ? "Turn off" : "Turn on"}
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
77
152
|
<div className="flex flex-wrap items-center gap-2">
|
|
78
153
|
<p className="text-muted-foreground min-w-0 flex-1 text-sm">
|
|
79
154
|
Settled worktrees older than {data?.days ?? data?.defaultDays ?? 7}{" "}
|
|
@@ -33,6 +33,21 @@ export function useSetSweepDays() {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function useSetReaperArmed() {
|
|
37
|
+
const queryClient = useQueryClient();
|
|
38
|
+
return useMutation({
|
|
39
|
+
mutationFn: async (armed: boolean) =>
|
|
40
|
+
json<SweepState>(
|
|
41
|
+
await fetch("/api/worktree-sweep", {
|
|
42
|
+
method: "PUT",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({ armed }),
|
|
45
|
+
})
|
|
46
|
+
),
|
|
47
|
+
onSuccess: (data) => queryClient.setQueryData(sweepKeys.all, data),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export function useRunSweep() {
|
|
37
52
|
const queryClient = useQueryClient();
|
|
38
53
|
return useMutation({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { refuseRealStateUnderTest } from "./test-home-guard";
|
|
4
|
+
|
|
5
|
+
// Every path under ~/.agent-os is built here, so a test run that resolves one
|
|
6
|
+
// to the real home throws instead of leaving files in the owner's state.
|
|
7
|
+
export function agentosHome(...parts: string[]): string {
|
|
8
|
+
return refuseRealStateUnderTest(
|
|
9
|
+
path.join(os.homedir(), ".agent-os", ...parts)
|
|
10
|
+
);
|
|
11
|
+
}
|
package/lib/agents/launch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { agentosHome } from "../agentos-home";
|
|
1
2
|
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { execFile } from "child_process";
|
|
5
5
|
import { promisify } from "util";
|
|
@@ -10,6 +10,7 @@ import { runInBackground } from "../async-operations";
|
|
|
10
10
|
import { trustPromptKeys } from "../tasks/state";
|
|
11
11
|
import { BUS_BRIEF } from "./brief";
|
|
12
12
|
import { selfUrl } from "./self-env";
|
|
13
|
+
import { sessionBaseEnv, UNSET_SERVER_ENV } from "./server-env";
|
|
13
14
|
import { sessionPath } from "./tool-path";
|
|
14
15
|
import { autoCompactEnv } from "../chat/refresh";
|
|
15
16
|
import { subagentEnv } from "../models/subagents";
|
|
@@ -21,12 +22,8 @@ import {
|
|
|
21
22
|
} from "../project-config/session";
|
|
22
23
|
|
|
23
24
|
const execFileAsync = promisify(execFile);
|
|
24
|
-
const PROMPTS_DIR =
|
|
25
|
-
export const BUS_BRIEF_FILE =
|
|
26
|
-
os.homedir(),
|
|
27
|
-
".agent-os",
|
|
28
|
-
"bus-brief.md"
|
|
29
|
-
);
|
|
25
|
+
const PROMPTS_DIR = agentosHome("prompts");
|
|
26
|
+
export const BUS_BRIEF_FILE = agentosHome("bus-brief.md");
|
|
30
27
|
|
|
31
28
|
export function ensureBusBrief(): void {
|
|
32
29
|
fs.mkdirSync(path.dirname(BUS_BRIEF_FILE), { recursive: true });
|
|
@@ -110,7 +107,7 @@ export async function launchClaude(opts: {
|
|
|
110
107
|
model_provider: opts.provider ?? null,
|
|
111
108
|
});
|
|
112
109
|
const mcpFlags = routedMcpFlags(opts.provider);
|
|
113
|
-
const agent =
|
|
110
|
+
const agent = `${UNSET_SERVER_ENV}export PATH=${shellQuote(AOS_BIN_DIR)}:"$HOME/.local/bin:$PATH"; ${routing.exportKey} ${provider.command} ${flags} ${mcpFlags}${CLAUDE_STATUS_SETTINGS_FLAG} --append-system-prompt-file ${shellQuote(briefFile)} "$(cat ${shellQuote(promptFile)})"; exec "\${SHELL:-/bin/sh}" -l`;
|
|
114
111
|
await execFileAsync(
|
|
115
112
|
"tmux",
|
|
116
113
|
[
|
|
@@ -123,7 +120,7 @@ export async function launchClaude(opts: {
|
|
|
123
120
|
...envArgs({ ...agentEnv(opts.sessionId), ...routing.env }),
|
|
124
121
|
agent,
|
|
125
122
|
],
|
|
126
|
-
{ cwd: opts.cwd }
|
|
123
|
+
{ cwd: opts.cwd, env: sessionBaseEnv() }
|
|
127
124
|
);
|
|
128
125
|
runInBackground(
|
|
129
126
|
() => acceptTrustPrompt(opts.tmuxName),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Variables the server sets only for itself (`npm start`, `npm run dev`,
|
|
2
|
+
// the dev-instance spawner). A session that inherits them breaks the
|
|
3
|
+
// project it works in: NODE_ENV=production makes Vitest load React's
|
|
4
|
+
// production build, PORT makes a dev server bind the live one's port.
|
|
5
|
+
// AGENTOS_URL, AGENTOS_PORT and the session ids are not here: sessions need them.
|
|
6
|
+
export const SERVER_ONLY_ENV = [
|
|
7
|
+
"NODE_ENV",
|
|
8
|
+
"PORT",
|
|
9
|
+
"AGENTOS_SERVER",
|
|
10
|
+
"AGENTOS_DEV",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
// Shell text that clears them inside a pane. `tmux new-session -e` can only
|
|
14
|
+
// add variables, and an already-running tmux server may hold them globally.
|
|
15
|
+
export const UNSET_SERVER_ENV = `unset ${SERVER_ONLY_ENV.join(" ")}; `;
|
|
16
|
+
|
|
17
|
+
// The server's environment as an agent or its tools should see it.
|
|
18
|
+
export function sessionBaseEnv(
|
|
19
|
+
env: NodeJS.ProcessEnv = process.env
|
|
20
|
+
): NodeJS.ProcessEnv {
|
|
21
|
+
const out = { ...env };
|
|
22
|
+
for (const k of SERVER_ONLY_ENV) delete out[k];
|
|
23
|
+
return out;
|
|
24
|
+
}
|
package/lib/artifacts/store.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* ~/.agent-os/artifacts/<session id>/, and a row saying whose it is.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { agentosHome } from "../agentos-home";
|
|
6
7
|
import fs from "fs";
|
|
7
|
-
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
10
|
import { db } from "../db";
|
|
@@ -23,10 +23,7 @@ export const MAX_TITLE_CHARS = 120;
|
|
|
23
23
|
const ID = /^[0-9a-f-]{36}$/;
|
|
24
24
|
|
|
25
25
|
export function artifactsDir(): string {
|
|
26
|
-
return (
|
|
27
|
-
process.env.AGENTOS_ARTIFACTS_DIR ||
|
|
28
|
-
path.join(os.homedir(), ".agent-os", "artifacts")
|
|
29
|
-
);
|
|
26
|
+
return process.env.AGENTOS_ARTIFACTS_DIR || agentosHome("artifacts");
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
// Session ids are UUIDs; anything else must not become a path segment.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import { randomUUID } from "crypto";
|
|
2
3
|
import {
|
|
3
4
|
getSessionMessages,
|
|
@@ -145,7 +146,7 @@ export const claudeDriver: ChatDriver = {
|
|
|
145
146
|
cwd,
|
|
146
147
|
permissionMode: "bypassPermissions",
|
|
147
148
|
allowDangerouslySkipPermissions: true,
|
|
148
|
-
env: { ...
|
|
149
|
+
env: { ...sessionBaseEnv(), ...env },
|
|
149
150
|
},
|
|
150
151
|
});
|
|
151
152
|
try {
|
|
@@ -235,7 +236,7 @@ export const claudeDriver: ChatDriver = {
|
|
|
235
236
|
append: options.systemAppend,
|
|
236
237
|
},
|
|
237
238
|
env: {
|
|
238
|
-
...
|
|
239
|
+
...sessionBaseEnv(),
|
|
239
240
|
...options.env,
|
|
240
241
|
// When the agent is done with every turn it had queued: the end
|
|
241
242
|
// of a turn that never sent its result.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -16,7 +17,7 @@ export const codexDriver: ChatDriver = {
|
|
|
16
17
|
|
|
17
18
|
async discover({ cwd, env }) {
|
|
18
19
|
const rpc = new CodexRpc(
|
|
19
|
-
{ cwd, env: { ...
|
|
20
|
+
{ cwd, env: { ...sessionBaseEnv(), ...env } },
|
|
20
21
|
{ notify: () => {}, request: () => {}, exit: () => {} }
|
|
21
22
|
);
|
|
22
23
|
try {
|
|
@@ -51,7 +52,7 @@ export const codexDriver: ChatDriver = {
|
|
|
51
52
|
let starting = false;
|
|
52
53
|
let stopOnStart = false;
|
|
53
54
|
const rpc: CodexRpc = new CodexRpc(
|
|
54
|
-
{ cwd: options.cwd, env: { ...
|
|
55
|
+
{ cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
|
|
55
56
|
{
|
|
56
57
|
notify(method, params) {
|
|
57
58
|
if (method === "item/started") approvals.sawItem(params.item as P);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -52,7 +53,7 @@ export const copilotDriver: ChatDriver = {
|
|
|
52
53
|
const held: { text: string; images?: ChatImage[] }[] = [];
|
|
53
54
|
const rpc: CopilotRpc = new CopilotRpc(
|
|
54
55
|
copilotArgs(options),
|
|
55
|
-
{ cwd: options.cwd, env: { ...
|
|
56
|
+
{ cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
|
|
56
57
|
{
|
|
57
58
|
notify(method, params) {
|
|
58
59
|
if (method === "session/update")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -46,7 +47,7 @@ export const openCodeDriver: ChatDriver = {
|
|
|
46
47
|
atRest: false,
|
|
47
48
|
|
|
48
49
|
async discover({ cwd, env }) {
|
|
49
|
-
const server = new OpenCodeServer(cwd, { ...
|
|
50
|
+
const server = new OpenCodeServer(cwd, { ...sessionBaseEnv(), ...env });
|
|
50
51
|
try {
|
|
51
52
|
const [providers, commands] = await Promise.all([
|
|
52
53
|
server.call<{ all: P[]; connected: string[] }>("GET", "/provider"),
|
|
@@ -79,7 +80,7 @@ export const openCodeDriver: ChatDriver = {
|
|
|
79
80
|
const out = new InputQueue<DriverEvent>();
|
|
80
81
|
const emit = (e: DriverEvent) => out.push(e);
|
|
81
82
|
const server = new OpenCodeServer(options.cwd, {
|
|
82
|
-
...
|
|
83
|
+
...sessionBaseEnv(),
|
|
83
84
|
...options.env,
|
|
84
85
|
});
|
|
85
86
|
let sessionId: string | null = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { agentosHome } from "../../agentos-home";
|
|
1
2
|
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import type { ChatAccess, DriverEvent } from "../events";
|
|
@@ -9,7 +9,7 @@ import { piTool } from "./pi-mapper";
|
|
|
9
9
|
type P = Record<string, unknown>;
|
|
10
10
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
11
11
|
|
|
12
|
-
const DIR =
|
|
12
|
+
const DIR = agentosHome("pi");
|
|
13
13
|
const MARK = "agentos:";
|
|
14
14
|
|
|
15
15
|
// Pi runs every tool without asking. This extension asks the chat first,
|
package/lib/chat/drivers/pi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
|
|
2
3
|
import type { ChatImage, DriverEvent } from "../events";
|
|
3
4
|
import { InputQueue } from "../queue";
|
|
@@ -45,7 +46,7 @@ export const piDriver: ChatDriver = {
|
|
|
45
46
|
async discover({ cwd, env }) {
|
|
46
47
|
const rpc = new PiRpc(
|
|
47
48
|
["--no-session", "--offline"],
|
|
48
|
-
{ cwd, env: { ...
|
|
49
|
+
{ cwd, env: { ...sessionBaseEnv(), ...env } },
|
|
49
50
|
{ event: () => {}, ui: () => {}, exit: () => {} }
|
|
50
51
|
);
|
|
51
52
|
try {
|
|
@@ -82,7 +83,7 @@ export const piDriver: ChatDriver = {
|
|
|
82
83
|
{
|
|
83
84
|
cwd: options.cwd,
|
|
84
85
|
env: {
|
|
85
|
-
...
|
|
86
|
+
...sessionBaseEnv(),
|
|
86
87
|
...options.env,
|
|
87
88
|
AGENTOS_PI_ACCESS_FILE: access.file,
|
|
88
89
|
PI_OFFLINE: "1",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UNSET_SERVER_ENV, sessionBaseEnv } from "../../agents/server-env";
|
|
1
2
|
/**
|
|
2
3
|
* The server's side of a chat worker: starts one in tmux when none is
|
|
3
4
|
* running, connects to its socket, and passes commands and events along.
|
|
@@ -224,7 +225,7 @@ async function spawnWorker(sessionId: string): Promise<void> {
|
|
|
224
225
|
if (await tmuxHas(name)) return;
|
|
225
226
|
fs.mkdirSync(workerDir(), { recursive: true, mode: 0o700 });
|
|
226
227
|
const env: Record<string, string | undefined> = {
|
|
227
|
-
...
|
|
228
|
+
...sessionBaseEnv(),
|
|
228
229
|
DB_PATH: path.resolve(
|
|
229
230
|
process.env.DB_PATH || path.join(process.cwd(), "agent-os.db")
|
|
230
231
|
),
|
|
@@ -233,18 +234,22 @@ async function spawnWorker(sessionId: string): Promise<void> {
|
|
|
233
234
|
delete env.TMUX_PANE;
|
|
234
235
|
fs.writeFileSync(envPath(sessionId), JSON.stringify(env), { mode: 0o600 });
|
|
235
236
|
const entry = path.join(process.cwd(), "lib/chat/worker/main.ts");
|
|
236
|
-
await execFileAsync(
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
await execFileAsync(
|
|
238
|
+
"tmux",
|
|
239
|
+
[
|
|
240
|
+
"new-session",
|
|
241
|
+
"-d",
|
|
242
|
+
"-s",
|
|
243
|
+
name,
|
|
244
|
+
"-c",
|
|
245
|
+
process.cwd(),
|
|
246
|
+
// The worker finds its env file by DB_PATH, so it can't come from there.
|
|
247
|
+
"-e",
|
|
248
|
+
`DB_PATH=${env.DB_PATH}`,
|
|
249
|
+
`${UNSET_SERVER_ENV}exec ${shellQuote(process.execPath)} --import tsx ${shellQuote(entry)} ${shellQuote(sessionId)} > ${shellQuote(path.join(workerDir(), `${sessionId}.log`))} 2>&1`,
|
|
250
|
+
],
|
|
251
|
+
{ env: sessionBaseEnv() }
|
|
252
|
+
);
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* worker's Unix socket: one JSON message per line.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { agentosHome } from "../../agentos-home";
|
|
6
7
|
import crypto from "crypto";
|
|
7
|
-
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import type {
|
|
10
10
|
ApprovalDecision,
|
|
@@ -116,7 +116,7 @@ export function workerDir(): string {
|
|
|
116
116
|
process.env.DB_PATH || path.join(process.cwd(), "agent-os.db")
|
|
117
117
|
);
|
|
118
118
|
const key = crypto.createHash("sha1").update(db).digest("hex").slice(0, 8);
|
|
119
|
-
return
|
|
119
|
+
return agentosHome("chat", key);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export const socketPath = (sessionId: string) =>
|
package/lib/connect/config.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* each time the tunnel starts.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { agentosHome } from "../agentos-home";
|
|
14
15
|
import fs from "fs";
|
|
15
|
-
import os from "os";
|
|
16
16
|
import path from "path";
|
|
17
17
|
|
|
18
18
|
export interface ConnectConfig {
|
|
@@ -32,8 +32,7 @@ export interface ConnectFiles {
|
|
|
32
32
|
export const MACHINE_HOST = /^m-[0-9a-f]{16}\.runagentos\.com$/;
|
|
33
33
|
|
|
34
34
|
export const connectDir = () =>
|
|
35
|
-
process.env.AGENTOS_CONNECT_DIR ||
|
|
36
|
-
path.join(os.homedir(), ".agent-os", "connect");
|
|
35
|
+
process.env.AGENTOS_CONNECT_DIR || agentosHome("connect");
|
|
37
36
|
|
|
38
37
|
export type ConnectRead =
|
|
39
38
|
| { ok: true; files: ConnectFiles; enabled: boolean }
|
package/lib/db/backup.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { agentosHome } from "../agentos-home";
|
|
1
2
|
import type Database from "better-sqlite3";
|
|
2
3
|
import Sqlite from "better-sqlite3";
|
|
3
4
|
import { randomBytes } from "crypto";
|
|
4
5
|
import fs from "fs";
|
|
5
|
-
import os from "os";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { refuseRealStateUnderTest } from "../test-home-guard";
|
|
8
8
|
|
|
9
9
|
export const backupsDir = (): string =>
|
|
10
|
-
refuseRealStateUnderTest(
|
|
10
|
+
refuseRealStateUnderTest(agentosHome("backups"));
|
|
11
11
|
|
|
12
12
|
// A full copy of the database (VACUUM INTO) under ~/.agent-os/backups, taken
|
|
13
13
|
// before a destructive migration. Throws, and the migration must stop, unless
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261018T1600_health_boot_deploys",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
const cols = (
|
|
7
|
+
db.prepare(`PRAGMA table_info(health_boots)`).all() as { name: string }[]
|
|
8
|
+
).map((c) => c.name);
|
|
9
|
+
// 1 when autodeploy planned the restart that started this boot.
|
|
10
|
+
if (!cols.includes("deploy"))
|
|
11
|
+
db.exec(`ALTER TABLE health_boots ADD COLUMN deploy INTEGER`);
|
|
12
|
+
},
|
|
13
|
+
} satisfies Migration;
|