@saadnvd1/agent-os 0.3.575 → 0.3.577
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/fresh-start/route.ts +3 -1
- package/components/Workspaces/FreshStartForm.tsx +20 -6
- package/lib/agents/launch.ts +2 -1
- package/lib/away/run.ts +4 -0
- package/lib/chat/refresh.ts +51 -9
- package/lib/orchestrator/testing.ts +2 -2
- package/lib/sessions/end.ts +5 -2
- package/lib/simulator/index.ts +1 -0
- package/lib/simulator/simctl.ts +10 -0
- package/lib/simulator/task-devices.ts +160 -0
- package/package.json +1 -1
- package/server.ts +2 -1
|
@@ -3,7 +3,8 @@ import { refreshSettings, setRefreshSettings } from "@/lib/chat/refresh";
|
|
|
3
3
|
|
|
4
4
|
// When a chat's conversation starts over (lib/chat/refresh): context
|
|
5
5
|
// tokens for orchestrators and for tasks, and at which chats and tasks
|
|
6
|
-
// compact: between turns (lib/chat/stale) and mid-turn, by the agent itself
|
|
6
|
+
// compact: between turns (lib/chat/stale) and mid-turn, by the agent itself
|
|
7
|
+
// (one size for tasks, one for a person's own sessions);
|
|
7
8
|
// 0 for never.
|
|
8
9
|
export async function GET() {
|
|
9
10
|
return NextResponse.json(refreshSettings());
|
|
@@ -18,6 +19,7 @@ export async function PUT(request: NextRequest) {
|
|
|
18
19
|
// Its name before it also set the agents' own auto-compact.
|
|
19
20
|
compactTokens: body.compactTokens ?? body.staleCompactTokens,
|
|
20
21
|
autoCompactTokens: body.autoCompactTokens,
|
|
22
|
+
interactiveAutoCompactTokens: body.interactiveAutoCompactTokens,
|
|
21
23
|
});
|
|
22
24
|
return NextResponse.json(refreshSettings());
|
|
23
25
|
} catch (error) {
|
|
@@ -19,7 +19,7 @@ export function FreshStartForm() {
|
|
|
19
19
|
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
20
20
|
return (
|
|
21
21
|
<Form
|
|
22
|
-
key={`${data.orchestratorTokens}:${data.taskTokens}:${data.compactTokens}:${data.autoCompactTokens}`}
|
|
22
|
+
key={`${data.orchestratorTokens}:${data.taskTokens}:${data.compactTokens}:${data.autoCompactTokens}:${data.interactiveAutoCompactTokens}`}
|
|
23
23
|
saved={data}
|
|
24
24
|
/>
|
|
25
25
|
);
|
|
@@ -31,14 +31,20 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
31
31
|
const task = useThreshold(saved.taskTokens, 200);
|
|
32
32
|
const compact = useThreshold(saved.compactTokens, 150);
|
|
33
33
|
const midTurn = useThreshold(saved.autoCompactTokens, 300);
|
|
34
|
+
const interactive = useThreshold(saved.interactiveAutoCompactTokens, 200);
|
|
34
35
|
const error =
|
|
35
|
-
orchestrator.error ??
|
|
36
|
+
orchestrator.error ??
|
|
37
|
+
task.error ??
|
|
38
|
+
compact.error ??
|
|
39
|
+
midTurn.error ??
|
|
40
|
+
interactive.error;
|
|
36
41
|
const dirty =
|
|
37
42
|
!error &&
|
|
38
43
|
(orchestrator.tokens !== saved.orchestratorTokens ||
|
|
39
44
|
task.tokens !== saved.taskTokens ||
|
|
40
45
|
compact.tokens !== saved.compactTokens ||
|
|
41
|
-
midTurn.tokens !== saved.autoCompactTokens
|
|
46
|
+
midTurn.tokens !== saved.autoCompactTokens ||
|
|
47
|
+
interactive.tokens !== saved.interactiveAutoCompactTokens);
|
|
42
48
|
|
|
43
49
|
return (
|
|
44
50
|
<form
|
|
@@ -51,6 +57,7 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
51
57
|
taskTokens: task.tokens,
|
|
52
58
|
compactTokens: compact.tokens,
|
|
53
59
|
autoCompactTokens: midTurn.tokens,
|
|
60
|
+
interactiveAutoCompactTokens: interactive.tokens,
|
|
54
61
|
},
|
|
55
62
|
{
|
|
56
63
|
onSuccess: () => toast.success("Context settings saved"),
|
|
@@ -77,12 +84,19 @@ function Form({ saved }: { saved: RefreshSettings }) {
|
|
|
77
84
|
AgentOS compacts a chat over the smaller size, or idle past its prompt
|
|
78
85
|
cache, before the next message; the composer offers to first. Either way
|
|
79
86
|
AgentOS hands the agent its task, scope changes, files and todos back.
|
|
80
|
-
|
|
87
|
+
Your own sessions, in a chat or a terminal, have their own mid-turn
|
|
88
|
+
size; a terminal can't be compacted between turns, so that is the
|
|
89
|
+
only compaction it gets. Orchestrators start fresh instead.
|
|
81
90
|
</p>
|
|
82
91
|
<ThresholdRow
|
|
83
|
-
label="Mid-turn"
|
|
92
|
+
label="Mid-turn, tasks"
|
|
84
93
|
field={midTurn}
|
|
85
|
-
switchLabel="Let
|
|
94
|
+
switchLabel="Let a task's agent compact itself mid-turn"
|
|
95
|
+
/>
|
|
96
|
+
<ThresholdRow
|
|
97
|
+
label="Mid-turn, your sessions"
|
|
98
|
+
field={interactive}
|
|
99
|
+
switchLabel="Let your own sessions compact themselves mid-turn"
|
|
86
100
|
/>
|
|
87
101
|
<ThresholdRow
|
|
88
102
|
label="Between turns"
|
package/lib/agents/launch.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { BUS_BRIEF } from "./brief";
|
|
|
12
12
|
import { selfUrl } from "./self-env";
|
|
13
13
|
import { autoCompactEnv } from "../chat/refresh";
|
|
14
14
|
import { subagentEnv } from "../models/subagents";
|
|
15
|
+
import { COMPACT_BRIEF } from "../chat/working-state";
|
|
15
16
|
import { CLAUDE_STATUS_SETTINGS_FLAG } from "../program-status/claude-flag";
|
|
16
17
|
import {
|
|
17
18
|
sessionProjectEnv,
|
|
@@ -28,7 +29,7 @@ export const BUS_BRIEF_FILE = path.join(
|
|
|
28
29
|
|
|
29
30
|
export function ensureBusBrief(): void {
|
|
30
31
|
fs.mkdirSync(path.dirname(BUS_BRIEF_FILE), { recursive: true });
|
|
31
|
-
fs.writeFileSync(BUS_BRIEF_FILE, BUS_BRIEF);
|
|
32
|
+
fs.writeFileSync(BUS_BRIEF_FILE, `${BUS_BRIEF}\n\n${COMPACT_BRIEF}`);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// Who a session is and how it reaches AgentOS, set in its tmux environment so
|
package/lib/away/run.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import { checkDeploy } from "./deploy";
|
|
56
56
|
import { buildAwayReport, phoneSummary, reportMarkdown } from "./report";
|
|
57
57
|
import { awayOnLine } from "./brief";
|
|
58
|
+
import { shutFinishedTaskSimulators } from "../simulator";
|
|
58
59
|
|
|
59
60
|
const PHONE_SOURCE = "away";
|
|
60
61
|
const PHONE_LINES = 8;
|
|
@@ -582,6 +583,9 @@ export function awayTick(now = Date.now()): void {
|
|
|
582
583
|
console.error(`[away] sweep of ${workspaceId}:`, error);
|
|
583
584
|
}
|
|
584
585
|
}
|
|
586
|
+
await shutFinishedTaskSimulators().catch((error) =>
|
|
587
|
+
console.error("[away] simulator sweep:", error)
|
|
588
|
+
);
|
|
585
589
|
// Ended meanwhile: the report's summary says what's waiting.
|
|
586
590
|
const still = activeAway();
|
|
587
591
|
if (still?.id === away.id) notifyBatch(still, Date.now());
|
package/lib/chat/refresh.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { db } from "../db";
|
|
15
15
|
import { effectiveModels } from "../models/config";
|
|
16
|
+
import { sessionRole, type ModelRole } from "../models/roles";
|
|
16
17
|
import { MAX_REFRESH_TOKENS, MIN_REFRESH_TOKENS } from "./refresh-limits";
|
|
17
18
|
|
|
18
19
|
export const ORCHESTRATOR_KEY = "orchestrator.refresh_tokens";
|
|
@@ -25,6 +26,12 @@ export const DEFAULT_COMPACT_TOKENS = 150_000;
|
|
|
25
26
|
// mid-job costs the agent its files and its place.
|
|
26
27
|
export const AUTO_COMPACT_KEY = "chat.auto_compact_tokens";
|
|
27
28
|
export const DEFAULT_AUTO_COMPACT_TOKENS = 300_000;
|
|
29
|
+
// The same window for a person's own (interactive) sessions, which run long
|
|
30
|
+
// on the biggest model. 300k (DEFAULT_AUTO_COMPACT_TOKENS) was its value
|
|
31
|
+
// before it had its own setting: set it back to restore that.
|
|
32
|
+
export const INTERACTIVE_AUTO_COMPACT_KEY =
|
|
33
|
+
"chat.interactive_auto_compact_tokens";
|
|
34
|
+
export const DEFAULT_INTERACTIVE_AUTO_COMPACT_TOKENS = 300_000;
|
|
28
35
|
export { MIN_REFRESH_TOKENS, MAX_REFRESH_TOKENS } from "./refresh-limits";
|
|
29
36
|
|
|
30
37
|
export interface RefreshSettings {
|
|
@@ -37,6 +44,8 @@ export interface RefreshSettings {
|
|
|
37
44
|
// Mid-turn: Claude Code's own auto-compact window for a chat's or task's
|
|
38
45
|
// agent. 0 is off.
|
|
39
46
|
autoCompactTokens: number;
|
|
47
|
+
// The same, for interactive sessions. 0 is off.
|
|
48
|
+
interactiveAutoCompactTokens: number;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function read(key: string, fallback: number): number {
|
|
@@ -53,6 +62,10 @@ export function refreshSettings(): RefreshSettings {
|
|
|
53
62
|
taskTokens: read(TASK_KEY, 0),
|
|
54
63
|
compactTokens: read(COMPACT_KEY, DEFAULT_COMPACT_TOKENS),
|
|
55
64
|
autoCompactTokens: read(AUTO_COMPACT_KEY, DEFAULT_AUTO_COMPACT_TOKENS),
|
|
65
|
+
interactiveAutoCompactTokens: read(
|
|
66
|
+
INTERACTIVE_AUTO_COMPACT_KEY,
|
|
67
|
+
DEFAULT_INTERACTIVE_AUTO_COMPACT_TOKENS
|
|
68
|
+
),
|
|
56
69
|
};
|
|
57
70
|
}
|
|
58
71
|
|
|
@@ -92,6 +105,11 @@ export function setRefreshSettings(input: Partial<RefreshSettings>): void {
|
|
|
92
105
|
AUTO_COMPACT_KEY,
|
|
93
106
|
checkRefreshTokens(input.autoCompactTokens),
|
|
94
107
|
]);
|
|
108
|
+
if (input.interactiveAutoCompactTokens !== undefined)
|
|
109
|
+
checked.push([
|
|
110
|
+
INTERACTIVE_AUTO_COMPACT_KEY,
|
|
111
|
+
checkRefreshTokens(input.interactiveAutoCompactTokens),
|
|
112
|
+
]);
|
|
95
113
|
db.transaction(() => {
|
|
96
114
|
for (const [key, n] of checked) put.run(key, String(n));
|
|
97
115
|
})();
|
|
@@ -121,22 +139,46 @@ export function compactThreshold(
|
|
|
121
139
|
return window ? Math.min(wanted, window) : wanted;
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
// Claude Code's own auto-compact window for an agent, before the provider's
|
|
143
|
+
// adjustment; 0 is none. A person's session has its own window, in a chat
|
|
144
|
+
// or a terminal alike (a terminal can't be compacted between turns, so this
|
|
145
|
+
// is the only compaction it gets). An orchestrator starts over from its
|
|
146
|
+
// brief at its own threshold instead, since a summary would carry what it
|
|
147
|
+
// read from tasks and CI without its untrusted fence.
|
|
148
|
+
export function autoCompactTokensFor(
|
|
149
|
+
role: ModelRole,
|
|
150
|
+
s: RefreshSettings
|
|
151
|
+
): number {
|
|
152
|
+
if (role === "orchestrator") return 0;
|
|
153
|
+
return role === "interactive"
|
|
154
|
+
? s.interactiveAutoCompactTokens
|
|
155
|
+
: s.autoCompactTokens;
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
// What an agent is launched with so it compacts itself at the threshold.
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// without its untrusted fence. Nothing when it's off, so a window set in
|
|
128
|
-
// the user's own environment still applies.
|
|
159
|
+
// Nothing when it's off, so a window set in the user's own environment
|
|
160
|
+
// still applies.
|
|
129
161
|
export function autoCompactEnv(sessionId: string): Record<string, string> {
|
|
130
162
|
const row = db
|
|
131
|
-
.prepare(
|
|
163
|
+
.prepare(
|
|
164
|
+
`SELECT role, task_status, plan_phase, started_by, model_provider
|
|
165
|
+
FROM sessions WHERE id = ?`
|
|
166
|
+
)
|
|
132
167
|
.get(sessionId) as
|
|
133
|
-
| {
|
|
168
|
+
| {
|
|
169
|
+
role: string | null;
|
|
170
|
+
task_status: string | null;
|
|
171
|
+
plan_phase: string | null;
|
|
172
|
+
started_by: string | null;
|
|
173
|
+
model_provider: string | null;
|
|
174
|
+
}
|
|
134
175
|
| undefined;
|
|
135
|
-
|
|
136
|
-
|
|
176
|
+
if (!row) return {};
|
|
177
|
+
const tokens = autoCompactTokensFor(sessionRole(row), refreshSettings());
|
|
178
|
+
if (tokens <= 0) return {};
|
|
137
179
|
const window = compactThreshold(
|
|
138
180
|
Math.max(AUTO_COMPACT_MIN, tokens),
|
|
139
|
-
row
|
|
181
|
+
row.model_provider
|
|
140
182
|
);
|
|
141
183
|
return { [AUTO_COMPACT_ENV]: String(window) };
|
|
142
184
|
}
|
|
@@ -36,11 +36,11 @@ export function seedSession(opts: {
|
|
|
36
36
|
export function seedWorkspace(name = `ws-${randomUUID().slice(0, 6)}`) {
|
|
37
37
|
const workspace = createWorkspace(name);
|
|
38
38
|
const app = createProject({
|
|
39
|
-
name: `app-${randomUUID().slice(0,
|
|
39
|
+
name: `app-${randomUUID().slice(0, 8)}`,
|
|
40
40
|
workingDirectory: "/tmp/app",
|
|
41
41
|
});
|
|
42
42
|
const api = createProject({
|
|
43
|
-
name: `api-${randomUUID().slice(0,
|
|
43
|
+
name: `api-${randomUUID().slice(0, 8)}`,
|
|
44
44
|
workingDirectory: "/tmp/api",
|
|
45
45
|
});
|
|
46
46
|
setProjectWorkspace(app.id, workspace.id);
|
package/lib/sessions/end.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { Session } from "../db";
|
|
|
2
2
|
import { stopChat } from "../chat/runner";
|
|
3
3
|
import { waitForExit } from "../chat/worker/client";
|
|
4
4
|
import { endTmuxSession } from "../tmux/end";
|
|
5
|
-
import { releaseOnEnd } from "../simulator";
|
|
5
|
+
import { releaseOnEnd, shutTaskSimulators } from "../simulator";
|
|
6
6
|
import { forgetPlanWait } from "../models/plan-step";
|
|
7
7
|
|
|
8
|
-
type Agent = Pick<Session, "id" | "host_id" | "tmux_name"
|
|
8
|
+
type Agent = Pick<Session, "id" | "host_id" | "tmux_name"> &
|
|
9
|
+
Partial<Pick<Session, "worktree_path" | "branch_name">>;
|
|
9
10
|
|
|
10
11
|
// A session's agent, ended: its chat worker, and its own tmux session (a
|
|
11
12
|
// chat session can still own one). The one call that delete, done, drop and
|
|
@@ -15,6 +16,8 @@ export async function endSessionAgent(s: Agent): Promise<void> {
|
|
|
15
16
|
stopChat(s.id);
|
|
16
17
|
// The task's simulator goes with it; no row, no work.
|
|
17
18
|
releaseOnEnd(s.id, { remove: true });
|
|
19
|
+
// And any the project's own scripts booted for it; shut down only.
|
|
20
|
+
shutTaskSimulators(s);
|
|
18
21
|
void waitForExit(s.id).catch(() => {});
|
|
19
22
|
await endTmuxSession(s.host_id, s.tmux_name);
|
|
20
23
|
}
|
package/lib/simulator/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ export class SimulatorError extends Error {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export { simulatorAvailable };
|
|
63
|
+
export { shutTaskSimulators, shutFinishedTaskSimulators } from "./task-devices";
|
|
63
64
|
|
|
64
65
|
const session = (id: string) =>
|
|
65
66
|
db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
|
package/lib/simulator/simctl.ts
CHANGED
|
@@ -142,6 +142,16 @@ export async function shutdownDevice(device: {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// Shut down only, never delete or erase. Also used for devices a project's
|
|
146
|
+
// own scripts made, matched to a finished task by name (task-devices.ts).
|
|
147
|
+
export async function shutdownByUdid(udid: string): Promise<void> {
|
|
148
|
+
try {
|
|
149
|
+
await run(["shutdown", udid]);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!/current state: Shutdown/i.test(String(error))) throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
export async function deleteDevice(device: {
|
|
146
156
|
udid: string;
|
|
147
157
|
name: string;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booted simulators that belong to a task, shut down when it ends. Two naming
|
|
3
|
+
* schemes: AgentOS's own (`aos-<instance>-<task id>`, from `aos sim`) and the
|
|
4
|
+
* agentos-mobile project's (`agentos-<branch>-<worktree hash>`, from its
|
|
5
|
+
* `npm run sim`). Shut down only: nothing here deletes or erases a device.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import { db, type Session } from "../db";
|
|
11
|
+
import { isRemoteHost } from "../hosts";
|
|
12
|
+
import { simulatorAvailable } from "./available";
|
|
13
|
+
import {
|
|
14
|
+
deviceNameFor,
|
|
15
|
+
listDevices,
|
|
16
|
+
shutdownByUdid,
|
|
17
|
+
type SimDevice,
|
|
18
|
+
} from "./simctl";
|
|
19
|
+
|
|
20
|
+
// Same as agentos-mobile's scripts/lib/sim-helpers.mjs (slug, simName); the
|
|
21
|
+
// two must stay in step.
|
|
22
|
+
export const simSlug = (text: string) =>
|
|
23
|
+
text
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
+
.replace(/^-+|-+$/g, "")
|
|
27
|
+
.slice(0, 40);
|
|
28
|
+
|
|
29
|
+
export const projectSimName = (
|
|
30
|
+
branch: string | null | undefined,
|
|
31
|
+
worktreePath: string
|
|
32
|
+
) =>
|
|
33
|
+
`agentos-${simSlug(branch || "detached") || "detached"}-${createHash("sha1")
|
|
34
|
+
.update(worktreePath)
|
|
35
|
+
.digest("hex")
|
|
36
|
+
.slice(0, 6)}`;
|
|
37
|
+
|
|
38
|
+
export const FINISHED = new Set(["merged", "dropped", "done"]);
|
|
39
|
+
|
|
40
|
+
export interface TaskRef {
|
|
41
|
+
id: string;
|
|
42
|
+
branch_name: string | null;
|
|
43
|
+
worktree_path: string | null;
|
|
44
|
+
task_status?: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The script resolves its worktree through symlinks; the row may not.
|
|
48
|
+
export function pathVariants(p: string | null): string[] {
|
|
49
|
+
if (!p) return [];
|
|
50
|
+
try {
|
|
51
|
+
return [...new Set([p, fs.realpathSync(p)])];
|
|
52
|
+
} catch {
|
|
53
|
+
return [p];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function taskDeviceNames(
|
|
58
|
+
t: Pick<TaskRef, "id" | "branch_name">,
|
|
59
|
+
paths: string[]
|
|
60
|
+
): string[] {
|
|
61
|
+
return [
|
|
62
|
+
...new Set([
|
|
63
|
+
deviceNameFor(t.id),
|
|
64
|
+
...paths.map((p) => projectSimName(t.branch_name, p)),
|
|
65
|
+
]),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const running = (d: SimDevice) => d.state === "Booted" || d.state === "Booting";
|
|
70
|
+
|
|
71
|
+
// Running devices whose every matching task is finished: a task re-run on the
|
|
72
|
+
// same branch and worktree keeps its device, and unmatched ones are not ours.
|
|
73
|
+
export function planFinishedShutdowns(
|
|
74
|
+
devices: SimDevice[],
|
|
75
|
+
tasks: (TaskRef & { paths?: string[] })[]
|
|
76
|
+
): SimDevice[] {
|
|
77
|
+
const owners = new Map<string, boolean[]>();
|
|
78
|
+
for (const t of tasks) {
|
|
79
|
+
const finished = FINISHED.has(t.task_status ?? "");
|
|
80
|
+
const paths = t.paths ?? pathVariants(t.worktree_path);
|
|
81
|
+
for (const name of taskDeviceNames(t, paths))
|
|
82
|
+
owners.set(name, [...(owners.get(name) ?? []), finished]);
|
|
83
|
+
}
|
|
84
|
+
return devices.filter((d) => {
|
|
85
|
+
const o = owners.get(d.name);
|
|
86
|
+
return running(d) && !!o && o.every(Boolean);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const enabled = () =>
|
|
91
|
+
simulatorAvailable() && process.env.AGENTOS_SIM_SHUTDOWN !== "off";
|
|
92
|
+
|
|
93
|
+
async function shutAll(devices: SimDevice[], why: string): Promise<number> {
|
|
94
|
+
let n = 0;
|
|
95
|
+
for (const d of devices) {
|
|
96
|
+
try {
|
|
97
|
+
await shutdownByUdid(d.udid);
|
|
98
|
+
n++;
|
|
99
|
+
console.log(`[simulator] shut down ${d.name}: ${why}`);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[simulator] shutdown ${d.name}:`, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return n;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For a task that is ending. Never blocks and never throws into the cleanup.
|
|
108
|
+
export function shutTaskSimulators(
|
|
109
|
+
s: Pick<Session, "id" | "host_id"> &
|
|
110
|
+
Partial<Pick<Session, "branch_name" | "worktree_path">>
|
|
111
|
+
): void {
|
|
112
|
+
try {
|
|
113
|
+
if (!enabled() || isRemoteHost(s.host_id)) return;
|
|
114
|
+
// Before the caller removes the worktree, which realpath needs.
|
|
115
|
+
const names = new Set(
|
|
116
|
+
taskDeviceNames(
|
|
117
|
+
{ id: s.id, branch_name: s.branch_name ?? null },
|
|
118
|
+
pathVariants(s.worktree_path ?? null)
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
void listDevices()
|
|
122
|
+
.then((devices) =>
|
|
123
|
+
shutAll(
|
|
124
|
+
devices.filter((d) => running(d) && names.has(d.name)),
|
|
125
|
+
"its task ended"
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
.catch((error) => console.error("[simulator] list:", error));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("[simulator] shutTaskSimulators:", error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let sweeping = false;
|
|
135
|
+
|
|
136
|
+
// Shuts down booted simulators whose task is merged, dropped or done.
|
|
137
|
+
export async function shutFinishedTaskSimulators(): Promise<number> {
|
|
138
|
+
if (!enabled() || sweeping) return 0;
|
|
139
|
+
sweeping = true;
|
|
140
|
+
try {
|
|
141
|
+
const devices = (await listDevices()).filter(
|
|
142
|
+
(d) =>
|
|
143
|
+
(running(d) && d.name.startsWith("agentos-")) ||
|
|
144
|
+
(running(d) && d.name.startsWith("aos-"))
|
|
145
|
+
);
|
|
146
|
+
if (devices.length === 0) return 0;
|
|
147
|
+
const tasks = (
|
|
148
|
+
db
|
|
149
|
+
.prepare(
|
|
150
|
+
`SELECT id, branch_name, worktree_path, task_status, host_id
|
|
151
|
+
FROM sessions WHERE task_status IS NOT NULL`
|
|
152
|
+
)
|
|
153
|
+
.all() as (TaskRef & { host_id: string })[]
|
|
154
|
+
).filter((t) => !isRemoteHost(t.host_id));
|
|
155
|
+
const stale = planFinishedShutdowns(devices, tasks);
|
|
156
|
+
return await shutAll(stale, "its task is finished");
|
|
157
|
+
} finally {
|
|
158
|
+
sweeping = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -79,7 +79,7 @@ import { claimAgentosEnv } from "./lib/agents/self-env";
|
|
|
79
79
|
import { wslNotes, wslVersion } from "./lib/wsl";
|
|
80
80
|
import { getAllProjects } from "./lib/projects";
|
|
81
81
|
import { isRemoteHost } from "./lib/hosts";
|
|
82
|
-
import { sweepSimulators } from "./lib/simulator";
|
|
82
|
+
import { shutFinishedTaskSimulators, sweepSimulators } from "./lib/simulator";
|
|
83
83
|
|
|
84
84
|
const dev = process.env.NODE_ENV !== "production";
|
|
85
85
|
const hostname = "127.0.0.1";
|
|
@@ -385,6 +385,7 @@ app.prepare().then(async () => {
|
|
|
385
385
|
sweepingSims = true;
|
|
386
386
|
try {
|
|
387
387
|
await sweepSimulators();
|
|
388
|
+
await shutFinishedTaskSimulators();
|
|
388
389
|
} catch (error) {
|
|
389
390
|
console.error("Sweeping simulators failed:", error);
|
|
390
391
|
} finally {
|