@phnx-labs/agents-cli 1.22.118 → 1.22.119
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 +66 -0
- package/dist/commands/accounts.js +7 -4
- package/dist/commands/projects.js +92 -7
- package/dist/lib/accounts/add.d.ts +3 -1
- package/dist/lib/accounts/add.js +24 -13
- package/dist/lib/agent-spec/agents.js +1 -0
- package/dist/lib/claude-account-token.d.ts +7 -1
- package/dist/lib/claude-account-token.js +26 -4
- package/dist/lib/exec.js +10 -2
- package/dist/lib/github/pr-mergeable.d.ts +4 -2
- package/dist/lib/github/pr-mergeable.js +5 -3
- package/dist/lib/github/project-prs.d.ts +58 -3
- package/dist/lib/github/project-prs.js +205 -7
- package/dist/lib/harness/adapters/cursor.js +14 -4
- package/dist/lib/harness-auth-capabilities.d.ts +4 -3
- package/dist/lib/harness-auth-capabilities.js +1 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -5
- package/dist/lib/installations/store.d.ts +9 -0
- package/dist/lib/installations/store.js +24 -0
- package/dist/lib/installations/versions.js +3 -2
- package/dist/lib/projects.d.ts +16 -0
- package/dist/lib/projects.js +31 -0
- package/dist/lib/session/active.d.ts +17 -6
- package/dist/lib/session/active.js +105 -49
- package/dist/lib/session/pid-registry.d.ts +6 -3
- package/dist/lib/session/pid-registry.js +18 -8
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +7 -3
- package/dist/lib/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/lib/projects.js
CHANGED
|
@@ -411,6 +411,37 @@ function projectRootsAbs(defs) {
|
|
|
411
411
|
}
|
|
412
412
|
return out;
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* The repository paths a project claims, as `{ slug, prefix }` with `prefix`
|
|
416
|
+
* repo-relative and `dir/` shaped — the same ownership {@link projectRootsAbs}
|
|
417
|
+
* applies to a session's cwd, expressed against the repository instead of a
|
|
418
|
+
* checkout: a `defaultPath` narrowed under `root` claims that subtree of the
|
|
419
|
+
* primary `repo`, and a `repos[]` entry with a `subpath` claims that subpath. A
|
|
420
|
+
* project with no narrowed claim on a repository claims all of it and yields no
|
|
421
|
+
* row for it. Two cases of {@link projectRootsAbs} have no repository form and
|
|
422
|
+
* yield nothing here: a `defaultPath` outside `root` (no repo-relative path),
|
|
423
|
+
* and the weak root of a narrowed project (it only takes what nobody claims,
|
|
424
|
+
* which for a PR is the `repo-wide` scope).
|
|
425
|
+
*/
|
|
426
|
+
export function repoPathClaims(def) {
|
|
427
|
+
const out = [];
|
|
428
|
+
const add = (slug, rel) => {
|
|
429
|
+
const trimmed = rel.split(path.sep).join('/').replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
430
|
+
if (trimmed && trimmed !== '.')
|
|
431
|
+
out.push({ slug, prefix: `${trimmed}/` });
|
|
432
|
+
};
|
|
433
|
+
if (def.repo && def.root && def.defaultPath) {
|
|
434
|
+
const rootAbs = path.resolve(expandLocalHome(def.root));
|
|
435
|
+
const defaultAbs = path.resolve(expandLocalHome(def.defaultPath));
|
|
436
|
+
if (isUnder(defaultAbs, rootAbs))
|
|
437
|
+
add(def.repo, path.relative(rootAbs, defaultAbs));
|
|
438
|
+
}
|
|
439
|
+
for (const r of def.repos ?? []) {
|
|
440
|
+
if (r.slug && r.subpath)
|
|
441
|
+
add(r.slug, r.subpath);
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
414
445
|
/** True when `child` is `parent` or nested under it (path-segment aware). */
|
|
415
446
|
function isUnder(child, parent) {
|
|
416
447
|
if (child === parent)
|
|
@@ -52,10 +52,10 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
|
|
|
52
52
|
* process table. `alive` receives the row's `startedAtMs` so callers can apply
|
|
53
53
|
* the same pid-reuse guard {@link isPidAlive} uses elsewhere.
|
|
54
54
|
*/
|
|
55
|
-
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
55
|
+
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean, ppidMap?: Map<number, number>): ActiveSession[];
|
|
56
56
|
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
57
57
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
58
|
-
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'firstUserMessage' | 'lastUserMessage' | 'generatedTitle' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount' | 'lastActivity'>;
|
|
58
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'accountId' | 'accountKey' | 'timestamp' | 'label' | 'firstUserMessage' | 'lastUserMessage' | 'generatedTitle' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount' | 'lastActivity'>;
|
|
59
59
|
/**
|
|
60
60
|
* Fold index-only enrichment onto live rows, then RE-DERIVE the recap for every
|
|
61
61
|
* row it touched. The re-derive is load-bearing: `foldRecap` runs inside
|
|
@@ -65,6 +65,8 @@ export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp'
|
|
|
65
65
|
* backfill call sites is what keeps them from drifting apart.
|
|
66
66
|
*/
|
|
67
67
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
68
|
+
/** Resolve a display name only when the indexed identity names one registered slot. */
|
|
69
|
+
export declare function sessionAccountLabel(kind: string, session: Pick<SessionMeta, 'accountId' | 'accountKey' | 'account'>, accounts?: import("../account-registry.js").NativeAccount[]): string | undefined;
|
|
68
70
|
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
69
71
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
70
72
|
export declare function activeSessionProjectKey(s: Pick<ActiveSession, 'cwd' | 'context'>): string;
|
|
@@ -319,6 +321,13 @@ export interface ActiveSession {
|
|
|
319
321
|
* `accountKey`.
|
|
320
322
|
*/
|
|
321
323
|
account?: string;
|
|
324
|
+
/**
|
|
325
|
+
* Human account-slot name (e.g. `gmail` in `claude#gmail`), display-only.
|
|
326
|
+
* Backfilled alongside {@link account} from indexed accountId/accountKey/email
|
|
327
|
+
* and the account registry. Unknown or ambiguous slots stay undefined.
|
|
328
|
+
* Never group on this label; use the index's `accountKey`.
|
|
329
|
+
*/
|
|
330
|
+
accountLabel?: string;
|
|
322
331
|
/**
|
|
323
332
|
* Last-activity epoch — the transcript's last write (mtime). Distinct from
|
|
324
333
|
* {@link startedAtMs} (session START): a session begun 3h ago but last touched
|
|
@@ -486,7 +495,7 @@ export interface ActiveSession {
|
|
|
486
495
|
};
|
|
487
496
|
/**
|
|
488
497
|
* The editor tab that launched this agent (`AGENT_TERMINAL_ID`), from the pid
|
|
489
|
-
* registry. This is the one identifier that survives an SSH hop AND a session
|
|
498
|
+
* registry or the published terminal entry. This is the one identifier that survives an SSH hop AND a session
|
|
490
499
|
* rotation: a Factory tab offloaded to a device has no local process to inspect,
|
|
491
500
|
* and its spawn-time session id goes stale the moment the agent moves to another
|
|
492
501
|
* session (`/clear`, exit-and-rerun), so `--active --device <device>` joined on
|
|
@@ -494,6 +503,8 @@ export interface ActiveSession {
|
|
|
494
503
|
* did not inherit a terminal id.
|
|
495
504
|
*/
|
|
496
505
|
terminalId?: string;
|
|
506
|
+
/** Published editor tab index, retained across agent restarts. */
|
|
507
|
+
tabIndex?: number;
|
|
497
508
|
/**
|
|
498
509
|
* The launch id (`AGENT_LAUNCH_ID`) the CLI stamps on every agent at spawn — a
|
|
499
510
|
* stable UUID that is identical locally and across an SSH hop and survives a
|
|
@@ -501,9 +512,8 @@ export interface ActiveSession {
|
|
|
501
512
|
* non-Claude harnesses only mint after boot) it exists from the first tick, so
|
|
502
513
|
* it is the join key a client uses to re-identify a session on the watch stream.
|
|
503
514
|
* Populated wherever the by-pid launch registry resolves — reliably for
|
|
504
|
-
* `agents run`-launched processes
|
|
505
|
-
*
|
|
506
|
-
* limitation the `readPidSessionEntry` call in `listTerminalsActive` documents).
|
|
515
|
+
* `agents run`-launched processes and editor terminals with a recorded live
|
|
516
|
+
* agent descendant.
|
|
507
517
|
*/
|
|
508
518
|
launchId?: string;
|
|
509
519
|
/**
|
|
@@ -750,6 +760,7 @@ interface ProcRow {
|
|
|
750
760
|
tty?: string;
|
|
751
761
|
comm: string;
|
|
752
762
|
kind?: string;
|
|
763
|
+
startTime?: string;
|
|
753
764
|
}
|
|
754
765
|
/** Parse `ConvertTo-Csv` output of Win32_Process rows. Exported for tests. */
|
|
755
766
|
export declare function parseWin32ProcessCsv(out: string): ProcRow[];
|
|
@@ -24,7 +24,9 @@ import { execFile, execFileSync } from 'child_process';
|
|
|
24
24
|
import { promisify } from 'util';
|
|
25
25
|
import { listActiveTasks } from '../cloud/store.js';
|
|
26
26
|
import { AgentManager } from '../teams/agents.js';
|
|
27
|
-
import { getTerminalsDir } from '../state.js';
|
|
27
|
+
import { getTerminalsDir, readMeta } from '../state.js';
|
|
28
|
+
import { listNativeAccounts } from '../account-registry.js';
|
|
29
|
+
import { parseNativeIdentityKey } from '../native-accounts.js';
|
|
28
30
|
import { readLivePidSessionEntry as readPidSessionEntry, listPidSessionEntries, prunePidSessionRegistry, sessionIdFromLivePid, isSessionIdShape, } from './pid-registry.js';
|
|
29
31
|
import { readSessionActorRecord, writeSessionAliasRecord } from './actor-sidecar.js';
|
|
30
32
|
import { loadHookSessionIndex, resolveHookSessionRecord, readStateSessionRecord } from './hook-sessions.js';
|
|
@@ -129,11 +131,11 @@ export function attributedSetLostPids(prev, next) {
|
|
|
129
131
|
* process table. `alive` receives the row's `startedAtMs` so callers can apply
|
|
130
132
|
* the same pid-reuse guard {@link isPidAlive} uses elsewhere.
|
|
131
133
|
*/
|
|
132
|
-
export function filterCachedUnattributed(sessions, attributed, alive) {
|
|
134
|
+
export function filterCachedUnattributed(sessions, attributed, alive, ppidMap) {
|
|
133
135
|
return sessions.filter((s) => {
|
|
134
136
|
if (s.pid == null)
|
|
135
137
|
return false;
|
|
136
|
-
if (attributed.has(s.pid))
|
|
138
|
+
if (attributed.has(s.pid) || (ppidMap && hasAttributedAncestor(s.pid, ppidMap, attributed)))
|
|
137
139
|
return false;
|
|
138
140
|
return alive(s.pid, s.startedAtMs);
|
|
139
141
|
});
|
|
@@ -147,6 +149,7 @@ export function filterCachedUnattributed(sessions, attributed, alive) {
|
|
|
147
149
|
* backfill call sites is what keeps them from drifting apart.
|
|
148
150
|
*/
|
|
149
151
|
export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
152
|
+
const accounts = listNativeAccounts(readMeta());
|
|
150
153
|
for (const s of sessions) {
|
|
151
154
|
if (!s.sessionId)
|
|
152
155
|
continue;
|
|
@@ -157,6 +160,7 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
157
160
|
s.version = m.version;
|
|
158
161
|
if (!s.account && m.account)
|
|
159
162
|
s.account = m.account;
|
|
163
|
+
s.accountLabel ??= sessionAccountLabel(s.kind, m, accounts);
|
|
160
164
|
if (!s.label && m.label)
|
|
161
165
|
s.label = m.label;
|
|
162
166
|
if (!s.firstUserMessage && m.firstUserMessage)
|
|
@@ -196,6 +200,30 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
196
200
|
applyRecap(s);
|
|
197
201
|
}
|
|
198
202
|
}
|
|
203
|
+
/** Resolve a display name only when the indexed identity names one registered slot. */
|
|
204
|
+
export function sessionAccountLabel(kind, session, accounts = listNativeAccounts(readMeta())) {
|
|
205
|
+
const matches = accounts.filter(account => {
|
|
206
|
+
if (account.agent !== kind)
|
|
207
|
+
return false;
|
|
208
|
+
if (session.accountId)
|
|
209
|
+
return account.id === session.accountId;
|
|
210
|
+
if (session.accountKey && !session.accountKey.startsWith('unattributed:')) {
|
|
211
|
+
// Claude's index groups quota by org; the registry names a login within it.
|
|
212
|
+
const indexed = parseNativeIdentityKey(account.agent, session.accountKey);
|
|
213
|
+
const emailMatches = !!session.account && account.identityLabel?.toLowerCase() === session.account.toLowerCase();
|
|
214
|
+
if (!indexed || (!indexed.account && !emailMatches))
|
|
215
|
+
return false;
|
|
216
|
+
if (account.identityKey === session.accountKey)
|
|
217
|
+
return true;
|
|
218
|
+
const registered = parseNativeIdentityKey(account.agent, account.identityKey);
|
|
219
|
+
return !!registered
|
|
220
|
+
&& Object.entries(indexed).every(([key, value]) => registered[key] === value)
|
|
221
|
+
&& (!session.account || emailMatches);
|
|
222
|
+
}
|
|
223
|
+
return !!session.account && account.identityLabel?.toLowerCase() === session.account.toLowerCase();
|
|
224
|
+
});
|
|
225
|
+
return matches.length === 1 ? matches[0].name : undefined;
|
|
226
|
+
}
|
|
199
227
|
function loadBackfillMetaFor(sessions) {
|
|
200
228
|
const byId = new Map();
|
|
201
229
|
try {
|
|
@@ -1062,49 +1090,48 @@ export async function listTerminalsActive() {
|
|
|
1062
1090
|
const procByPid = new Map();
|
|
1063
1091
|
for (const r of await readProcessTable())
|
|
1064
1092
|
procByPid.set(r.pid, r);
|
|
1093
|
+
const children = childrenByParent(new Map([...procByPid.values()].map(r => [r.pid, r.ppid])));
|
|
1065
1094
|
// Build label map from Claude's sessions/*.json for /rename support
|
|
1066
1095
|
const labelMap = buildClaudeLabelMap();
|
|
1067
1096
|
// Run-name handles (`agents run --name`) keyed by session id, for the same
|
|
1068
1097
|
// sessionId → handle resolution as labels.
|
|
1069
1098
|
const runNameMap = buildRunNameMap();
|
|
1070
1099
|
return entries.map((t) => {
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
// back to the stale cached id — the duplicate-card fix comes from
|
|
1078
|
-
// pickSessionFile no longer borrowing a sibling, not from this lookup. Kept as
|
|
1079
|
-
// a forward-looking hook for the cases where the pid does line up.
|
|
1080
|
-
const pidEntry = readPidSessionEntry(t.pid);
|
|
1100
|
+
// Keep the shell pid for tab ownership and headless suppression; the live
|
|
1101
|
+
// agent below it owns the session after an exit-and-rerun in the same tab.
|
|
1102
|
+
const directEntry = readPidSessionEntry(t.pid, procByPid.get(t.pid)?.startTime);
|
|
1103
|
+
const candidate = directEntry?.sessionId ? directEntry
|
|
1104
|
+
: (!t.pidDead ? terminalDescendantEntry(t.pid, procByPid, children) : undefined) ?? directEntry;
|
|
1105
|
+
const pidEntry = candidate && (!isSessionTrackedAgent(t.kind) || candidate.agent === t.kind) ? candidate : undefined;
|
|
1081
1106
|
const resolvedId = pidEntry?.sessionId ?? t.sessionId;
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1107
|
+
const cwd = pidEntry?.cwd ?? t.cwd ?? undefined;
|
|
1108
|
+
const sessionKind = pidEntry?.agent ?? t.kind;
|
|
1109
|
+
const sessionFile = findSessionFileForKind(sessionKind, cwd, resolvedId);
|
|
1110
|
+
const label = t.label ?? (resolvedId ? labelMap.get(resolvedId) : undefined) ?? undefined;
|
|
1085
1111
|
// Durable run name from `agents run --name`, resolved by the run's session id.
|
|
1086
1112
|
const name = resolvedId ? runNameMap.get(resolvedId) ?? undefined : undefined;
|
|
1087
1113
|
// Extract topic from session file (first meaningful user message)
|
|
1088
1114
|
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
1089
1115
|
const pidAlive = isPidAlive(t.pid, t.startedAtMs);
|
|
1090
|
-
const { state, tokPerSec } = computeLiveSignals(
|
|
1116
|
+
const { state, tokPerSec } = computeLiveSignals(sessionKind, sessionFile, cwd, pidAlive);
|
|
1091
1117
|
return applyState({
|
|
1092
1118
|
context: 'terminal',
|
|
1093
|
-
kind:
|
|
1119
|
+
kind: sessionKind,
|
|
1094
1120
|
harness: pidEntry?.harness,
|
|
1095
1121
|
host: detectHost(t.pid, procByPid),
|
|
1096
1122
|
tty: procByPid.get(t.pid)?.tty,
|
|
1097
1123
|
pid: t.pid,
|
|
1098
|
-
sessionId:
|
|
1124
|
+
sessionId: resolvedId ?? sessionIdFromFile(sessionFile),
|
|
1099
1125
|
launchId: pidEntry?.launchId,
|
|
1100
|
-
terminalId: pidEntry?.terminalId,
|
|
1101
|
-
|
|
1126
|
+
terminalId: pidEntry?.terminalId ?? t.terminalId,
|
|
1127
|
+
tabIndex: t.tabIndex,
|
|
1128
|
+
cwd,
|
|
1102
1129
|
label,
|
|
1103
1130
|
name,
|
|
1104
1131
|
topic,
|
|
1105
1132
|
tokPerSec,
|
|
1106
1133
|
sessionFile,
|
|
1107
|
-
startedAtMs: t.startedAtMs,
|
|
1134
|
+
startedAtMs: pidEntry?.startedAtMs ?? t.startedAtMs,
|
|
1108
1135
|
lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
|
|
1109
1136
|
windowId: t.windowId,
|
|
1110
1137
|
windowHeartbeatMs: t.windowHeartbeatMs,
|
|
@@ -1183,16 +1210,16 @@ async function readProcessTableLive() {
|
|
|
1183
1210
|
return readProcessTableWin32();
|
|
1184
1211
|
let out;
|
|
1185
1212
|
try {
|
|
1186
|
-
({ stdout: out } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,tty=,comm='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: PS_SNAPSHOT_TIMEOUT_MS }));
|
|
1213
|
+
({ stdout: out } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,tty=,lstart=,comm='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: PS_SNAPSHOT_TIMEOUT_MS }));
|
|
1187
1214
|
}
|
|
1188
1215
|
catch {
|
|
1189
1216
|
return [];
|
|
1190
1217
|
}
|
|
1191
1218
|
const rows = [];
|
|
1192
1219
|
for (const line of out.split('\n')) {
|
|
1193
|
-
// pid ppid tty comm — tty is a single token ('ttys003', 's003', or '??'/'?'
|
|
1220
|
+
// pid ppid tty lstart comm — tty is a single token ('ttys003', 's003', or '??'/'?'
|
|
1194
1221
|
// for none); comm stays last so it may contain spaces.
|
|
1195
|
-
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
1222
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.+)$/);
|
|
1196
1223
|
if (!m)
|
|
1197
1224
|
continue;
|
|
1198
1225
|
const pid = parseInt(m[1], 10);
|
|
@@ -1201,8 +1228,8 @@ async function readProcessTableLive() {
|
|
|
1201
1228
|
continue;
|
|
1202
1229
|
const ttyRaw = m[3];
|
|
1203
1230
|
const tty = ttyRaw === '??' || ttyRaw === '?' || ttyRaw === '-' ? undefined : ttyRaw;
|
|
1204
|
-
const commRaw = m[
|
|
1205
|
-
rows.push({ pid, ppid, tty, comm: commRaw, kind: agentKindFromComm(commRaw) });
|
|
1231
|
+
const commRaw = m[5].trim();
|
|
1232
|
+
rows.push({ pid, ppid, tty, startTime: m[4], comm: commRaw, kind: agentKindFromComm(commRaw) });
|
|
1206
1233
|
}
|
|
1207
1234
|
return rows;
|
|
1208
1235
|
}
|
|
@@ -1240,6 +1267,50 @@ export function parseWin32ProcessCsv(out) {
|
|
|
1240
1267
|
}
|
|
1241
1268
|
return rows;
|
|
1242
1269
|
}
|
|
1270
|
+
function childrenByParent(ppidMap) {
|
|
1271
|
+
const children = new Map();
|
|
1272
|
+
for (const [pid, ppid] of ppidMap) {
|
|
1273
|
+
const siblings = children.get(ppid);
|
|
1274
|
+
if (siblings)
|
|
1275
|
+
siblings.push(pid);
|
|
1276
|
+
else
|
|
1277
|
+
children.set(ppid, [pid]);
|
|
1278
|
+
}
|
|
1279
|
+
return children;
|
|
1280
|
+
}
|
|
1281
|
+
function terminalDescendantEntry(pid, processes, children) {
|
|
1282
|
+
// Windows only proves pidExists: a recycled PID can be a live shell descendant.
|
|
1283
|
+
// Retain the published identity until that platform can prove process starts.
|
|
1284
|
+
if (process.platform === 'win32')
|
|
1285
|
+
return undefined;
|
|
1286
|
+
let pending = children.get(pid) ?? [];
|
|
1287
|
+
const seen = new Set([pid]);
|
|
1288
|
+
while (pending.length) {
|
|
1289
|
+
const next = [];
|
|
1290
|
+
let recordedAgent = false;
|
|
1291
|
+
let newest;
|
|
1292
|
+
for (const child of pending) {
|
|
1293
|
+
if (seen.has(child))
|
|
1294
|
+
continue;
|
|
1295
|
+
seen.add(child);
|
|
1296
|
+
const entry = readPidSessionEntry(child, processes.get(child)?.startTime);
|
|
1297
|
+
if (!entry || !isSessionTrackedAgent(entry.agent)) {
|
|
1298
|
+
next.push(...(children.get(child) ?? []));
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
// A recorded agent is a boundary even before its session ID is known:
|
|
1302
|
+
// its tools may launch agents of their own, which never own this tab.
|
|
1303
|
+
recordedAgent = true;
|
|
1304
|
+
if (entry.sessionId && (!newest || entry.startedAtMs > newest.startedAtMs
|
|
1305
|
+
|| (entry.startedAtMs === newest.startedAtMs && entry.pid > newest.pid)))
|
|
1306
|
+
newest = entry;
|
|
1307
|
+
}
|
|
1308
|
+
if (recordedAgent)
|
|
1309
|
+
return newest;
|
|
1310
|
+
pending = next;
|
|
1311
|
+
}
|
|
1312
|
+
return undefined;
|
|
1313
|
+
}
|
|
1243
1314
|
/**
|
|
1244
1315
|
* True when any ancestor in pid's parent chain is a known attributed PID.
|
|
1245
1316
|
* VS Code / Cursor terminals store the *shell* PID in live-terminals.json,
|
|
@@ -1456,11 +1527,11 @@ export async function listUnattributedActive(attributed) {
|
|
|
1456
1527
|
if (unattributedCache &&
|
|
1457
1528
|
now - unattributedCache.at < UNATTRIBUTED_RESCAN_MS &&
|
|
1458
1529
|
!attributedSetLostPids(unattributedCache.attributed, attributed)) {
|
|
1459
|
-
return filterCachedUnattributed(unattributedCache.sessions, attributed, isPidAlive);
|
|
1530
|
+
return filterCachedUnattributed(unattributedCache.sessions, attributed, isPidAlive, unattributedCache.ppidMap);
|
|
1460
1531
|
}
|
|
1461
|
-
const
|
|
1462
|
-
unattributedCache = { at: now, attributed: new Set(attributed),
|
|
1463
|
-
return
|
|
1532
|
+
const result = await listUnattributedActiveLive(attributed);
|
|
1533
|
+
unattributedCache = { at: now, attributed: new Set(attributed), ...result };
|
|
1534
|
+
return result.sessions;
|
|
1464
1535
|
}
|
|
1465
1536
|
/** Unthrottled headless scan (live process table + cwd probes). */
|
|
1466
1537
|
async function listUnattributedActiveLive(attributed) {
|
|
@@ -1495,28 +1566,13 @@ async function listUnattributedActiveLive(attributed) {
|
|
|
1495
1566
|
// lacks an exact launch-time id, so an all-Claude set does neither. The ~3s
|
|
1496
1567
|
// poll must not re-read the dir (or re-invert the map) per candidate.
|
|
1497
1568
|
let hookIndex;
|
|
1498
|
-
let
|
|
1569
|
+
let children;
|
|
1499
1570
|
// Durable `agents run --name` handles keyed by session id — the same source the
|
|
1500
1571
|
// terminal path uses to name a row. Headless agents have no live-terminals
|
|
1501
1572
|
// label and no /rename, so without this a `--name`d headless run would surface
|
|
1502
1573
|
// with only a topic and no tab title. Built once per scan.
|
|
1503
1574
|
const runNameMap = buildRunNameMap();
|
|
1504
|
-
const ensureChildren = () =>
|
|
1505
|
-
if (childrenByParent)
|
|
1506
|
-
return childrenByParent;
|
|
1507
|
-
const m = new Map();
|
|
1508
|
-
// The hook records under the agent pid; a wrapper/shell pid we recorded has
|
|
1509
|
-
// the agent as a child, so we resolve via a recorded pid's immediate children.
|
|
1510
|
-
for (const [childPid, parentPid] of ppidMap) {
|
|
1511
|
-
const arr = m.get(parentPid);
|
|
1512
|
-
if (arr)
|
|
1513
|
-
arr.push(childPid);
|
|
1514
|
-
else
|
|
1515
|
-
m.set(parentPid, [childPid]);
|
|
1516
|
-
}
|
|
1517
|
-
childrenByParent = m;
|
|
1518
|
-
return m;
|
|
1519
|
-
};
|
|
1575
|
+
const ensureChildren = () => children ??= childrenByParent(ppidMap);
|
|
1520
1576
|
const out = [];
|
|
1521
1577
|
for (let i = 0; i < kept.length; i++) {
|
|
1522
1578
|
const { pid, kind } = kept[i];
|
|
@@ -1603,7 +1659,7 @@ async function listUnattributedActiveLive(attributed) {
|
|
|
1603
1659
|
}
|
|
1604
1660
|
// Housekeeping: drop registry files for pids that have since died.
|
|
1605
1661
|
prunePidSessionRegistry(isPidAlive);
|
|
1606
|
-
return out;
|
|
1662
|
+
return { sessions: out, ppidMap };
|
|
1607
1663
|
}
|
|
1608
1664
|
/**
|
|
1609
1665
|
* Attribute a single tmux pane to the agent actually running in it.
|
|
@@ -58,8 +58,10 @@ export interface PidSessionEntry {
|
|
|
58
58
|
startTime?: string;
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
/** ps lstart fields can differ in padding or date formatting between invocations. */
|
|
62
|
+
export declare function processStartTimesMatch(recorded: string | undefined, observed: string | undefined): boolean | undefined;
|
|
61
63
|
/** Read-only identity proof for exact joins. Unknown is never a positive match. */
|
|
62
|
-
export declare function pidSessionEntryMatchesLiveProcess(entry: PidSessionEntry): boolean | undefined;
|
|
64
|
+
export declare function pidSessionEntryMatchesLiveProcess(entry: PidSessionEntry, startTime?: string): boolean | undefined;
|
|
63
65
|
export declare function isSessionIdShape(value: string): boolean;
|
|
64
66
|
export declare function extractSessionIdArg(args: string[]): string | undefined;
|
|
65
67
|
/**
|
|
@@ -86,8 +88,9 @@ export declare function sessionIdFromLivePid(pid: number): string | undefined;
|
|
|
86
88
|
export declare function writePidSessionEntry(entry: PidSessionEntry): void;
|
|
87
89
|
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
88
90
|
export declare function readPidSessionEntry(pid: number): PidSessionEntry | undefined;
|
|
89
|
-
/** Live consumers require identity proof; raw history is not a PID binding.
|
|
90
|
-
|
|
91
|
+
/** Live consumers require identity proof; raw history is not a PID binding.
|
|
92
|
+
* A shared process snapshot may supply lstart to avoid a per-pid ps probe. */
|
|
93
|
+
export declare function readLivePidSessionEntry(pid: number, startTime?: string): PidSessionEntry | undefined;
|
|
91
94
|
/**
|
|
92
95
|
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
93
96
|
* by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
|
|
@@ -55,16 +55,25 @@ function darwinStartTime(pid) {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function processStartTimeMs(value) {
|
|
59
|
+
return typeof value === 'string' ? Date.parse(value.trim().replace(/\s+/g, ' ')) : NaN;
|
|
60
|
+
}
|
|
61
|
+
/** ps lstart fields can differ in padding or date formatting between invocations. */
|
|
62
|
+
export function processStartTimesMatch(recorded, observed) {
|
|
63
|
+
const recordedMs = processStartTimeMs(recorded);
|
|
64
|
+
const observedMs = processStartTimeMs(observed);
|
|
65
|
+
return Number.isFinite(recordedMs) && Number.isFinite(observedMs) ? recordedMs === observedMs : undefined;
|
|
66
|
+
}
|
|
58
67
|
/** Read-only identity proof for exact joins. Unknown is never a positive match. */
|
|
59
|
-
export function pidSessionEntryMatchesLiveProcess(entry) {
|
|
68
|
+
export function pidSessionEntryMatchesLiveProcess(entry, startTime) {
|
|
60
69
|
if (!Number.isInteger(entry.pid) || entry.pid < 1)
|
|
61
70
|
return undefined;
|
|
62
71
|
if (process.platform === 'darwin') {
|
|
63
72
|
const exists = pidExists(entry.pid);
|
|
64
73
|
if (exists !== true)
|
|
65
74
|
return exists;
|
|
66
|
-
const start = darwinStartTime(entry.pid);
|
|
67
|
-
return
|
|
75
|
+
const start = startTime ?? darwinStartTime(entry.pid);
|
|
76
|
+
return processStartTimesMatch(entry.processIdentity?.startTime, start);
|
|
68
77
|
}
|
|
69
78
|
if (process.platform !== 'linux')
|
|
70
79
|
return undefined;
|
|
@@ -233,8 +242,9 @@ export function readPidSessionEntry(pid) {
|
|
|
233
242
|
}
|
|
234
243
|
return undefined;
|
|
235
244
|
}
|
|
236
|
-
/** Live consumers require identity proof; raw history is not a PID binding.
|
|
237
|
-
|
|
245
|
+
/** Live consumers require identity proof; raw history is not a PID binding.
|
|
246
|
+
* A shared process snapshot may supply lstart to avoid a per-pid ps probe. */
|
|
247
|
+
export function readLivePidSessionEntry(pid, startTime) {
|
|
238
248
|
const entry = readPidSessionEntry(pid);
|
|
239
249
|
if (!entry || !hostProcessView())
|
|
240
250
|
return undefined;
|
|
@@ -244,8 +254,8 @@ export function readLivePidSessionEntry(pid) {
|
|
|
244
254
|
// Upgrade a native legacy launch only after the home namespace is proven,
|
|
245
255
|
// and the actual process start excludes a recycled PID. Hook timestamps may
|
|
246
256
|
// be later than process start, but cannot predate this process incarnation.
|
|
247
|
-
const start = darwinStartTime(pid);
|
|
248
|
-
const startMs =
|
|
257
|
+
const start = startTime ?? darwinStartTime(pid);
|
|
258
|
+
const startMs = processStartTimeMs(start);
|
|
249
259
|
if (Number.isFinite(startMs) && entry.startedAtMs >= startMs - 1000 && entry.startedAtMs <= Date.now()) {
|
|
250
260
|
const scope = hostProcessView();
|
|
251
261
|
entry.processIdentity = process.platform === 'linux'
|
|
@@ -257,7 +267,7 @@ export function readLivePidSessionEntry(pid) {
|
|
|
257
267
|
catch { /* read remains useful */ }
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
|
-
return pidSessionEntryMatchesLiveProcess(entry) === true ? entry : undefined;
|
|
270
|
+
return pidSessionEntryMatchesLiveProcess(entry, startTime) === true ? entry : undefined;
|
|
261
271
|
}
|
|
262
272
|
/**
|
|
263
273
|
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
@@ -88,7 +88,7 @@ export declare function sessionWatchRowKey(scope: string, row: ActiveSession): s
|
|
|
88
88
|
export declare function toSessionWatchRow(scope: string, row: ActiveSession): SessionWatchRow;
|
|
89
89
|
/** Project one durable indexed session into the same canonical watch contract
|
|
90
90
|
* as live sessions. This is the only history backfill consumed by AGI EXT. */
|
|
91
|
-
export declare function toPreviousSessionWatchRow(scope: string, session: SessionMeta): SessionWatchRow;
|
|
91
|
+
export declare function toPreviousSessionWatchRow(scope: string, session: SessionMeta, accounts?: import("../../account-registry.js").NativeAccount[]): SessionWatchRow;
|
|
92
92
|
/** Stream-local sequencer and convergent row diff. */
|
|
93
93
|
export declare class SessionWatchState {
|
|
94
94
|
readonly streamId: string;
|
|
@@ -8,7 +8,9 @@ import { machineId, normalizeHost } from '../../machine-id.js';
|
|
|
8
8
|
import { shellQuote } from '../../ssh-exec.js';
|
|
9
9
|
import { streamFromPeer } from './peer-stream.js';
|
|
10
10
|
import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
|
|
11
|
-
import { isReapableOrphan } from '../active.js';
|
|
11
|
+
import { isReapableOrphan, sessionAccountLabel } from '../active.js';
|
|
12
|
+
import { listNativeAccounts } from '../../account-registry.js';
|
|
13
|
+
import { readMeta } from '../../state.js';
|
|
12
14
|
import { querySessions, readSessionSummaryAny, readSessionTimelineAny } from '../db.js';
|
|
13
15
|
import { linearIssueUrl } from '../linear.js';
|
|
14
16
|
import { sessionAgentSupportsResume } from '../recovery.js';
|
|
@@ -79,7 +81,7 @@ function previousRowTitle(session, foldedHeadline) {
|
|
|
79
81
|
}
|
|
80
82
|
/** Project one durable indexed session into the same canonical watch contract
|
|
81
83
|
* as live sessions. This is the only history backfill consumed by AGI EXT. */
|
|
82
|
-
export function toPreviousSessionWatchRow(scope, session) {
|
|
84
|
+
export function toPreviousSessionWatchRow(scope, session, accounts = listNativeAccounts(readMeta())) {
|
|
83
85
|
const resumable = sessionAgentSupportsResume(session.agent);
|
|
84
86
|
const sourceDevice = normalizeHost(session.machine ?? scope);
|
|
85
87
|
const startedAtMs = epochMs(session.timestamp);
|
|
@@ -125,6 +127,7 @@ export function toPreviousSessionWatchRow(scope, session) {
|
|
|
125
127
|
...(session.firstUserMessage ? { firstUserMessage: session.firstUserMessage } : {}),
|
|
126
128
|
...(session.version ? { version: session.version } : {}),
|
|
127
129
|
...(session.account ? { account: session.account } : {}),
|
|
130
|
+
accountLabel: sessionAccountLabel(session.agent, session, accounts),
|
|
128
131
|
...(session.prUrl ? { pr: { url: session.prUrl, number: session.prNumber } } : {}),
|
|
129
132
|
...(worktree ? { worktree } : {}),
|
|
130
133
|
...(session.gitBranch ? { branch: session.gitBranch } : {}),
|
|
@@ -226,10 +229,11 @@ export class SessionWatchState {
|
|
|
226
229
|
const liveRows = sourceRows.map((row) => toSessionWatchRow(scope, row));
|
|
227
230
|
const sourceIds = new Set(liveRows.map((row) => row.sessionId).filter((id) => Boolean(id)));
|
|
228
231
|
const rows = new Map(liveRows.map((row) => [row.rowKey, row]));
|
|
232
|
+
const accounts = indexedRows.length ? listNativeAccounts(readMeta()) : [];
|
|
229
233
|
for (const indexed of indexedRows) {
|
|
230
234
|
if (sourceIds.has(indexed.id))
|
|
231
235
|
continue;
|
|
232
|
-
const row = toPreviousSessionWatchRow(scope, indexed);
|
|
236
|
+
const row = toPreviousSessionWatchRow(scope, indexed, accounts);
|
|
233
237
|
rows.set(row.rowKey, row);
|
|
234
238
|
}
|
|
235
239
|
this.prunePrevious(scope, rows);
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -199,6 +199,12 @@ export interface AgentConfig {
|
|
|
199
199
|
* without this flag only see plugin skills through the flattened copies.
|
|
200
200
|
*/
|
|
201
201
|
nativePluginSkills?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Directories under the version home's `skills/` that the harness itself
|
|
204
|
+
* writes (Claude Code's `synced` bucket). The sync never placed them, so the
|
|
205
|
+
* full-sync orphan sweep and skill cleanup must never remove them.
|
|
206
|
+
*/
|
|
207
|
+
ownedSkillDirs?: readonly string[];
|
|
202
208
|
/**
|
|
203
209
|
* This agent's *own* cloud backend. `agents cloud run --agent <id>` routes
|
|
204
210
|
* here when no `--provider` is given (precedence: --provider > this >
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.119",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|