@phnx-labs/agents-cli 1.20.77 → 1.20.82
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 +589 -0
- package/README.md +27 -25
- 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 +74 -0
- package/dist/commands/doctor.js +718 -40
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -8
- 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/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 +195 -11
- package/dist/commands/routines.js +16 -4
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +108 -34
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +118 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.d.ts +15 -1
- package/dist/commands/sessions-picker.js +90 -7
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +126 -1
- package/dist/commands/sessions.js +365 -42
- 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/ssh.js +9 -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 +8 -13
- 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 -23
- package/dist/lib/agents.js +72 -100
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- 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 +26 -40
- package/dist/lib/daemon.js +63 -123
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -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.js +35 -8
- 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 +27 -14
- package/dist/lib/git.js +138 -43
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- 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/remote-cmd.js +2 -0
- 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/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -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 +20 -0
- package/dist/lib/routines.js +6 -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 +71 -29
- package/dist/lib/sandbox.js +0 -16
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +18 -0
- package/dist/lib/secrets/agent.js +131 -17
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +12 -9
- package/dist/lib/secrets/bundles.js +47 -31
- 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/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +10 -0
- package/dist/lib/secrets/session-store.js +64 -11
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +117 -26
- package/dist/lib/session/active.js +199 -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 +41 -1
- package/dist/lib/session/db.js +173 -8
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +19 -2
- package/dist/lib/session/discover.js +32 -11
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- 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/state.d.ts +1 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -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/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
- package/dist/lib/secrets/account-token.d.ts +0 -20
- package/dist/lib/secrets/account-token.js +0 -64
|
@@ -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 {
|
|
29
|
-
import {
|
|
28
|
+
import { readSessionActorRecord } from './actor-sidecar.js';
|
|
29
|
+
import { loadHookSessionIndex, resolveHookSessionRecord, readStateSessionRecord } from './hook-sessions.js';
|
|
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
|
|
@@ -67,7 +81,6 @@ export function activeStatusFromCloudStatus(status) {
|
|
|
67
81
|
return 'queued';
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
const HOME = os.homedir();
|
|
71
84
|
const LIVE_TERMINALS_FILE = path.join(getTerminalsDir(), 'live-terminals.json');
|
|
72
85
|
/**
|
|
73
86
|
* A process is classified `running` if its session file was touched in the
|
|
@@ -83,6 +96,16 @@ const ACTIVE_MTIME_WINDOW_MS = 2 * 60_000;
|
|
|
83
96
|
* transcript just because a GUI app service with the same basename is alive.
|
|
84
97
|
*/
|
|
85
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;
|
|
86
109
|
/** Executables we recognize as agent CLIs when scanning the process table. */
|
|
87
110
|
const AGENT_CLI_NAMES = {
|
|
88
111
|
claude: 'claude',
|
|
@@ -217,9 +240,45 @@ function claudeProjectDirName(cwd) {
|
|
|
217
240
|
* Locate the active Claude session file for a process. If we know the session
|
|
218
241
|
* UUID (from terminal env or team parent), prefer the exact match. Otherwise
|
|
219
242
|
* fall back to the most-recent-mtime .jsonl in the project's folder.
|
|
243
|
+
*
|
|
244
|
+
* Searches EVERY version-home project root, not just the live `~/.claude`
|
|
245
|
+
* symlink. `~/.claude` points at the currently-installed agent version; a
|
|
246
|
+
* session launched under an EARLIER version keeps its transcript under that
|
|
247
|
+
* version's home (`…/.history/versions/claude/<ver>/home/.claude/projects/`).
|
|
248
|
+
* Resolving only `~/.claude/projects` meant that the instant a newer version
|
|
249
|
+
* was installed, every still-running older-version session lost its transcript
|
|
250
|
+
* here — no `sessionFile`, so no start/activity time, so `agents sessions`
|
|
251
|
+
* rendered it `unknown` and the watchdog skipped it as "no activity timestamp".
|
|
252
|
+
* `getAgentSessionDirs('claude','projects')` is the same version-aware enumerator
|
|
253
|
+
* the rest of the CLI uses, so this stays in lockstep with discovery.
|
|
220
254
|
*/
|
|
221
255
|
function findClaudeSessionFile(cwd, sessionId) {
|
|
222
|
-
return
|
|
256
|
+
return pickClaudeSessionFileAcrossRoots(getAgentSessionDirs('claude', 'projects'), cwd, sessionId);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Resolve a Claude transcript for `cwd` across the given project roots, newest
|
|
260
|
+
* mtime winning when the same id/cwd resolves in more than one version home
|
|
261
|
+
* (the actively-written copy is the newest). Pure over `projectRoots`, so it is
|
|
262
|
+
* testable against temp dirs without touching the real home directory.
|
|
263
|
+
*/
|
|
264
|
+
export function pickClaudeSessionFileAcrossRoots(projectRoots, cwd, sessionId) {
|
|
265
|
+
const enc = claudeProjectDirName(cwd);
|
|
266
|
+
let best;
|
|
267
|
+
for (const root of projectRoots) {
|
|
268
|
+
const hit = pickSessionFile(path.join(root, enc), sessionId);
|
|
269
|
+
if (!hit)
|
|
270
|
+
continue;
|
|
271
|
+
let mtime;
|
|
272
|
+
try {
|
|
273
|
+
mtime = fs.statSync(hit).mtimeMs;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!best || mtime > best.mtime)
|
|
279
|
+
best = { path: hit, mtime };
|
|
280
|
+
}
|
|
281
|
+
return best?.path;
|
|
223
282
|
}
|
|
224
283
|
/**
|
|
225
284
|
* Pick a Claude transcript file within a project dir.
|
|
@@ -274,46 +333,68 @@ export function sessionFileTimes(sessionFile) {
|
|
|
274
333
|
return {};
|
|
275
334
|
}
|
|
276
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
|
+
}
|
|
277
359
|
/**
|
|
278
360
|
* The ONE place a fallback status is decided when no rich transcript state is
|
|
279
|
-
* available —
|
|
280
|
-
*
|
|
281
|
-
*
|
|
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`.
|
|
282
364
|
*
|
|
283
|
-
* -
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* UI reads as "done and waiting"), and it never lies as `running` either.
|
|
292
|
-
* - 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`.
|
|
293
373
|
*/
|
|
294
|
-
export function resolveFallbackStatus(sessionFile, pidAlive) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
299
|
-
return Date.now() - mtimeMs < ACTIVE_MTIME_WINDOW_MS ? 'running' : 'idle';
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
return 'unknown';
|
|
303
|
-
}
|
|
374
|
+
export function resolveFallbackStatus(sessionFile, pidAlive, nowMs = Date.now()) {
|
|
375
|
+
const { mtimeMs } = sessionFileTimes(sessionFile);
|
|
376
|
+
return lifecycleStatus(pidAlive, mtimeMs, nowMs) ?? 'running';
|
|
304
377
|
}
|
|
305
378
|
/**
|
|
306
|
-
* Locate the live transcript for an agent process. Claude files are keyed by
|
|
307
|
-
*
|
|
308
|
-
*
|
|
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`.
|
|
309
389
|
*/
|
|
310
390
|
export function findSessionFileForKind(kind, cwd, sessionId) {
|
|
311
391
|
if (!cwd)
|
|
312
392
|
return undefined;
|
|
313
393
|
if (kind === 'claude')
|
|
314
394
|
return findClaudeSessionFile(cwd, sessionId);
|
|
315
|
-
if (kind
|
|
316
|
-
return latestSessionFileForCwd(
|
|
395
|
+
if (isSessionTrackedAgent(kind)) {
|
|
396
|
+
return latestSessionFileForCwd(kind, cwd, { maxAgeMs: ACTIVE_SESSION_STALE_MS });
|
|
397
|
+
}
|
|
317
398
|
return undefined;
|
|
318
399
|
}
|
|
319
400
|
/** Recover the session UUID from a transcript filename (Claude `<uuid>.jsonl`, Codex `rollout-…-<uuid>.jsonl`). */
|
|
@@ -323,28 +404,64 @@ function sessionIdFromFile(file) {
|
|
|
323
404
|
return undefined;
|
|
324
405
|
return path.basename(file).match(UUID_RE)?.[0];
|
|
325
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;
|
|
326
410
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
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.
|
|
332
417
|
*/
|
|
333
|
-
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) {
|
|
334
443
|
if (!sessionFile)
|
|
335
444
|
return {};
|
|
336
|
-
const agent = kind === 'codex' ? 'codex' : 'claude';
|
|
337
|
-
const { events, content } = readSessionTailWithRaw(sessionFile, agent);
|
|
338
|
-
if (events.length === 0)
|
|
339
|
-
return {};
|
|
340
445
|
let mtimeMs;
|
|
341
446
|
try {
|
|
342
447
|
mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
343
448
|
}
|
|
344
449
|
catch { /* vanished between calls */ }
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
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) };
|
|
348
465
|
}
|
|
349
466
|
/** Map inferred activity onto the coarse ActiveStatus used by the renderer and counts. */
|
|
350
467
|
function statusFromActivity(activity) {
|
|
@@ -352,17 +469,23 @@ function statusFromActivity(activity) {
|
|
|
352
469
|
}
|
|
353
470
|
/**
|
|
354
471
|
* Fold a computed SessionState onto an active-session row: rich status +
|
|
355
|
-
* preview + PR/worktree/ticket badges. With no state (
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
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`.
|
|
359
476
|
*/
|
|
360
477
|
function applyState(base, state, fallbackFile, pidAlive) {
|
|
361
478
|
if (!state)
|
|
362
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);
|
|
363
486
|
return {
|
|
364
487
|
...base,
|
|
365
|
-
status: statusFromActivity(state.activity),
|
|
488
|
+
status: life ?? statusFromActivity(state.activity),
|
|
366
489
|
activity: state.activity,
|
|
367
490
|
awaitingReason: state.awaitingReason,
|
|
368
491
|
question: state.question,
|
|
@@ -477,6 +600,10 @@ export async function listTeamsActive() {
|
|
|
477
600
|
lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
|
|
478
601
|
teamName: a.taskName,
|
|
479
602
|
agentId: a.agentId,
|
|
603
|
+
// The frozen actor stamped on the teammate record (RUSH-2028) — who ran
|
|
604
|
+
// this teammate, surfaced as the owner in --active (RUSH-2018); sidecar
|
|
605
|
+
// fallback for a teammate record predating the actor field.
|
|
606
|
+
owner: resolveOwner(a.actor, sessionId ?? sessionIdFromFile(sessionFile)),
|
|
480
607
|
}, state, sessionFile, pidAlive);
|
|
481
608
|
});
|
|
482
609
|
}
|
|
@@ -505,7 +632,8 @@ export async function listTerminalsActive() {
|
|
|
505
632
|
// back to the stale cached id — the duplicate-card fix comes from
|
|
506
633
|
// pickSessionFile no longer borrowing a sibling, not from this lookup. Kept as
|
|
507
634
|
// a forward-looking hook for the cases where the pid does line up.
|
|
508
|
-
const
|
|
635
|
+
const pidEntry = readPidSessionEntry(t.pid);
|
|
636
|
+
const resolvedId = pidEntry?.sessionId ?? t.sessionId;
|
|
509
637
|
const sessionFile = findSessionFileForKind(t.kind, t.cwd ?? undefined, resolvedId);
|
|
510
638
|
// Prefer label from live terminal, fall back to Claude's session label
|
|
511
639
|
const label = t.label ?? (t.sessionId ? labelMap.get(t.sessionId) : undefined) ?? undefined;
|
|
@@ -531,6 +659,7 @@ export async function listTerminalsActive() {
|
|
|
531
659
|
startedAtMs: t.startedAtMs,
|
|
532
660
|
lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
|
|
533
661
|
windowId: t.windowId,
|
|
662
|
+
owner: resolveOwner(pidEntry?.actor, resolvedId),
|
|
534
663
|
}, state, sessionFile, pidAlive);
|
|
535
664
|
});
|
|
536
665
|
}
|
|
@@ -932,7 +1061,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
932
1061
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
933
1062
|
// pidAlive is true by construction: this pid was just enumerated from the
|
|
934
1063
|
// live process table, so an opaque (non-parseable) kind resolves to
|
|
935
|
-
// `
|
|
1064
|
+
// `running`, not a fake `idle` or blanket `unknown`.
|
|
936
1065
|
const { state, tokPerSec } = computeLiveSignals(kind, sessionFile, cwd, true);
|
|
937
1066
|
const { birthtimeMs, mtimeMs } = sessionFileTimes(sessionFile);
|
|
938
1067
|
// Durable run name from `agents run --name`, resolved by the run's session id
|
|
@@ -961,6 +1090,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
961
1090
|
startedAtMs: hookRec?.ts ?? birthtimeMs,
|
|
962
1091
|
lastActivityMs: mtimeMs,
|
|
963
1092
|
pidCount: 1 + (foldedByRoot.get(pid) ?? 0),
|
|
1093
|
+
owner: resolveOwner(entry?.actor, resolvedId),
|
|
964
1094
|
}, state, sessionFile, true));
|
|
965
1095
|
}
|
|
966
1096
|
// Housekeeping: drop registry files for pids that have since died.
|
|
@@ -1063,9 +1193,24 @@ export async function listTmuxAgentSessions() {
|
|
|
1063
1193
|
continue;
|
|
1064
1194
|
const meta = readSessionMeta(sessName);
|
|
1065
1195
|
const liveEntry = liveByPane.get(pane);
|
|
1066
|
-
|
|
1196
|
+
let id = resolvePaneIdentity(pane, meta, liveEntry, getHookIndex);
|
|
1067
1197
|
if (!id)
|
|
1068
1198
|
continue;
|
|
1199
|
+
// RUSH-2007 Layer A: a non-Claude tmux session whose id resolved via neither the
|
|
1200
|
+
// launch registry (no id minted at spawn) nor the session-tracker index (not
|
|
1201
|
+
// deployed on the fleet — its dir is empty) — backfill it from the DEPLOYED
|
|
1202
|
+
// hook's own per-pid record at state/sessions/<pid>.json. Targeted single-file
|
|
1203
|
+
// reads on the pane leaf pid, then the registry launch pid; freshness-guarded by
|
|
1204
|
+
// the launch's known start so a reused-pid graveyard file can't cross sessions.
|
|
1205
|
+
// Without this the session surfaces id-less (keyed on the bare pane) and is
|
|
1206
|
+
// invisible to `agents sessions focus` — the remaining RUSH-2007 discovery gap.
|
|
1207
|
+
if (!id.sessionId) {
|
|
1208
|
+
const panePid = parseInt(pidRaw, 10) || undefined;
|
|
1209
|
+
const backfilled = (panePid ? readStateSessionRecord(panePid, liveEntry?.startedAtMs)?.session_id : undefined)
|
|
1210
|
+
?? (liveEntry ? readStateSessionRecord(liveEntry.pid, liveEntry.startedAtMs)?.session_id : undefined);
|
|
1211
|
+
if (backfilled)
|
|
1212
|
+
id = { ...id, sessionId: backfilled };
|
|
1213
|
+
}
|
|
1069
1214
|
// Dedupe by resolved session id; an as-yet-unresolved id (a hookless/lagging
|
|
1070
1215
|
// split) keys on the unique pane so it still surfaces as its own row.
|
|
1071
1216
|
const dedupKey = id.sessionId ?? pane;
|
|
@@ -1108,6 +1253,7 @@ export async function listTmuxAgentSessions() {
|
|
|
1108
1253
|
startedAtMs: birthtimeMs,
|
|
1109
1254
|
lastActivityMs: mtimeMs,
|
|
1110
1255
|
provenance,
|
|
1256
|
+
owner: resolveOwner(liveEntry?.actor, id.sessionId ?? sessionIdFromFile(sessionFile)),
|
|
1111
1257
|
}, state, sessionFile, pidAlive));
|
|
1112
1258
|
}
|
|
1113
1259
|
return out;
|
|
@@ -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;
|
|
@@ -40,10 +41,13 @@ export interface SessionRow {
|
|
|
40
41
|
worktree_slug: string | null;
|
|
41
42
|
ticket_id: string | null;
|
|
42
43
|
plan: string | null;
|
|
44
|
+
machine: string | null;
|
|
43
45
|
todos: string | null;
|
|
44
46
|
recent_directories_touched: string | null;
|
|
45
47
|
linear_project: string | null;
|
|
46
48
|
linear_project_url: string | null;
|
|
49
|
+
actor: string | null;
|
|
50
|
+
initiated_by: string | null;
|
|
47
51
|
}
|
|
48
52
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
49
53
|
export interface ScanStamp {
|
|
@@ -248,6 +252,42 @@ export interface UsageRollupRow {
|
|
|
248
252
|
}
|
|
249
253
|
/** What to group a usage rollup by. */
|
|
250
254
|
export type UsageRollupGroup = 'agent' | 'project' | 'day';
|
|
255
|
+
/**
|
|
256
|
+
* Smart-launch affinity priors: group sessions by origin machine, harness, or
|
|
257
|
+
* joint (machine + agent). Ordered by launch count desc.
|
|
258
|
+
*
|
|
259
|
+
* SQL shape (device example):
|
|
260
|
+
* SELECT machine, COUNT(*) launches, SUM(duration_ms), SUM(token_count)
|
|
261
|
+
* FROM sessions
|
|
262
|
+
* WHERE timestamp >= ? AND origin = 'cli' AND is_team_origin = 0
|
|
263
|
+
* GROUP BY machine ORDER BY launches DESC
|
|
264
|
+
*
|
|
265
|
+
* Account rotation is NOT done here — that stays on live rate-limit windows
|
|
266
|
+
* via `--strategy balanced` / rotate.ts.
|
|
267
|
+
*/
|
|
268
|
+
export type AffinityGroup = 'machine' | 'agent' | 'machine_agent';
|
|
269
|
+
export interface AffinityRow {
|
|
270
|
+
/** Group key: machine name, agent id, or "machine\\tagent". */
|
|
271
|
+
key: string;
|
|
272
|
+
machine?: string;
|
|
273
|
+
agent?: string;
|
|
274
|
+
launches: number;
|
|
275
|
+
durationMs: number;
|
|
276
|
+
tokenCount: number;
|
|
277
|
+
costUsd: number;
|
|
278
|
+
}
|
|
279
|
+
export declare function queryAffinityRollup(options: {
|
|
280
|
+
groupBy: AffinityGroup;
|
|
281
|
+
/** ISO cutoff or ms; defaults to 14 days ago when omitted. */
|
|
282
|
+
sinceMs?: number;
|
|
283
|
+
/** Restrict to these harnesses (e.g. claude/codex/kimi). */
|
|
284
|
+
agents?: SessionAgentId[];
|
|
285
|
+
/** Default true: only origin=cli rows. */
|
|
286
|
+
onlyCli?: boolean;
|
|
287
|
+
/** Default true: drop team-spawned sessions. */
|
|
288
|
+
excludeTeamOrigin?: boolean;
|
|
289
|
+
project?: string;
|
|
290
|
+
}): AffinityRow[];
|
|
251
291
|
/**
|
|
252
292
|
* Aggregate cost / duration / tokens across sessions, grouped by agent,
|
|
253
293
|
* project, or calendar day. Honors the same filter shape as querySessions
|