@kylecheng3146/agent-ops 0.7.2 → 0.7.3
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.
|
@@ -141,6 +141,21 @@ export async function worktreeDiff(git, path, base) {
|
|
|
141
141
|
}
|
|
142
142
|
return { files: paths.length, insertions, deletions, paths: paths.slice(0, MAX_PATHS), recent: recent ?? paths[0] ?? null };
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Work already in the target branch by some route other than `worktree finish`
|
|
146
|
+
* (a merged pull request): commits past its base, all reachable from the
|
|
147
|
+
* target, and nothing uncommitted. A fresh worktree (HEAD at base) is not merged.
|
|
148
|
+
* ponytail: ancestry only, so a squash-merged branch stays visible until removed.
|
|
149
|
+
*/
|
|
150
|
+
export async function worktreeMerged(git, path, base, targetBranch) {
|
|
151
|
+
const head = (await git(path, ["rev-parse", "HEAD"])).stdout.trim();
|
|
152
|
+
if (!/^[0-9a-f]{40,64}$/u.test(head) || head === base || !/^[A-Za-z0-9._/-]{1,128}$/u.test(targetBranch))
|
|
153
|
+
return false;
|
|
154
|
+
if ((await git(path, ["merge-base", "--is-ancestor", head, `refs/heads/${targetBranch}`])).exitCode !== 0)
|
|
155
|
+
return false;
|
|
156
|
+
const status = await git(path, ["status", "--porcelain"]);
|
|
157
|
+
return status.exitCode === 0 && status.stdout.trim() === "";
|
|
158
|
+
}
|
|
144
159
|
/** Each run is read on its own: one unreadable run must not hide the building. */
|
|
145
160
|
export async function readRuns(commonDir) {
|
|
146
161
|
const directory = join(commonDir, "agent-ops", "runs");
|
|
@@ -207,14 +222,21 @@ export async function readReviewSlots(commonDir) {
|
|
|
207
222
|
export async function collectOfficeInput(mainRoot, commonDir, git = officeGit, now = Date.now()) {
|
|
208
223
|
const worktrees = [];
|
|
209
224
|
const discovered = [];
|
|
225
|
+
const merged = new Set();
|
|
210
226
|
for (const entry of parseWorktreeListPorcelain((await git(mainRoot, ["worktree", "list", "--porcelain"])).stdout)) {
|
|
211
227
|
const path = await realpath(resolve(entry.path)).catch(() => resolve(entry.path));
|
|
212
228
|
const record = await readWorktreeRecord(path).catch(() => null);
|
|
213
229
|
if (record === null)
|
|
214
230
|
continue;
|
|
231
|
+
if (record.runId === undefined && await worktreeMerged(git, path, record.base, record.targetBranch)) {
|
|
232
|
+
// A subagent's worktree shares its coordinator's session, which stays.
|
|
233
|
+
if (record.agentId === undefined)
|
|
234
|
+
merged.add(record.sessionId);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
215
237
|
discovered.push({ path, record });
|
|
216
238
|
}
|
|
217
|
-
const sessions = await readSessions(mainRoot, commonDir, now, [mainRoot, ...discovered.map(item => item.path)]);
|
|
239
|
+
const sessions = (await readSessions(mainRoot, commonDir, now, [mainRoot, ...discovered.map(item => item.path)])).filter(session => !merged.has(session.sessionId));
|
|
218
240
|
for (const { path, record } of discovered) {
|
|
219
241
|
const session = sessions.find(item => item.sessionId === record.sessionId);
|
|
220
242
|
const task = await attachedTaskView(path, record.sessionId, session?.taskId);
|
|
@@ -68,13 +68,12 @@ function retained(record, now) {
|
|
|
68
68
|
const lastSeen = Date.parse(record.lastSeenAt);
|
|
69
69
|
if (!Number.isFinite(lastSeen))
|
|
70
70
|
return false;
|
|
71
|
-
if (record.status === "active") {
|
|
72
|
-
// ponytail: one TTL handles crashed hook clients
|
|
71
|
+
if (record.status === "active" || record.completedAt === null || record.completedAt === undefined) {
|
|
72
|
+
// ponytail: one TTL handles crashed hook clients and windows closed without `end`;
|
|
73
|
+
// a persistent host heartbeat can replace it if needed.
|
|
73
74
|
return now - lastSeen <= OFFICE_SESSION_STALE_MS;
|
|
74
75
|
}
|
|
75
|
-
const completed =
|
|
76
|
-
? lastSeen
|
|
77
|
-
: Date.parse(record.completedAt);
|
|
76
|
+
const completed = Date.parse(record.completedAt);
|
|
78
77
|
return Number.isFinite(completed) && now - completed < OFFICE_SESSION_RETENTION_MS;
|
|
79
78
|
}
|
|
80
79
|
function prune(records, now) {
|