feinai 0.8.3 → 0.8.5
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 +12 -0
- package/package.json +1 -1
- package/src/agents-status.ts +77 -48
- package/src/cli.ts +1 -1
- package/src/dashboard.html +12 -2
- package/src/server.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.5] - 2026-09-07
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Contador "active agents" del navbar sobrecontaba y oscilaba.** El pill sumaba `agentProcesses.length`, es decir, todos los procesos con cwd dentro de un worktree — pero una sesión de agente real suele lanzar procesos hijos (`git`, `bun`, `rg`, `tsc`, ...) que también quedan momentáneamente con cwd en ese worktree y se contaban como agentes separados. Esto inflaba el número (ej. 5 en vez de 1) y lo hacía oscilar entre polls de 5s a medida que esos hijos aparecían y desaparecían, mientras que la card de un task individual sólo reflejaba ese task puntual. `renderAgentsPill()` ahora agrupa por `taskId` y cuenta tasks distintas con al menos un proceso matcheado, en línea con lo que ya mostraba cada worktree card.
|
|
11
|
+
|
|
12
|
+
## [0.8.4] - 2026-09-02
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
6
18
|
## [0.8.3] - 2026-09-02
|
|
7
19
|
|
|
8
20
|
### 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/dashboard.html
CHANGED
|
@@ -1721,8 +1721,18 @@
|
|
|
1721
1721
|
function renderAgentsPill() {
|
|
1722
1722
|
const el = $("#agents-pill");
|
|
1723
1723
|
if (!el) return;
|
|
1724
|
-
|
|
1725
|
-
|
|
1724
|
+
// Group by taskId: a single agent session commonly spawns child
|
|
1725
|
+
// processes (git, bun, rg, tsc, ...) whose cwd also lands inside the
|
|
1726
|
+
// worktree, so agentProcesses.length overcounts and flickers as those
|
|
1727
|
+
// children come and go. What "active agents" means is distinct tasks
|
|
1728
|
+
// with at least one matched process.
|
|
1729
|
+
const byTask = new Map();
|
|
1730
|
+
for (const a of agentProcesses) {
|
|
1731
|
+
if (!byTask.has(a.taskId)) byTask.set(a.taskId, []);
|
|
1732
|
+
byTask.get(a.taskId).push(a);
|
|
1733
|
+
}
|
|
1734
|
+
const total = byTask.size;
|
|
1735
|
+
const verified = [...byTask.values()].filter((procs) => procs.some((a) => a.verified)).length;
|
|
1726
1736
|
const unverified = total - verified;
|
|
1727
1737
|
el.classList.toggle("live", total > 0);
|
|
1728
1738
|
const title = total === 0
|
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
|
}
|