feinai 0.8.3 → 0.8.4
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/agents-status.ts +77 -48
- package/src/cli.ts +1 -1
- package/src/server.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
5
|
|
|
6
|
+
## [0.8.4] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Detección de agentes activos no reflejaba agentes reales.** El matching en 0.8.3 seguía dependiendo de encontrar el substring `TASK-...` en la línea de comando del proceso — pero sesiones reales (`claude`, `opencode` corriendo cd'd al worktree de la tarea) casi nunca tienen el task id en su `argv`, así que nunca matcheaban. Además `task.worktree` se guarda relativo al root del proyecto (`.worktrees/TASK-XXX`) y se comparaba tal cual contra rutas absolutas de `/proc/:pid/cwd`, que tampoco matcheaban nunca. `listAgentProcesses` ahora usa el cwd del proceso como señal primaria: escanea `/proc/*/cwd` de todos los procesos y los compara contra el worktree absoluto de cada tarea `in_progress` (resuelto en `server.ts` con `resolve(process.cwd(), task.worktree)`). El grep por `TASK-...` en el comando queda sólo como fallback de baja confianza (`verified: false`) para tareas sin worktree.
|
|
11
|
+
|
|
6
12
|
## [0.8.3] - 2026-09-02
|
|
7
13
|
|
|
8
14
|
### Added
|
package/package.json
CHANGED
package/src/agents-status.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readlink } from "node:fs/promises";
|
|
1
|
+
import { readdir, readlink } from "node:fs/promises";
|
|
2
2
|
|
|
3
3
|
export interface AgentProcess {
|
|
4
4
|
pid: number;
|
|
@@ -12,68 +12,97 @@ export interface AgentProcess {
|
|
|
12
12
|
|
|
13
13
|
export interface ExpectedAgent {
|
|
14
14
|
taskId: string;
|
|
15
|
+
/** absolute path to the task's worktree, or null if it has none */
|
|
15
16
|
worktree: string | null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* process already exited, or /proc isn't readable), but reported as
|
|
27
|
-
* `verified: false` since a substring match can be coincidental or belong
|
|
28
|
-
* to an unrelated project using the same task ID scheme.
|
|
29
|
-
*/
|
|
30
|
-
export async function listAgentProcesses(
|
|
31
|
-
expected: ExpectedAgent[] = [],
|
|
32
|
-
): Promise<AgentProcess[]> {
|
|
33
|
-
let lines: string[];
|
|
19
|
+
interface PsRow {
|
|
20
|
+
pid: number;
|
|
21
|
+
cpu: number;
|
|
22
|
+
mem: number;
|
|
23
|
+
command: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function psSnapshot(): Promise<PsRow[]> {
|
|
34
27
|
try {
|
|
35
28
|
const result = await Bun.$`ps -eo pid,pcpu,pmem,command`.text();
|
|
36
|
-
|
|
29
|
+
return result
|
|
30
|
+
.trim()
|
|
31
|
+
.split("\n")
|
|
32
|
+
.slice(1)
|
|
33
|
+
.map((line) => {
|
|
34
|
+
const parts = line.trim().split(/\s+/);
|
|
35
|
+
return {
|
|
36
|
+
pid: parseInt(parts[0]!, 10),
|
|
37
|
+
cpu: parseFloat(parts[1]!),
|
|
38
|
+
mem: parseFloat(parts[2]!),
|
|
39
|
+
command: parts.slice(3).join(" "),
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
.filter((p) => Number.isFinite(p.pid));
|
|
37
43
|
} catch {
|
|
38
44
|
return [];
|
|
39
45
|
}
|
|
46
|
+
}
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Snapshot of processes working on active tasks.
|
|
50
|
+
*
|
|
51
|
+
* Real agent sessions here (an interactive `claude` or `opencode` process cd'd
|
|
52
|
+
* into a task's git worktree) rarely put the task ID anywhere in their
|
|
53
|
+
* command line — so grepping ps output for `TASK-...` misses them entirely.
|
|
54
|
+
* The reliable signal is the process's current working directory: for each
|
|
55
|
+
* in_progress task with a worktree, we check every process's
|
|
56
|
+
* `/proc/:pid/cwd` against that worktree path. A cwd match is `verified: true`.
|
|
57
|
+
*
|
|
58
|
+
* As a fallback (e.g. a task has no worktree yet, or /proc isn't readable),
|
|
59
|
+
* we also look for a literal `TASK-...` substring in the command line —
|
|
60
|
+
* kept as `verified: false` since it's coincidental at best.
|
|
61
|
+
*/
|
|
62
|
+
export async function listAgentProcesses(
|
|
63
|
+
expected: ExpectedAgent[] = [],
|
|
64
|
+
): Promise<AgentProcess[]> {
|
|
65
|
+
const rows = await psSnapshot();
|
|
66
|
+
if (!rows.length) return [];
|
|
46
67
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const parts = line.trim().split(/\s+/);
|
|
50
|
-
if (parts.length < 4) continue;
|
|
51
|
-
const pid = parseInt(parts[0]!, 10);
|
|
52
|
-
const cpu = parseFloat(parts[1]!);
|
|
53
|
-
const mem = parseFloat(parts[2]!);
|
|
54
|
-
const command = parts.slice(3).join(" ");
|
|
55
|
-
if (!Number.isFinite(pid)) continue;
|
|
56
|
-
const taskMatch = command.match(/TASK-[A-Z0-9-]+/);
|
|
57
|
-
const taskId = taskMatch ? taskMatch[0] : null;
|
|
58
|
-
if (!taskId) continue;
|
|
59
|
-
candidates.push({ pid, cpu, mem, command, taskId });
|
|
60
|
-
}
|
|
68
|
+
const pids = await readdir("/proc").catch(() => [] as string[]);
|
|
69
|
+
const numericPids = new Set(pids.filter((p) => /^\d+$/.test(p)).map(Number));
|
|
61
70
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const cwdByPid = new Map<number, string>();
|
|
72
|
+
await Promise.all(
|
|
73
|
+
rows
|
|
74
|
+
.filter((r) => numericPids.has(r.pid))
|
|
75
|
+
.map(async (r) => {
|
|
67
76
|
try {
|
|
68
|
-
|
|
69
|
-
verified = cwd === worktree || cwd.startsWith(worktree + "/");
|
|
77
|
+
cwdByPid.set(r.pid, await readlink(`/proc/${r.pid}/cwd`));
|
|
70
78
|
} catch {
|
|
71
|
-
|
|
79
|
+
// process exited or unreadable — no cwd signal for it
|
|
72
80
|
}
|
|
73
|
-
}
|
|
74
|
-
return { ...c, verified };
|
|
75
|
-
}),
|
|
81
|
+
}),
|
|
76
82
|
);
|
|
77
83
|
|
|
84
|
+
const results: AgentProcess[] = [];
|
|
85
|
+
const matchedPids = new Set<number>();
|
|
86
|
+
|
|
87
|
+
for (const { taskId, worktree } of expected) {
|
|
88
|
+
if (!worktree) continue;
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
if (matchedPids.has(row.pid)) continue;
|
|
91
|
+
const cwd = cwdByPid.get(row.pid);
|
|
92
|
+
if (!cwd) continue;
|
|
93
|
+
if (cwd === worktree || cwd.startsWith(worktree + "/")) {
|
|
94
|
+
matchedPids.add(row.pid);
|
|
95
|
+
results.push({ ...row, taskId, verified: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
if (matchedPids.has(row.pid)) continue;
|
|
102
|
+
const taskMatch = row.command.match(/TASK-[A-Z0-9-]+/);
|
|
103
|
+
if (!taskMatch) continue;
|
|
104
|
+
results.push({ ...row, taskId: taskMatch[0], verified: false });
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
return results;
|
|
79
108
|
}
|
package/src/cli.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -441,10 +441,16 @@ export function startServer(opts: ServerOptions): { url: string; stop: () => voi
|
|
|
441
441
|
return json(status);
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
// GET /api/agents — running
|
|
444
|
+
// GET /api/agents — running agent processes with task matching
|
|
445
445
|
if (path === "/api/agents" && method === "GET") {
|
|
446
446
|
const inProgress = listTasks(db, { status: "in_progress" });
|
|
447
|
-
|
|
447
|
+
// task.worktree is stored relative to the project root the server
|
|
448
|
+
// was launched from — resolve it so it can be compared against the
|
|
449
|
+
// absolute paths /proc/:pid/cwd returns.
|
|
450
|
+
const expected = inProgress.map((t) => ({
|
|
451
|
+
taskId: t.id,
|
|
452
|
+
worktree: t.worktree ? resolve(process.cwd(), t.worktree) : null,
|
|
453
|
+
}));
|
|
448
454
|
const agents = await listAgentProcesses(expected);
|
|
449
455
|
return json(agents);
|
|
450
456
|
}
|