@saadnvd1/agent-os 0.3.477 → 0.3.481
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 +26 -1
- package/app/api/schedules/route.ts +1 -0
- package/app/api/sessions/[id]/route.test.ts +66 -0
- package/app/api/sessions/[id]/route.ts +10 -7
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/tmux/kill-all/route.ts +2 -1
- package/components/Orchestrator/OrchestratorBar.tsx +12 -0
- package/components/Schedules/ScheduleForm.tsx +94 -12
- package/components/Schedules/SchedulesPanel.tsx +60 -14
- package/components/SessionList/OrchestratorPinRow.tsx +12 -6
- package/components/SessionList/QuickChangeRow.tsx +58 -0
- package/components/SessionList/SessionRow.tsx +4 -0
- package/components/SessionList/SidebarHeader.tsx +1 -1
- package/components/SessionList/useSchedulesGlance.ts +6 -2
- package/components/Tasks/TaskRow.tsx +2 -17
- package/components/Tasks/task-state-labels.ts +18 -0
- package/lib/away/sweep.ts +2 -0
- package/lib/chat/drivers/demo.ts +2 -1
- package/lib/chat/hold.ts +6 -2
- package/lib/chat/runner.ts +5 -0
- package/lib/chat/test-touch.ts +58 -12
- package/lib/chat/worker/client.ts +2 -3
- package/lib/db/migrations.ts +31 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/index.ts +2 -8
- package/lib/orchestration.ts +4 -5
- package/lib/orchestrator/act.ts +3 -6
- package/lib/orchestrator/brief.ts +8 -3
- package/lib/orchestrator/conditions.ts +2 -1
- package/lib/orchestrator/describe.ts +1 -1
- package/lib/orchestrator/facts.ts +13 -0
- package/lib/orchestrator/git-guard.ts +201 -0
- package/lib/orchestrator/pr-wake.ts +9 -1
- package/lib/orchestrator/quick-store.ts +18 -0
- package/lib/orchestrator/quick.ts +307 -0
- package/lib/orchestrator/rebase.ts +11 -1
- package/lib/orchestrator/reminders.ts +91 -0
- package/lib/orchestrator/review.ts +3 -0
- package/lib/orchestrator/serve.ts +26 -0
- package/lib/orchestrator/signoff.ts +28 -6
- package/lib/orchestrator/task-state.ts +2 -0
- package/lib/orchestrator/tool-names.ts +12 -2
- package/lib/orchestrator/tool-schemas.ts +43 -0
- package/lib/orchestrator/tools.ts +10 -0
- package/lib/orchestrator/write-guard.ts +80 -3
- package/lib/schedules/badge.ts +11 -1
- package/lib/schedules/cron.ts +1 -1
- package/lib/schedules/index.ts +67 -6
- package/lib/schedules/run.ts +23 -6
- package/lib/schedules/scheduler.ts +12 -2
- package/lib/schedules/start.ts +11 -5
- package/lib/schedules/store.ts +133 -16
- package/lib/schedules/when.ts +166 -0
- package/lib/sessions/end.ts +15 -0
- package/lib/sidebar/shelves.ts +19 -1
- package/lib/tasks/finish.ts +2 -8
- package/lib/tasks/index.ts +18 -6
- package/lib/tasks/move-guard.ts +2 -0
- package/lib/tasks/move.ts +2 -6
- package/lib/tasks/queue.ts +3 -2
- package/lib/tasks/start.ts +2 -4
- package/lib/tmux/end.ts +33 -0
- package/lib/tmux/orphans.ts +146 -0
- package/package.json +1 -1
- package/server.ts +9 -0
package/lib/tasks/start.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { execFile } from "child_process";
|
|
10
10
|
import { promisify } from "util";
|
|
11
|
+
import { endTmuxSession } from "../tmux/end";
|
|
11
12
|
import { db, type Session } from "../db";
|
|
12
13
|
import { getProject } from "../projects";
|
|
13
14
|
import { setupWorktree } from "../env-setup";
|
|
@@ -195,10 +196,7 @@ function earlierSetup(row: SetupColumns): TaskSetup | undefined {
|
|
|
195
196
|
return undefined;
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
export const killTmux = (name: string) =>
|
|
199
|
-
execFileAsync("tmux", ["kill-session", "-t", `=${name}`], {
|
|
200
|
-
timeout: 5000,
|
|
201
|
-
}).catch(() => undefined);
|
|
199
|
+
export const killTmux = (name: string) => endTmuxSession(null, name);
|
|
202
200
|
|
|
203
201
|
const tmuxAlive = (name: string) =>
|
|
204
202
|
execFileAsync("tmux", ["has-session", "-t", `=${name}`], {
|
package/lib/tmux/end.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { hostExec } from "../hosts";
|
|
2
|
+
import { shellQuote } from "../hosts/ssh";
|
|
3
|
+
import { isValidTmuxName } from "../hosts/tmux-name";
|
|
4
|
+
|
|
5
|
+
// Run by sh on the machine the session lives on. The pane processes and
|
|
6
|
+
// everything under them are read before tmux ends the session (the kill only
|
|
7
|
+
// hangs up the panes' own shells), then asked to stop and, if any is still
|
|
8
|
+
// there a second later, killed. Only pids found under this session's panes
|
|
9
|
+
// are ever signalled.
|
|
10
|
+
export function endTmuxScript(name: string): string {
|
|
11
|
+
return [
|
|
12
|
+
`t=${shellQuote(`=${name}`)}`,
|
|
13
|
+
`roots=$(tmux list-panes -s -t "$t" -F '#{pane_pid}' 2>/dev/null)`,
|
|
14
|
+
`tree=$(ps -Ao pid=,ppid= | awk -v roots="$roots" 'BEGIN{n=split(roots,r,/[ \\n]+/);for(i=1;i<=n;i++)if(r[i]!="")s[r[i]]=1} {p[$1]=$2;a[NR]=$1} END{do{c=0;for(i=1;i<=NR;i++){k=a[i];if(!(k in s)&&(p[k] in s)){s[k]=1;c=1}}}while(c);for(k in s)print k}')`,
|
|
15
|
+
`tmux kill-session -t "$t" 2>/dev/null`,
|
|
16
|
+
`if [ -n "$tree" ]; then`,
|
|
17
|
+
` kill -TERM $tree 2>/dev/null`,
|
|
18
|
+
` alive=; for p in $tree; do kill -0 "$p" 2>/dev/null && alive="$alive $p"; done`,
|
|
19
|
+
` if [ -n "$alive" ]; then sleep 1; kill -KILL $alive 2>/dev/null; fi`,
|
|
20
|
+
`fi`,
|
|
21
|
+
`exit 0`,
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Ends a tmux session and what its panes started, on the machine it runs on.
|
|
26
|
+
// A session that isn't there is nothing to do; this never throws.
|
|
27
|
+
export async function endTmuxSession(
|
|
28
|
+
hostId: string | null | undefined,
|
|
29
|
+
name: string | null | undefined
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
if (!name || !isValidTmuxName(name)) return;
|
|
32
|
+
await hostExec(hostId, endTmuxScript(name), 15_000).catch(() => {});
|
|
33
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { db } from "../db";
|
|
2
|
+
import { getManagedSessionPattern } from "../providers/registry";
|
|
3
|
+
import { WORKER_TMUX_PREFIX } from "../chat/worker/protocol";
|
|
4
|
+
import { isValidTmuxName } from "../hosts/tmux-name";
|
|
5
|
+
import { endTmuxSession } from "./end";
|
|
6
|
+
import { tmux } from "./exec";
|
|
7
|
+
|
|
8
|
+
// tmux sessions AgentOS made whose session row is gone: deleted before every
|
|
9
|
+
// end-of-life path ended the agent. Swept once at start. Only sessions that
|
|
10
|
+
// carry AgentOS's own marks are ever touched: its names (<agent>-<uuid>,
|
|
11
|
+
// aos-chat-<uuid>) or the AGENTOS_SESSION_ID it sets on the session at
|
|
12
|
+
// launch. Dispatch's sessions and anyone's own terminals have neither.
|
|
13
|
+
|
|
14
|
+
const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
15
|
+
const WORKER_NAME = new RegExp(`^${WORKER_TMUX_PREFIX}(${UUID})$`, "i");
|
|
16
|
+
const AGENT_NAME = new RegExp(`(${UUID})$`, "i");
|
|
17
|
+
|
|
18
|
+
export interface TmuxListing {
|
|
19
|
+
name: string;
|
|
20
|
+
// AGENTOS_SESSION_ID on the session itself; null when absent or when the
|
|
21
|
+
// whole tmux server carries one (every session would then).
|
|
22
|
+
taggedId: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionRow {
|
|
26
|
+
id: string;
|
|
27
|
+
tmux_name: string | null;
|
|
28
|
+
host_id: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isLocal = (hostId: string | null) => !hostId || hostId === "local";
|
|
32
|
+
|
|
33
|
+
// The AgentOS session id a tmux session belongs to, or null if it isn't one
|
|
34
|
+
// of AgentOS's.
|
|
35
|
+
export function agentOsIdOf(
|
|
36
|
+
l: TmuxListing,
|
|
37
|
+
managed = getManagedSessionPattern()
|
|
38
|
+
): string | null {
|
|
39
|
+
const worker = WORKER_NAME.exec(l.name);
|
|
40
|
+
if (worker) return worker[1].toLowerCase();
|
|
41
|
+
if (managed.test(l.name)) return AGENT_NAME.exec(l.name)![1].toLowerCase();
|
|
42
|
+
return l.taggedId && new RegExp(`^${UUID}$`, "i").test(l.taggedId)
|
|
43
|
+
? l.taggedId.toLowerCase()
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function findOrphans(
|
|
48
|
+
listing: TmuxListing[],
|
|
49
|
+
rows: SessionRow[]
|
|
50
|
+
): { name: string; id: string }[] {
|
|
51
|
+
const ids = new Set(rows.map((r) => r.id.toLowerCase()));
|
|
52
|
+
const names = new Set(
|
|
53
|
+
rows.filter((r) => isLocal(r.host_id)).map((r) => r.tmux_name)
|
|
54
|
+
);
|
|
55
|
+
const orphans: { name: string; id: string }[] = [];
|
|
56
|
+
for (const l of listing) {
|
|
57
|
+
const id = agentOsIdOf(l);
|
|
58
|
+
if (id && !ids.has(id) && !names.has(l.name))
|
|
59
|
+
orphans.push({ name: l.name, id });
|
|
60
|
+
}
|
|
61
|
+
return orphans;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function taggedId(
|
|
65
|
+
name: string,
|
|
66
|
+
serverWide: boolean
|
|
67
|
+
): Promise<string | null> {
|
|
68
|
+
if (serverWide) return null;
|
|
69
|
+
const out = await tmux(
|
|
70
|
+
["show-environment", "-t", `=${name}`, "AGENTOS_SESSION_ID"],
|
|
71
|
+
5000
|
|
72
|
+
).catch(() => "");
|
|
73
|
+
return /^AGENTOS_SESSION_ID=(.+)$/m.exec(out)?.[1].trim() ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function listLocal(createdBefore: number): Promise<TmuxListing[]> {
|
|
77
|
+
const out = await tmux(
|
|
78
|
+
["list-sessions", "-F", "#{session_created} #{session_name}"],
|
|
79
|
+
5000
|
|
80
|
+
).catch(() => "");
|
|
81
|
+
// Sessions made after the cutoff belong to work this process started: their
|
|
82
|
+
// rows may not be committed yet, so they are never candidates.
|
|
83
|
+
const names = out
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((l) => /^(\d+) (.+)$/.exec(l))
|
|
86
|
+
.filter((m): m is RegExpExecArray => !!m && Number(m[1]) <= createdBefore)
|
|
87
|
+
.map((m) => m[2])
|
|
88
|
+
.filter(isValidTmuxName);
|
|
89
|
+
// A server started from inside an agent's shell has the variable in its
|
|
90
|
+
// own environment, which every session on it then shows.
|
|
91
|
+
const global = await tmux(
|
|
92
|
+
["show-environment", "-g", "AGENTOS_SESSION_ID"],
|
|
93
|
+
5000
|
|
94
|
+
).catch(() => "");
|
|
95
|
+
const serverWide = /^AGENTOS_SESSION_ID=/m.test(global);
|
|
96
|
+
const managed = getManagedSessionPattern();
|
|
97
|
+
const listing: TmuxListing[] = [];
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
const byName = WORKER_NAME.test(name) || managed.test(name);
|
|
100
|
+
listing.push({
|
|
101
|
+
name,
|
|
102
|
+
taggedId: byName ? null : await taggedId(name, serverWide),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return listing;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasRow = (id: string, tmuxName: string) =>
|
|
109
|
+
!!db
|
|
110
|
+
.prepare(`SELECT 1 FROM sessions WHERE id = ? OR tmux_name = ?`)
|
|
111
|
+
.get(id, tmuxName);
|
|
112
|
+
|
|
113
|
+
export async function sweepOrphanTmux(
|
|
114
|
+
opts: {
|
|
115
|
+
dryRun?: boolean;
|
|
116
|
+
rows?: SessionRow[];
|
|
117
|
+
// Epoch seconds; only tmux sessions created at or before it are swept.
|
|
118
|
+
// Defaults to when this process started.
|
|
119
|
+
createdBefore?: number;
|
|
120
|
+
} = {}
|
|
121
|
+
): Promise<{ found: string[]; killed: string[] }> {
|
|
122
|
+
const none = { found: [], killed: [] };
|
|
123
|
+
const rows =
|
|
124
|
+
opts.rows ??
|
|
125
|
+
(db
|
|
126
|
+
.prepare(`SELECT id, tmux_name, host_id FROM sessions`)
|
|
127
|
+
.all() as SessionRow[]);
|
|
128
|
+
// An empty table is a fresh or wrong database, not proof that nothing is
|
|
129
|
+
// alive: never sweep against it.
|
|
130
|
+
if (!rows.length) return none;
|
|
131
|
+
const cutoff =
|
|
132
|
+
opts.createdBefore ?? Math.floor(Date.now() / 1000 - process.uptime());
|
|
133
|
+
const orphans = findOrphans(await listLocal(cutoff), rows);
|
|
134
|
+
const killed: string[] = [];
|
|
135
|
+
for (const o of orphans) {
|
|
136
|
+
if (opts.dryRun) continue;
|
|
137
|
+
// The snapshot is old by now: a row inserted since keeps its session.
|
|
138
|
+
if (!opts.rows && hasRow(o.id, o.name)) continue;
|
|
139
|
+
await endTmuxSession(null, o.name);
|
|
140
|
+
killed.push(o.name);
|
|
141
|
+
console.log(`[tmux-sweep] ended ${o.name}: session ${o.id} has no row`);
|
|
142
|
+
}
|
|
143
|
+
if (killed.length)
|
|
144
|
+
console.log(`[tmux-sweep] ended ${killed.length} orphaned tmux sessions`);
|
|
145
|
+
return { found: orphans.map((o) => o.name), killed };
|
|
146
|
+
}
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { installClaudeStatusHooks } from "./lib/program-status/claude-hooks";
|
|
|
61
61
|
import os from "os";
|
|
62
62
|
import { resumeHeldStarts, resumeTaskStarts } from "./lib/tasks/start";
|
|
63
63
|
import { failInterruptedSetups } from "./lib/sessions/worktree-setup";
|
|
64
|
+
import { sweepOrphanTmux } from "./lib/tmux/orphans";
|
|
64
65
|
import { releaseInterruptedClaims } from "./lib/orchestrator/ask-approvals";
|
|
65
66
|
import { resumeReviews } from "./lib/orchestrator/review";
|
|
66
67
|
import { claimAgentosEnv } from "./lib/agents/self-env";
|
|
@@ -357,6 +358,14 @@ app.prepare().then(async () => {
|
|
|
357
358
|
}
|
|
358
359
|
// New sessions whose worktree setup a restart cut off.
|
|
359
360
|
failInterruptedSetups();
|
|
361
|
+
// tmux sessions of sessions deleted before agents were ended with them.
|
|
362
|
+
// Production only: a dev or test instance has its own database but shares
|
|
363
|
+
// the default tmux server, so it would see the real instance's sessions as
|
|
364
|
+
// orphans.
|
|
365
|
+
if (!dev && process.env.AGENTOS_TMUX_SWEEP !== "off")
|
|
366
|
+
sweepOrphanTmux().catch((error) =>
|
|
367
|
+
console.error("Sweeping orphaned tmux sessions failed:", error)
|
|
368
|
+
);
|
|
360
369
|
// Chat turns that kept running through a restart.
|
|
361
370
|
void reattachChats();
|
|
362
371
|
// Reviews this restart cut off run again and deliver their verdicts.
|