@phnx-labs/agents-cli 1.20.78 → 1.20.83
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 +595 -0
- package/README.md +26 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +66 -0
- package/dist/commands/doctor.js +596 -33
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +241 -77
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +115 -0
- package/dist/commands/sessions.js +189 -35
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +52 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +69 -11
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +71 -0
- package/dist/lib/hooks.js +245 -95
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +33 -0
- package/dist/lib/routines.js +122 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/scheduler.js +16 -4
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +128 -26
- package/dist/lib/session/active.js +192 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/state.js +57 -1
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
|
@@ -25,19 +25,33 @@ import { listActiveTasks } from '../cloud/store.js';
|
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
26
|
import { getTerminalsDir } from '../state.js';
|
|
27
27
|
import { readPidSessionEntry, listPidSessionEntries, prunePidSessionRegistry } from './pid-registry.js';
|
|
28
|
-
import {
|
|
28
|
+
import { readSessionActorRecord } from './actor-sidecar.js';
|
|
29
|
+
import { loadHookSessionIndex, resolveHookSessionRecord, readStateSessionRecord } from './hook-sessions.js';
|
|
29
30
|
import { buildClaudeLabelMap, getAgentSessionDirs } from './discover.js';
|
|
30
31
|
import { buildRunNameMap } from './run-names.js';
|
|
31
32
|
import { latestSessionFileForCwd } from './db.js';
|
|
32
33
|
import { extractSessionTopic } from './prompt.js';
|
|
33
34
|
import { readSessionTailWithRaw } from './tail.js';
|
|
35
|
+
import { parseSession } from './parse.js';
|
|
34
36
|
import { computeTokPerSec } from './throughput.js';
|
|
35
37
|
import { inferSessionState } from './state.js';
|
|
38
|
+
import { isSessionTrackedAgent } from './types.js';
|
|
36
39
|
import { detectProvenance } from './provenance.js';
|
|
37
40
|
import { loadDevices } from '../devices/registry.js';
|
|
38
41
|
import { presenceFromStore } from './detached.js';
|
|
39
42
|
import { mapBounded } from '../concurrency.js';
|
|
40
43
|
const execFileAsync = promisify(execFile);
|
|
44
|
+
/**
|
|
45
|
+
* The owner (actor id) to show for a session in `--active`. Prefers the actor
|
|
46
|
+
* recorded on the live-attribution source (the pid registry / teammate record),
|
|
47
|
+
* but falls back to the durable per-session actor sidecar — written at spawn and,
|
|
48
|
+
* unlike the pid entry, NOT overwritten by the SessionStart hook's own by-pid
|
|
49
|
+
* write. Without this fallback a real `agents run` shows no owner whenever the
|
|
50
|
+
* hook's actor-less entry wins the by-pid file (RUSH-2018 fix).
|
|
51
|
+
*/
|
|
52
|
+
export function resolveOwner(pidActor, sessionId) {
|
|
53
|
+
return pidActor ?? (sessionId ? readSessionActorRecord(sessionId)?.actor : undefined) ?? undefined;
|
|
54
|
+
}
|
|
41
55
|
/**
|
|
42
56
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
43
57
|
* fan-out: a simultaneous system-wide `lsof` burst reads to behavioral EDR
|
|
@@ -82,6 +96,16 @@ const ACTIVE_MTIME_WINDOW_MS = 2 * 60_000;
|
|
|
82
96
|
* transcript just because a GUI app service with the same basename is alive.
|
|
83
97
|
*/
|
|
84
98
|
export const ACTIVE_SESSION_STALE_MS = 24 * 60 * 60_000;
|
|
99
|
+
/**
|
|
100
|
+
* A session whose transcript hasn't been written in this long is ABANDONED /
|
|
101
|
+
* dangling — the framework can no longer treat it as live work, whether its PID
|
|
102
|
+
* is dead (long gone) or still alive but making no progress (a hung agent).
|
|
103
|
+
* Two days: past a normal work gap, well short of a session legitimately kept
|
|
104
|
+
* open across a long weekend. Deliberately far wider than
|
|
105
|
+
* {@link ACTIVE_SESSION_STALE_MS} (which bounds transcript-borrowing, a different
|
|
106
|
+
* concern) — this is the lifecycle threshold, not the freshness window.
|
|
107
|
+
*/
|
|
108
|
+
export const ABANDONED_STALE_MS = 2 * 24 * 60 * 60_000;
|
|
85
109
|
/** Executables we recognize as agent CLIs when scanning the process table. */
|
|
86
110
|
const AGENT_CLI_NAMES = {
|
|
87
111
|
claude: 'claude',
|
|
@@ -309,46 +333,68 @@ export function sessionFileTimes(sessionFile) {
|
|
|
309
333
|
return {};
|
|
310
334
|
}
|
|
311
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* The lifecycle status computed purely from the two hard signals the framework
|
|
338
|
+
* always has — PID liveness and transcript last-write (mtime) — independent of
|
|
339
|
+
* anything the agent self-reports or what its last transcript turn happened to
|
|
340
|
+
* look like. Returns the definitive lifecycle label when one applies, or
|
|
341
|
+
* `undefined` when the process is live and fresh (so the caller uses the richer
|
|
342
|
+
* activity-derived status instead).
|
|
343
|
+
*
|
|
344
|
+
* - No write in {@link ABANDONED_STALE_MS} ⇒ `abandoned` (dangling): a
|
|
345
|
+
* days-stale session is not live work, whether its PID is dead (long gone)
|
|
346
|
+
* or still alive but stuck. Checked first — it outranks a bare `closed`.
|
|
347
|
+
* - Otherwise, PID dead ⇒ `closed`: the process has exited. This is the fix
|
|
348
|
+
* for the old "dead PID reports idle" lie — `idle` reads as "done, waiting
|
|
349
|
+
* for you", but a dead process is done, period.
|
|
350
|
+
* - Otherwise (alive + fresh) ⇒ `undefined`: defer to the activity engine.
|
|
351
|
+
*/
|
|
352
|
+
export function lifecycleStatus(pidAlive, mtimeMs, nowMs = Date.now()) {
|
|
353
|
+
if (mtimeMs !== undefined && nowMs - mtimeMs >= ABANDONED_STALE_MS)
|
|
354
|
+
return 'abandoned';
|
|
355
|
+
if (!pidAlive)
|
|
356
|
+
return 'closed';
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
312
359
|
/**
|
|
313
360
|
* The ONE place a fallback status is decided when no rich transcript state is
|
|
314
|
-
* available —
|
|
315
|
-
*
|
|
316
|
-
*
|
|
361
|
+
* available — an opaque kind we cannot parse (cursor, openclaw), or a transcript
|
|
362
|
+
* whose parse/tail was empty or unreadable. Honest by construction: computed from
|
|
363
|
+
* PID + mtime, never a fabricated `idle`.
|
|
317
364
|
*
|
|
318
|
-
* -
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* UI reads as "done and waiting"), and it never lies as `running` either.
|
|
327
|
-
* - No transcript and the process is not known alive → nothing to report ⇒ `idle`.
|
|
365
|
+
* - Dead PID, or a days-stale transcript ⇒ {@link lifecycleStatus} (`closed` /
|
|
366
|
+
* `abandoned`). A dead process is `closed`, not the old fabricated `idle`
|
|
367
|
+
* (which the UI reads as "done, waiting for you"); a dead process whose file
|
|
368
|
+
* also vanished is still `closed` (death is a definitive answer, not `unknown`).
|
|
369
|
+
* - Live + fresh ⇒ `running`. A live process is, at minimum, running: we may
|
|
370
|
+
* not see WHAT an opaque harness (or an empty tail) is doing, but the process
|
|
371
|
+
* being alive is a positive signal — never a blank `unknown` (the old blanket
|
|
372
|
+
* bug for every non-claude/codex live agent) nor a downgraded `idle`.
|
|
328
373
|
*/
|
|
329
|
-
export function resolveFallbackStatus(sessionFile, pidAlive) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
334
|
-
return Date.now() - mtimeMs < ACTIVE_MTIME_WINDOW_MS ? 'running' : 'idle';
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
return 'unknown';
|
|
338
|
-
}
|
|
374
|
+
export function resolveFallbackStatus(sessionFile, pidAlive, nowMs = Date.now()) {
|
|
375
|
+
const { mtimeMs } = sessionFileTimes(sessionFile);
|
|
376
|
+
return lifecycleStatus(pidAlive, mtimeMs, nowMs) ?? 'running';
|
|
339
377
|
}
|
|
340
378
|
/**
|
|
341
|
-
* Locate the live transcript for an agent process. Claude files are keyed by
|
|
342
|
-
*
|
|
343
|
-
*
|
|
379
|
+
* Locate the live transcript for an agent process. Claude files are keyed by cwd
|
|
380
|
+
* (+ optional session uuid), so they resolve straight off disk. Every OTHER
|
|
381
|
+
* tracked harness — Codex (date-partitioned), plus grok / droid / rush / gemini /
|
|
382
|
+
* kimi / hermes / opencode / antigravity (per-session dirs, SQLite, single-JSON)
|
|
383
|
+
* — is resolved through the session index by cwd: the newest indexed transcript
|
|
384
|
+
* for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never borrows a
|
|
385
|
+
* weeks-old transcript. This is what lets a live NON-claude/codex agent get a real
|
|
386
|
+
* status instead of falling through to `unknown` (the file feeds
|
|
387
|
+
* {@link computeLiveSignals}). An opaque kind we don't track (cursor) still yields
|
|
388
|
+
* undefined here and degrades honestly to a live `running`.
|
|
344
389
|
*/
|
|
345
390
|
export function findSessionFileForKind(kind, cwd, sessionId) {
|
|
346
391
|
if (!cwd)
|
|
347
392
|
return undefined;
|
|
348
393
|
if (kind === 'claude')
|
|
349
394
|
return findClaudeSessionFile(cwd, sessionId);
|
|
350
|
-
if (kind
|
|
351
|
-
return latestSessionFileForCwd(
|
|
395
|
+
if (isSessionTrackedAgent(kind)) {
|
|
396
|
+
return latestSessionFileForCwd(kind, cwd, { maxAgeMs: ACTIVE_SESSION_STALE_MS });
|
|
397
|
+
}
|
|
352
398
|
return undefined;
|
|
353
399
|
}
|
|
354
400
|
/** Recover the session UUID from a transcript filename (Claude `<uuid>.jsonl`, Codex `rollout-…-<uuid>.jsonl`). */
|
|
@@ -358,28 +404,64 @@ function sessionIdFromFile(file) {
|
|
|
358
404
|
return undefined;
|
|
359
405
|
return path.basename(file).match(UUID_RE)?.[0];
|
|
360
406
|
}
|
|
407
|
+
/** Cap the events fed to state inference for a non-tailable harness — the last
|
|
408
|
+
* turns are all `inferActivity` needs, and a bound keeps a huge transcript cheap. */
|
|
409
|
+
const LIVE_STATE_MAX_EVENTS = 80;
|
|
361
410
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
411
|
+
* Parse a NON-claude/codex transcript with that harness's own parser and return
|
|
412
|
+
* its last events for state inference. These formats vary too much for the
|
|
413
|
+
* single-file byte-tail fast path (per-session dirs for grok/kimi, SQLite for
|
|
414
|
+
* opencode/antigravity, single-JSON for gemini/hermes), so parse the whole
|
|
415
|
+
* (size-guarded, via safeReadSessionFile) transcript and keep the tail. A parse
|
|
416
|
+
* failure yields no events, degrading honestly to the live fallback.
|
|
367
417
|
*/
|
|
368
|
-
function
|
|
418
|
+
function parseTailEventsForKind(agent, sessionFile) {
|
|
419
|
+
let events;
|
|
420
|
+
try {
|
|
421
|
+
events = parseSession(sessionFile, agent);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
return events.length > LIVE_STATE_MAX_EVENTS ? events.slice(-LIVE_STATE_MAX_EVENTS) : events;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
430
|
+
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
431
|
+
*
|
|
432
|
+
* Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
|
|
433
|
+
* the hot path, and the only two that also yield throughput (their raw lines
|
|
434
|
+
* carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
|
|
435
|
+
* rush, gemini, kimi, hermes, opencode, antigravity) is parsed with its own
|
|
436
|
+
* parser and run through the SAME {@link inferSessionState}, so a live
|
|
437
|
+
* non-claude/codex agent gets a real working/waiting/idle instead of the blanket
|
|
438
|
+
* `unknown` it used to fall through to. An opaque/untracked kind (cursor) or an
|
|
439
|
+
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
440
|
+
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
441
|
+
*/
|
|
442
|
+
export function computeLiveSignals(kind, sessionFile, cwd, pidAlive) {
|
|
369
443
|
if (!sessionFile)
|
|
370
444
|
return {};
|
|
371
|
-
const agent = kind === 'codex' ? 'codex' : 'claude';
|
|
372
|
-
const { events, content } = readSessionTailWithRaw(sessionFile, agent);
|
|
373
|
-
if (events.length === 0)
|
|
374
|
-
return {};
|
|
375
445
|
let mtimeMs;
|
|
376
446
|
try {
|
|
377
447
|
mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
378
448
|
}
|
|
379
449
|
catch { /* vanished between calls */ }
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
450
|
+
const ctx = { cwd, pidAlive, mtimeMs, activeWindowMs: ACTIVE_MTIME_WINDOW_MS };
|
|
451
|
+
if (kind === 'claude' || kind === 'codex') {
|
|
452
|
+
const { events, content } = readSessionTailWithRaw(sessionFile, kind);
|
|
453
|
+
if (events.length === 0)
|
|
454
|
+
return {};
|
|
455
|
+
const state = inferSessionState(events, ctx);
|
|
456
|
+
const tokPerSec = computeTokPerSec(content, kind);
|
|
457
|
+
return { state, tokPerSec: tokPerSec > 0 ? tokPerSec : undefined };
|
|
458
|
+
}
|
|
459
|
+
if (!isSessionTrackedAgent(kind))
|
|
460
|
+
return {};
|
|
461
|
+
const events = parseTailEventsForKind(kind, sessionFile);
|
|
462
|
+
if (events.length === 0)
|
|
463
|
+
return {};
|
|
464
|
+
return { state: inferSessionState(events, ctx) };
|
|
383
465
|
}
|
|
384
466
|
/** Map inferred activity onto the coarse ActiveStatus used by the renderer and counts. */
|
|
385
467
|
function statusFromActivity(activity) {
|
|
@@ -387,17 +469,23 @@ function statusFromActivity(activity) {
|
|
|
387
469
|
}
|
|
388
470
|
/**
|
|
389
471
|
* Fold a computed SessionState onto an active-session row: rich status +
|
|
390
|
-
* preview + PR/worktree/ticket badges. With no state (
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
472
|
+
* preview + PR/worktree/ticket badges. With no state (an opaque/untracked kind,
|
|
473
|
+
* or an unreadable/empty transcript) it degrades to
|
|
474
|
+
* {@link resolveFallbackStatus}, which reports the honest live floor (`running`
|
|
475
|
+
* for an alive process) rather than the old blanket `unknown`.
|
|
394
476
|
*/
|
|
395
477
|
function applyState(base, state, fallbackFile, pidAlive) {
|
|
396
478
|
if (!state)
|
|
397
479
|
return { ...base, status: resolveFallbackStatus(fallbackFile, pidAlive) };
|
|
480
|
+
// Lifecycle (closed/abandoned) is computed from PID + mtime and OVERRIDES the
|
|
481
|
+
// activity-derived status: a dead or days-stale process is closed/abandoned no
|
|
482
|
+
// matter what its last parsed transcript turn looked like (a dead session whose
|
|
483
|
+
// tail ended mid-tool-call must not read as `running`). `base.lastActivityMs` is
|
|
484
|
+
// the transcript mtime the row already resolved — reuse it, no extra stat.
|
|
485
|
+
const life = lifecycleStatus(pidAlive, base.lastActivityMs ?? sessionFileTimes(fallbackFile).mtimeMs);
|
|
398
486
|
return {
|
|
399
487
|
...base,
|
|
400
|
-
status: statusFromActivity(state.activity),
|
|
488
|
+
status: life ?? statusFromActivity(state.activity),
|
|
401
489
|
activity: state.activity,
|
|
402
490
|
awaitingReason: state.awaitingReason,
|
|
403
491
|
question: state.question,
|
|
@@ -493,16 +581,24 @@ export async function listTeamsActive() {
|
|
|
493
581
|
const mgr = new AgentManager();
|
|
494
582
|
const running = await mgr.listRunning();
|
|
495
583
|
return running.map((a) => {
|
|
496
|
-
|
|
497
|
-
|
|
584
|
+
// The teammate's OWN transcript is `remoteSessionId` (captured from its first
|
|
585
|
+
// stream event). `parentSessionId` is the ORCHESTRATOR that spawned the team
|
|
586
|
+
// (AGENTS_SESSION_ID at spawn) — a link, not this teammate's id. Keying the
|
|
587
|
+
// row off the orchestrator conflated the two (a teammate showed the
|
|
588
|
+
// orchestrator's id/topic and lineage was invisible); resolve the teammate's
|
|
589
|
+
// own session for the row and expose the orchestrator separately.
|
|
590
|
+
const ownSessionId = a.remoteSessionId ?? undefined;
|
|
591
|
+
const sessionFile = findSessionFileForKind(a.agentType, a.cwd ?? undefined, ownSessionId);
|
|
498
592
|
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
499
593
|
const pidAlive = a.pid ? isPidAlive(a.pid) : true;
|
|
500
594
|
const { state, tokPerSec } = computeLiveSignals(a.agentType, sessionFile, a.cwd ?? undefined, pidAlive);
|
|
595
|
+
const resolvedId = ownSessionId ?? sessionIdFromFile(sessionFile);
|
|
501
596
|
return applyState({
|
|
502
597
|
context: 'teams',
|
|
503
598
|
kind: a.agentType,
|
|
504
599
|
pid: a.pid ?? undefined,
|
|
505
|
-
sessionId:
|
|
600
|
+
sessionId: resolvedId,
|
|
601
|
+
orchestratorSessionId: a.parentSessionId ?? undefined,
|
|
506
602
|
cwd: a.cwd ?? undefined,
|
|
507
603
|
label: a.name ?? undefined,
|
|
508
604
|
topic,
|
|
@@ -512,6 +608,10 @@ export async function listTeamsActive() {
|
|
|
512
608
|
lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
|
|
513
609
|
teamName: a.taskName,
|
|
514
610
|
agentId: a.agentId,
|
|
611
|
+
// The frozen actor stamped on the teammate record (RUSH-2028) — who ran
|
|
612
|
+
// this teammate, surfaced as the owner in --active (RUSH-2018); sidecar
|
|
613
|
+
// fallback for a teammate record predating the actor field.
|
|
614
|
+
owner: resolveOwner(a.actor, resolvedId),
|
|
515
615
|
}, state, sessionFile, pidAlive);
|
|
516
616
|
});
|
|
517
617
|
}
|
|
@@ -540,7 +640,8 @@ export async function listTerminalsActive() {
|
|
|
540
640
|
// back to the stale cached id — the duplicate-card fix comes from
|
|
541
641
|
// pickSessionFile no longer borrowing a sibling, not from this lookup. Kept as
|
|
542
642
|
// a forward-looking hook for the cases where the pid does line up.
|
|
543
|
-
const
|
|
643
|
+
const pidEntry = readPidSessionEntry(t.pid);
|
|
644
|
+
const resolvedId = pidEntry?.sessionId ?? t.sessionId;
|
|
544
645
|
const sessionFile = findSessionFileForKind(t.kind, t.cwd ?? undefined, resolvedId);
|
|
545
646
|
// Prefer label from live terminal, fall back to Claude's session label
|
|
546
647
|
const label = t.label ?? (t.sessionId ? labelMap.get(t.sessionId) : undefined) ?? undefined;
|
|
@@ -566,6 +667,7 @@ export async function listTerminalsActive() {
|
|
|
566
667
|
startedAtMs: t.startedAtMs,
|
|
567
668
|
lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
|
|
568
669
|
windowId: t.windowId,
|
|
670
|
+
owner: resolveOwner(pidEntry?.actor, resolvedId),
|
|
569
671
|
}, state, sessionFile, pidAlive);
|
|
570
672
|
});
|
|
571
673
|
}
|
|
@@ -967,7 +1069,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
967
1069
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
968
1070
|
// pidAlive is true by construction: this pid was just enumerated from the
|
|
969
1071
|
// live process table, so an opaque (non-parseable) kind resolves to
|
|
970
|
-
// `
|
|
1072
|
+
// `running`, not a fake `idle` or blanket `unknown`.
|
|
971
1073
|
const { state, tokPerSec } = computeLiveSignals(kind, sessionFile, cwd, true);
|
|
972
1074
|
const { birthtimeMs, mtimeMs } = sessionFileTimes(sessionFile);
|
|
973
1075
|
// Durable run name from `agents run --name`, resolved by the run's session id
|
|
@@ -996,6 +1098,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
996
1098
|
startedAtMs: hookRec?.ts ?? birthtimeMs,
|
|
997
1099
|
lastActivityMs: mtimeMs,
|
|
998
1100
|
pidCount: 1 + (foldedByRoot.get(pid) ?? 0),
|
|
1101
|
+
owner: resolveOwner(entry?.actor, resolvedId),
|
|
999
1102
|
}, state, sessionFile, true));
|
|
1000
1103
|
}
|
|
1001
1104
|
// Housekeeping: drop registry files for pids that have since died.
|
|
@@ -1098,9 +1201,24 @@ export async function listTmuxAgentSessions() {
|
|
|
1098
1201
|
continue;
|
|
1099
1202
|
const meta = readSessionMeta(sessName);
|
|
1100
1203
|
const liveEntry = liveByPane.get(pane);
|
|
1101
|
-
|
|
1204
|
+
let id = resolvePaneIdentity(pane, meta, liveEntry, getHookIndex);
|
|
1102
1205
|
if (!id)
|
|
1103
1206
|
continue;
|
|
1207
|
+
// RUSH-2007 Layer A: a non-Claude tmux session whose id resolved via neither the
|
|
1208
|
+
// launch registry (no id minted at spawn) nor the session-tracker index (not
|
|
1209
|
+
// deployed on the fleet — its dir is empty) — backfill it from the DEPLOYED
|
|
1210
|
+
// hook's own per-pid record at state/sessions/<pid>.json. Targeted single-file
|
|
1211
|
+
// reads on the pane leaf pid, then the registry launch pid; freshness-guarded by
|
|
1212
|
+
// the launch's known start so a reused-pid graveyard file can't cross sessions.
|
|
1213
|
+
// Without this the session surfaces id-less (keyed on the bare pane) and is
|
|
1214
|
+
// invisible to `agents sessions focus` — the remaining RUSH-2007 discovery gap.
|
|
1215
|
+
if (!id.sessionId) {
|
|
1216
|
+
const panePid = parseInt(pidRaw, 10) || undefined;
|
|
1217
|
+
const backfilled = (panePid ? readStateSessionRecord(panePid, liveEntry?.startedAtMs)?.session_id : undefined)
|
|
1218
|
+
?? (liveEntry ? readStateSessionRecord(liveEntry.pid, liveEntry.startedAtMs)?.session_id : undefined);
|
|
1219
|
+
if (backfilled)
|
|
1220
|
+
id = { ...id, sessionId: backfilled };
|
|
1221
|
+
}
|
|
1104
1222
|
// Dedupe by resolved session id; an as-yet-unresolved id (a hookless/lagging
|
|
1105
1223
|
// split) keys on the unique pane so it still surfaces as its own row.
|
|
1106
1224
|
const dedupKey = id.sessionId ?? pane;
|
|
@@ -1143,6 +1261,7 @@ export async function listTmuxAgentSessions() {
|
|
|
1143
1261
|
startedAtMs: birthtimeMs,
|
|
1144
1262
|
lastActivityMs: mtimeMs,
|
|
1145
1263
|
provenance,
|
|
1264
|
+
owner: resolveOwner(liveEntry?.actor, id.sessionId ?? sessionIdFromFile(sessionFile)),
|
|
1146
1265
|
}, state, sessionFile, pidAlive));
|
|
1147
1266
|
}
|
|
1148
1267
|
return out;
|
|
@@ -1176,8 +1295,28 @@ export async function getActiveSessions(opts = {}) {
|
|
|
1176
1295
|
await enrichProvenance(merged);
|
|
1177
1296
|
await resolveOrigins(merged);
|
|
1178
1297
|
foldPresence(merged);
|
|
1298
|
+
annotateOrchestratorLabels(merged);
|
|
1179
1299
|
return merged;
|
|
1180
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
1303
|
+
* when that orchestrator session is itself in the active set (it usually is — the
|
|
1304
|
+
* agent that ran `agents teams add` is running). Falls back to nothing, so the
|
|
1305
|
+
* renderer shows the short id. Pure over the array; exported for tests.
|
|
1306
|
+
*/
|
|
1307
|
+
export function annotateOrchestratorLabels(sessions) {
|
|
1308
|
+
const byId = new Map();
|
|
1309
|
+
for (const s of sessions)
|
|
1310
|
+
if (s.sessionId)
|
|
1311
|
+
byId.set(s.sessionId, s);
|
|
1312
|
+
for (const s of sessions) {
|
|
1313
|
+
if (!s.orchestratorSessionId)
|
|
1314
|
+
continue;
|
|
1315
|
+
const orch = byId.get(s.orchestratorSessionId);
|
|
1316
|
+
if (orch)
|
|
1317
|
+
s.orchestratorLabel = orch.label || orch.topic || undefined;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1181
1320
|
/**
|
|
1182
1321
|
* Fold detach/attach presence onto each row from the detach store. A stored
|
|
1183
1322
|
* record wins (`background`/`parked`); otherwise a live terminal session is
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SessionActorRecord {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
/** Resolved actor id (`resolveActor().id`) — the responsible human/agent. */
|
|
4
|
+
actor: string;
|
|
5
|
+
/** Actor kind (`resolveActor().kind`). */
|
|
6
|
+
initiatedBy: 'human' | 'agent';
|
|
7
|
+
startedAtMs: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Record the actor a session was launched under. Never throws — the sidecar is
|
|
11
|
+
* an attribution optimization; a session with no record simply scans unattributed.
|
|
12
|
+
* No-ops without a concrete session id (nothing to key on).
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeSessionActorRecord(record: SessionActorRecord): void;
|
|
15
|
+
/** Read one session's actor record. Returns undefined if absent/corrupt. */
|
|
16
|
+
export declare function readSessionActorRecord(sessionId: string): SessionActorRecord | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Load every session actor record into a `sessionId -> record` map, for the scan
|
|
19
|
+
* path to join a whole batch of sessions in one directory read instead of a
|
|
20
|
+
* stat-per-row. Best-effort: unreadable/corrupt files are skipped, a missing dir
|
|
21
|
+
* yields an empty map.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadSessionActorIndex(): Map<string, SessionActorRecord>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable sessionId -> actor sidecar (RUSH-2019, P3 lineage).
|
|
3
|
+
*
|
|
4
|
+
* The actor behind a run is resolved at spawn (`resolveActor()`), but the session
|
|
5
|
+
* transcript on disk carries no record of it — so the scanner that indexes those
|
|
6
|
+
* transcripts into `sessions.db` (`discover.ts` -> `upsertSessionsBatch`) has
|
|
7
|
+
* nothing to attribute a session to a person with. The pid-registry answers this
|
|
8
|
+
* for LIVE processes (`--active` owner, RUSH-2018), but it lives under `.cache`
|
|
9
|
+
* and is pruned when the pid dies, so it can't back a DURABLE, historical listing.
|
|
10
|
+
*
|
|
11
|
+
* This module writes one small, durable record per launched session — keyed by
|
|
12
|
+
* session id, under `~/.agents/.history` (never pruned) — that the scanner joins
|
|
13
|
+
* on to fill the write-once `actor` / `initiated_by` columns. Best-effort
|
|
14
|
+
* throughout: a failed write or a corrupt file degrades to an unattributed row,
|
|
15
|
+
* never throws into the launch or the scan path.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { getHistoryDir } from '../state.js';
|
|
20
|
+
function sidecarDir() {
|
|
21
|
+
return path.join(getHistoryDir(), 'by-session');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A session id safe to use as a filename: no path separators or `..`, so a
|
|
25
|
+
* caller-supplied `--session-id` can never escape `by-session/` via
|
|
26
|
+
* `path.join`. Session ids are uuids in practice; anything else is rejected
|
|
27
|
+
* rather than sanitized, so a bad id degrades to no record, never a write
|
|
28
|
+
* outside the directory.
|
|
29
|
+
*/
|
|
30
|
+
function isSafeSessionId(sessionId) {
|
|
31
|
+
return sessionId.length > 0 && !/[/\\]/.test(sessionId) && sessionId !== '.' && sessionId !== '..';
|
|
32
|
+
}
|
|
33
|
+
function recordPath(sessionId) {
|
|
34
|
+
return path.join(sidecarDir(), `${sessionId}.json`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Record the actor a session was launched under. Never throws — the sidecar is
|
|
38
|
+
* an attribution optimization; a session with no record simply scans unattributed.
|
|
39
|
+
* No-ops without a concrete session id (nothing to key on).
|
|
40
|
+
*/
|
|
41
|
+
export function writeSessionActorRecord(record) {
|
|
42
|
+
if (!isSafeSessionId(record.sessionId))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(sidecarDir(), { recursive: true });
|
|
46
|
+
fs.writeFileSync(recordPath(record.sessionId), JSON.stringify(record), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* degrade to an unattributed row */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Read one session's actor record. Returns undefined if absent/corrupt. */
|
|
53
|
+
export function readSessionActorRecord(sessionId) {
|
|
54
|
+
if (!isSafeSessionId(sessionId))
|
|
55
|
+
return undefined;
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = fs.readFileSync(recordPath(sessionId), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' && typeof parsed.actor === 'string') {
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* unparseable */
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load every session actor record into a `sessionId -> record` map, for the scan
|
|
76
|
+
* path to join a whole batch of sessions in one directory read instead of a
|
|
77
|
+
* stat-per-row. Best-effort: unreadable/corrupt files are skipped, a missing dir
|
|
78
|
+
* yields an empty map.
|
|
79
|
+
*/
|
|
80
|
+
export function loadSessionActorIndex() {
|
|
81
|
+
const out = new Map();
|
|
82
|
+
let files;
|
|
83
|
+
try {
|
|
84
|
+
files = fs.readdirSync(sidecarDir()).filter(f => f.endsWith('.json'));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
for (const f of files) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(sidecarDir(), f), 'utf8'));
|
|
92
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' && typeof parsed.actor === 'string') {
|
|
93
|
+
out.set(parsed.sessionId, parsed);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* raced with a writer, or corrupt — skip */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SQLite-backed session index and full-text search.
|
|
3
3
|
*
|
|
4
4
|
* Stores session metadata and user-prompt text in a WAL-mode SQLite database
|
|
5
|
-
* at ~/.agents/sessions/sessions.db. Provides incremental upsert, scan-stamp
|
|
5
|
+
* at ~/.agents/.history/sessions/sessions.db. Provides incremental upsert, scan-stamp
|
|
6
6
|
* ledger (mtime/size tracking to skip unchanged files), FTS5 search with
|
|
7
7
|
* BM25 ranking, and label-first search for /rename'd sessions.
|
|
8
8
|
*/
|
|
@@ -30,6 +30,7 @@ export interface SessionRow {
|
|
|
30
30
|
output_tokens: number | null;
|
|
31
31
|
cost_usd: number | null;
|
|
32
32
|
duration_ms: number | null;
|
|
33
|
+
model: string | null;
|
|
33
34
|
file_path: string;
|
|
34
35
|
file_mtime_ms: number | null;
|
|
35
36
|
file_size: number | null;
|
|
@@ -45,6 +46,8 @@ export interface SessionRow {
|
|
|
45
46
|
recent_directories_touched: string | null;
|
|
46
47
|
linear_project: string | null;
|
|
47
48
|
linear_project_url: string | null;
|
|
49
|
+
actor: string | null;
|
|
50
|
+
initiated_by: string | null;
|
|
48
51
|
}
|
|
49
52
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
50
53
|
export interface ScanStamp {
|