@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.
Files changed (212) hide show
  1. package/CHANGELOG.md +589 -0
  2. package/README.md +27 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +74 -0
  12. package/dist/commands/doctor.js +718 -40
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +53 -8
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +195 -11
  29. package/dist/commands/routines.js +16 -4
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +108 -34
  33. package/dist/commands/sessions-browser.d.ts +45 -3
  34. package/dist/commands/sessions-browser.js +118 -12
  35. package/dist/commands/sessions-export.d.ts +3 -0
  36. package/dist/commands/sessions-export.js +13 -10
  37. package/dist/commands/sessions-inject.d.ts +7 -4
  38. package/dist/commands/sessions-inject.js +18 -18
  39. package/dist/commands/sessions-picker.d.ts +15 -1
  40. package/dist/commands/sessions-picker.js +90 -7
  41. package/dist/commands/sessions-resume.d.ts +16 -0
  42. package/dist/commands/sessions-resume.js +14 -13
  43. package/dist/commands/sessions.d.ts +126 -1
  44. package/dist/commands/sessions.js +365 -42
  45. package/dist/commands/set.d.ts +15 -0
  46. package/dist/commands/set.js +79 -0
  47. package/dist/commands/setup-fleet.d.ts +21 -0
  48. package/dist/commands/setup-fleet.js +201 -0
  49. package/dist/commands/setup-secrets.d.ts +20 -0
  50. package/dist/commands/setup-secrets.js +223 -0
  51. package/dist/commands/setup.js +17 -3
  52. package/dist/commands/skills.js +1 -1
  53. package/dist/commands/ssh.js +9 -1
  54. package/dist/commands/sync.js +7 -2
  55. package/dist/commands/teams-picker.js +0 -1
  56. package/dist/commands/teams.d.ts +33 -1
  57. package/dist/commands/teams.js +208 -44
  58. package/dist/commands/versions.js +10 -5
  59. package/dist/commands/view.d.ts +1 -0
  60. package/dist/commands/view.js +11 -0
  61. package/dist/index.js +8 -13
  62. package/dist/lib/acp/harnesses.js +0 -7
  63. package/dist/lib/activity.d.ts +156 -0
  64. package/dist/lib/activity.js +282 -0
  65. package/dist/lib/agents.d.ts +9 -23
  66. package/dist/lib/agents.js +72 -100
  67. package/dist/lib/auto-pull.d.ts +16 -8
  68. package/dist/lib/auto-pull.js +23 -28
  69. package/dist/lib/browser/service.js +3 -0
  70. package/dist/lib/browser/types.d.ts +7 -0
  71. package/dist/lib/capabilities.js +6 -2
  72. package/dist/lib/crabbox/lease.js +2 -2
  73. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  74. package/dist/lib/crabbox/setup-copy.js +4 -4
  75. package/dist/lib/daemon.d.ts +26 -40
  76. package/dist/lib/daemon.js +63 -123
  77. package/dist/lib/devices/fleet-divergence.d.ts +101 -0
  78. package/dist/lib/devices/fleet-divergence.js +188 -0
  79. package/dist/lib/devices/fleet-inventory.d.ts +19 -0
  80. package/dist/lib/devices/fleet-inventory.js +57 -0
  81. package/dist/lib/devices/fleet.d.ts +1 -1
  82. package/dist/lib/devices/fleet.js +1 -1
  83. package/dist/lib/devices/health-report.d.ts +10 -2
  84. package/dist/lib/devices/health-report.js +32 -1
  85. package/dist/lib/doctor-diff.d.ts +20 -0
  86. package/dist/lib/doctor-diff.js +6 -1
  87. package/dist/lib/drift.d.ts +31 -11
  88. package/dist/lib/drift.js +58 -7
  89. package/dist/lib/events.d.ts +8 -1
  90. package/dist/lib/events.js +10 -1
  91. package/dist/lib/exec.js +35 -8
  92. package/dist/lib/feed-post.d.ts +28 -1
  93. package/dist/lib/feed-post.js +110 -2
  94. package/dist/lib/fleet/apply.js +8 -1
  95. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  96. package/dist/lib/fleet/auth-sync.js +25 -11
  97. package/dist/lib/fleet/remote-login.js +5 -0
  98. package/dist/lib/git.d.ts +27 -14
  99. package/dist/lib/git.js +138 -43
  100. package/dist/lib/hooks.d.ts +42 -0
  101. package/dist/lib/hooks.js +137 -93
  102. package/dist/lib/hosts/dispatch.d.ts +28 -1
  103. package/dist/lib/hosts/dispatch.js +33 -5
  104. package/dist/lib/hosts/option.js +2 -2
  105. package/dist/lib/hosts/passthrough.d.ts +22 -1
  106. package/dist/lib/hosts/passthrough.js +238 -13
  107. package/dist/lib/hosts/remote-cmd.js +2 -0
  108. package/dist/lib/hosts/run-target.d.ts +2 -0
  109. package/dist/lib/hosts/run-target.js +1 -0
  110. package/dist/lib/hq/floor.d.ts +1 -1
  111. package/dist/lib/hq/floor.js +6 -1
  112. package/dist/lib/mcp.js +3 -71
  113. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
  114. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  115. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  116. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  117. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  118. package/dist/lib/menubar/notify-desktop.js +41 -5
  119. package/dist/lib/merged-resources.d.ts +11 -0
  120. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  121. package/dist/lib/permissions.d.ts +0 -32
  122. package/dist/lib/permissions.js +4 -157
  123. package/dist/lib/picker.js +1 -1
  124. package/dist/lib/platform/process.d.ts +16 -5
  125. package/dist/lib/platform/process.js +54 -0
  126. package/dist/lib/plugins.d.ts +0 -8
  127. package/dist/lib/plugins.js +4 -110
  128. package/dist/lib/project-resources.js +4 -1
  129. package/dist/lib/refresh.d.ts +5 -3
  130. package/dist/lib/refresh.js +7 -5
  131. package/dist/lib/resources/commands.js +4 -1
  132. package/dist/lib/resources/mcp.js +0 -4
  133. package/dist/lib/resources/permissions.d.ts +1 -1
  134. package/dist/lib/resources/permissions.js +1 -5
  135. package/dist/lib/resources/rules.js +4 -1
  136. package/dist/lib/resources/skills.js +4 -1
  137. package/dist/lib/resources/subagents.js +4 -0
  138. package/dist/lib/resources/types.d.ts +1 -1
  139. package/dist/lib/routines.d.ts +20 -0
  140. package/dist/lib/routines.js +6 -0
  141. package/dist/lib/run-defaults.d.ts +1 -0
  142. package/dist/lib/run-defaults.js +9 -0
  143. package/dist/lib/runner.d.ts +0 -12
  144. package/dist/lib/runner.js +71 -29
  145. package/dist/lib/sandbox.js +0 -16
  146. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  147. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  149. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  151. package/dist/lib/secrets/agent.d.ts +18 -0
  152. package/dist/lib/secrets/agent.js +131 -17
  153. package/dist/lib/secrets/audit.d.ts +46 -0
  154. package/dist/lib/secrets/audit.js +56 -0
  155. package/dist/lib/secrets/bundles.d.ts +12 -9
  156. package/dist/lib/secrets/bundles.js +47 -31
  157. package/dist/lib/secrets/index.d.ts +2 -1
  158. package/dist/lib/secrets/index.js +8 -2
  159. package/dist/lib/secrets/remote.d.ts +75 -2
  160. package/dist/lib/secrets/remote.js +139 -5
  161. package/dist/lib/secrets/scope.d.ts +26 -0
  162. package/dist/lib/secrets/scope.js +29 -0
  163. package/dist/lib/secrets/session-store.d.ts +10 -0
  164. package/dist/lib/secrets/session-store.js +64 -11
  165. package/dist/lib/secrets/sync.js +3 -3
  166. package/dist/lib/session/active.d.ts +117 -26
  167. package/dist/lib/session/active.js +199 -53
  168. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  169. package/dist/lib/session/actor-sidecar.js +101 -0
  170. package/dist/lib/session/db.d.ts +41 -1
  171. package/dist/lib/session/db.js +173 -8
  172. package/dist/lib/session/digest.js +126 -21
  173. package/dist/lib/session/discover.d.ts +19 -2
  174. package/dist/lib/session/discover.js +32 -11
  175. package/dist/lib/session/hook-sessions.d.ts +9 -0
  176. package/dist/lib/session/hook-sessions.js +57 -8
  177. package/dist/lib/session/origin-machine.d.ts +18 -0
  178. package/dist/lib/session/origin-machine.js +34 -0
  179. package/dist/lib/session/pid-registry.d.ts +13 -0
  180. package/dist/lib/session/render.d.ts +2 -0
  181. package/dist/lib/session/render.js +1 -1
  182. package/dist/lib/session/state.d.ts +1 -0
  183. package/dist/lib/session/state.js +1 -1
  184. package/dist/lib/session/sync/config.js +2 -2
  185. package/dist/lib/session/types.d.ts +16 -0
  186. package/dist/lib/smart-launch.d.ts +86 -0
  187. package/dist/lib/smart-launch.js +172 -0
  188. package/dist/lib/staleness/detectors/commands.js +2 -2
  189. package/dist/lib/staleness/detectors/permissions.js +1 -45
  190. package/dist/lib/staleness/writers/commands.js +4 -5
  191. package/dist/lib/staleness/writers/hooks.js +1 -1
  192. package/dist/lib/startup/command-registry.d.ts +1 -2
  193. package/dist/lib/startup/command-registry.js +2 -4
  194. package/dist/lib/subagents-registry.js +5 -12
  195. package/dist/lib/subagents.d.ts +0 -10
  196. package/dist/lib/subagents.js +0 -12
  197. package/dist/lib/teams/agents.d.ts +0 -2
  198. package/dist/lib/teams/agents.js +10 -27
  199. package/dist/lib/teams/index.d.ts +1 -1
  200. package/dist/lib/teams/index.js +1 -1
  201. package/dist/lib/types.d.ts +17 -9
  202. package/dist/lib/usage.d.ts +28 -5
  203. package/dist/lib/usage.js +271 -8
  204. package/dist/lib/versions.js +7 -1
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +25 -13
  207. package/package.json +1 -2
  208. package/dist/commands/check.d.ts +0 -15
  209. package/dist/commands/check.js +0 -180
  210. package/dist/commands/resources.d.ts +0 -5
  211. package/dist/lib/secrets/account-token.d.ts +0 -20
  212. package/dist/lib/secrets/account-token.js +0 -64
@@ -26,7 +26,8 @@ import { extractSessionTopic } from './prompt.js';
26
26
  import { parseAntigravity } from './parse.js';
27
27
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
28
28
  import { costOfUsage } from '../pricing/index.js';
29
- import { machineId } from './sync/config.js';
29
+ import { machineForSessionFile } from './origin-machine.js';
30
+ export { machineForSessionFile } from './origin-machine.js';
30
31
  import { mapBounded } from '../concurrency.js';
31
32
  import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, cacheLinearProject, } from './db.js';
32
33
  import { buildRunNameMap } from './run-names.js';
@@ -223,7 +224,6 @@ function dispatchAgentScan(agent, onProgress) {
223
224
  default: return Promise.resolve();
224
225
  }
225
226
  }
226
- let _localMachineId;
227
227
  /**
228
228
  * The machine a discovered session originated on. Cross-machine sync mirrors a
229
229
  * remote transcript to backups/<agent>/<machine>/<subdir>/… (see mirrorPath in
@@ -270,15 +270,6 @@ export function isManagedSessionFile(filePath) {
270
270
  return !!realRoot && real.startsWith(realRoot + path.sep);
271
271
  });
272
272
  }
273
- export function machineForSessionFile(filePath, agent) {
274
- const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
275
- if (filePath.startsWith(base)) {
276
- const seg = filePath.slice(base.length).split(path.sep)[0];
277
- if (seg)
278
- return seg;
279
- }
280
- return (_localMachineId ??= machineId());
281
- }
282
273
  /**
283
274
  * Count sessions in scope without running an incremental scan. Assumes the DB
284
275
  * is already fresh (typically true because `discoverSessions` ran first this
@@ -388,6 +379,23 @@ export function isCompleteSessionId(query) {
388
379
  const q = query.trim().toLowerCase();
389
380
  return UUID_36.test(q.replace(SESSION_UUID_PREFIX, '')) || SES_ULID.test(q);
390
381
  }
382
+ /**
383
+ * Whether a query should be treated as a session id rather than a search phrase
384
+ * — the one canonical id-shaped test, shared by every session-id resolver.
385
+ *
386
+ * True for a complete id (`isCompleteSessionId`) AND for a bare hex short-id or
387
+ * prefix (`d3470b57`), which the complete-id check rejects. Any id-shaped query
388
+ * resolves by id ONLY (exact -> prefix -> `findSessionsById` index) and must
389
+ * never fall back to fuzzy content search: a short id like `d3470b57` otherwise
390
+ * surfaces every transcript that merely MENTIONS the string (a resume prompt
391
+ * echoes the parent id into the body of many later sessions). The hex test
392
+ * catches the bare short-id/prefix; `isCompleteSessionId` additionally catches
393
+ * the prefixed whole ids (`session_…`, `ses_…`) that the hex test rejects.
394
+ */
395
+ export function looksLikeSessionId(query) {
396
+ const trimmed = query.trim();
397
+ return /^[0-9a-f-]{6,}$/i.test(trimmed) || isCompleteSessionId(trimmed);
398
+ }
391
399
  /**
392
400
  * Resolve a session by full or short ID. Accepts a pre-loaded session list
393
401
  * (fast path from discoverSessions) and falls back to a DB lookup for the
@@ -992,6 +1000,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
992
1000
  filePath,
993
1001
  gitBranch: scan.gitBranch,
994
1002
  version: scan.version,
1003
+ model: scan.model,
995
1004
  account,
996
1005
  topic: scan.topic,
997
1006
  label,
@@ -1022,6 +1031,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
1022
1031
  lastActivity: scan.lastActivity,
1023
1032
  filePath,
1024
1033
  account,
1034
+ model: scan.model,
1025
1035
  label,
1026
1036
  messageCount: scan.messageCount,
1027
1037
  tokenCount: scan.tokenCount,
@@ -1319,6 +1329,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
1319
1329
  filePath,
1320
1330
  gitBranch: scan.gitBranch,
1321
1331
  version: resolveSessionVersion('codex', filePath, scan.version, currentVersion),
1332
+ model: scan.model,
1322
1333
  topic: scan.topic,
1323
1334
  messageCount: scan.messageCount,
1324
1335
  tokenCount: scan.tokenCount,
@@ -1537,6 +1548,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1537
1548
  cwd,
1538
1549
  filePath,
1539
1550
  version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
1551
+ model: sessionModel,
1540
1552
  topic,
1541
1553
  messageCount,
1542
1554
  tokenCount: sawTokenCount ? tokenCount : undefined,
@@ -2154,6 +2166,7 @@ function readHermesMeta(filePath) {
2154
2166
  project: platform,
2155
2167
  filePath,
2156
2168
  version: model,
2169
+ model,
2157
2170
  topic,
2158
2171
  messageCount: messageCount || (typeof session.message_count === 'number' ? session.message_count : undefined),
2159
2172
  };
@@ -2252,6 +2265,7 @@ async function readDroidMeta(filePath, currentVersion) {
2252
2265
  cwd,
2253
2266
  filePath,
2254
2267
  version: resolveSessionVersion('droid', filePath, undefined, currentVersion),
2268
+ model,
2255
2269
  topic: scan.topic,
2256
2270
  messageCount: scan.messageCount,
2257
2271
  tokenCount,
@@ -2395,6 +2409,7 @@ export function initClaudeParseState() {
2395
2409
  cwd: undefined,
2396
2410
  gitBranch: undefined,
2397
2411
  version: undefined,
2412
+ model: undefined,
2398
2413
  topic: undefined,
2399
2414
  customTitle: undefined,
2400
2415
  aiTitle: undefined,
@@ -2579,6 +2594,8 @@ export function applyClaudeLine(state, parsed) {
2579
2594
  // Per-assistant-message cost: each event carries its own model, so we
2580
2595
  // multiply that event's raw token directions by that model's price.
2581
2596
  const model = parsed.message?.model;
2597
+ if (typeof model === 'string' && model)
2598
+ state.model = model;
2582
2599
  if (model && usageObj && typeof usageObj === 'object') {
2583
2600
  const eventCost = costOfUsage({
2584
2601
  model,
@@ -2611,6 +2628,7 @@ export function finalizeClaudeScan(state) {
2611
2628
  cwd: state.cwd,
2612
2629
  gitBranch: state.gitBranch,
2613
2630
  version: state.version,
2631
+ model: state.model,
2614
2632
  topic: resolvedTopic,
2615
2633
  entrypoint: state.entrypoint,
2616
2634
  messageCount: state.messageCount,
@@ -2676,6 +2694,7 @@ export function serializeClaudeParserState(state, offset) {
2676
2694
  cwd: state.cwd,
2677
2695
  gitBranch: state.gitBranch,
2678
2696
  version: state.version,
2697
+ model: state.model,
2679
2698
  entrypoint: state.entrypoint,
2680
2699
  firstTsMs: state.firstTsMs,
2681
2700
  topic: state.topic,
@@ -2736,6 +2755,7 @@ export function hydrateClaudeParseState(prior) {
2736
2755
  cwd: prior.cwd,
2737
2756
  gitBranch: prior.gitBranch,
2738
2757
  version: prior.version,
2758
+ model: prior.model,
2739
2759
  topic: prior.topic,
2740
2760
  customTitle: prior.customTitle,
2741
2761
  aiTitle: prior.aiTitle,
@@ -3090,6 +3110,7 @@ export function finalizeCodexScan(state) {
3090
3110
  cwd: state.cwd,
3091
3111
  gitBranch: state.gitBranch,
3092
3112
  version: state.version,
3113
+ model: state.model,
3093
3114
  topic: state.topic,
3094
3115
  messageCount: state.messageCount,
3095
3116
  tokenCount: state.tokenCount,
@@ -14,6 +14,15 @@ export interface HookSessionIndex {
14
14
  byTerminalId: Map<string, HookSessionRecord>;
15
15
  byPid: Map<number, HookSessionRecord>;
16
16
  }
17
+ /**
18
+ * Read the deployed hook's record for one specific pid, or undefined if absent /
19
+ * corrupt / not fresh. `startedAtMs` (the live process's known start, when the
20
+ * caller has it) rejects a stale record left by a PRIOR process at a reused pid:
21
+ * the hook writes its record AFTER the agent boots, so a record whose `ts`
22
+ * predates the current process's start belongs to a dead predecessor. `ts` is
23
+ * Unix SECONDS (the hook stamps `date +%s`); `startedAtMs` is millis.
24
+ */
25
+ export declare function readStateSessionRecord(pid: number, startedAtMs?: number): HookSessionRecord | undefined;
17
26
  /**
18
27
  * Scan the hook state dir once and index every record by launch_id, terminal_id,
19
28
  * and pid. Returns empty maps if the dir is absent. Newest-by-`ts` wins a key
@@ -1,13 +1,18 @@
1
1
  /**
2
2
  * Read-only view of the SessionStart hook's live-session state files.
3
3
  *
4
- * The `@agents/session-tracker` hook (installed into each agent's own config)
5
- * writes `~/.agents/.cache/terminals/sessions/<pid>.json` AFTER an agent boots,
6
- * carrying the agent's OWN authoritative session id (from its SessionStart
7
- * payload) plus the join keys `launch_id` / `terminal_id`. It is the only writer
8
- * that sees agents `ag run` did NOT launch (you typing `claude` in a terminal),
9
- * and the only source of an exact id for non-Claude agents (whose id we can't
10
- * know at spawn).
4
+ * Two on-disk sources carry a non-Claude agent's authoritative id (whose id we
5
+ * can't know at spawn), both read here:
6
+ * 1. The `@agents/session-tracker` hook writes
7
+ * `~/.agents/.cache/terminals/sessions/<pid>.json` with the id plus the join
8
+ * keys `launch_id` / `terminal_id`. Rich schema, but this package is NOT
9
+ * deployed on the fleet its dir is empty there. Scanned into the index by
10
+ * {@link loadHookSessionIndex}.
11
+ * 2. The ACTUALLY-DEPLOYED SessionStart hook writes
12
+ * `~/.agents/.cache/state/sessions/<pid>.json` ({@link readStateSessionRecord})
13
+ * — `{session_id,cwd,pid,ts}`, keyed purely by pid. This is the real fleet id
14
+ * source for every agent; read targeted per-pid, never scanned (RUSH-2007).
15
+ * Both see agents `ag run` did NOT launch (you typing `claude` in a terminal).
11
16
  *
12
17
  * This module lets `sessions --active` reconcile a `ps`-discovered pid to that
13
18
  * authoritative id. It is deliberately a small hand-rolled reader — the CLI does
@@ -23,12 +28,56 @@
23
28
  */
24
29
  import fs from 'fs';
25
30
  import path from 'path';
26
- import { getTerminalsDir } from '../state.js';
31
+ import { getTerminalsDir, getRuntimeStateDir } from '../state.js';
27
32
  function hookSessionsDir() {
28
33
  // Sibling of the pid-registry's by-pid/ dir. The hook hardcodes this same path
29
34
  // (packages/session-tracker/src/hook.sh); we read it, never move it.
30
35
  return path.join(getTerminalsDir(), 'sessions');
31
36
  }
37
+ /**
38
+ * The path the ACTUALLY-DEPLOYED SessionStart hook writes:
39
+ * `~/.agents/.cache/state/sessions/<pid>.json`, carrying `{session_id,cwd,pid,ts}`
40
+ * for EVERY agent (claude/codex/gemini/kimi/grok/antigravity). This is the fleet's
41
+ * real id source; the `terminals/sessions/` path above belongs to the separate
42
+ * `@agents/session-tracker` package, which is not deployed (its dir is empty on the
43
+ * fleet), so a non-Claude tmux session's id resolved ONLY through the index above
44
+ * never landed (RUSH-2007). These records are keyed purely by pid — no
45
+ * launch_id/terminal_id/agent — so they join only on pid. The dir is an unpruned
46
+ * graveyard (thousands of dead-pid files), so this is a TARGETED per-pid read on
47
+ * demand, never a full-dir scan folded into {@link loadHookSessionIndex}.
48
+ */
49
+ function stateSessionRecordPath(pid) {
50
+ return path.join(getRuntimeStateDir(), 'sessions', `${pid}.json`);
51
+ }
52
+ /**
53
+ * Read the deployed hook's record for one specific pid, or undefined if absent /
54
+ * corrupt / not fresh. `startedAtMs` (the live process's known start, when the
55
+ * caller has it) rejects a stale record left by a PRIOR process at a reused pid:
56
+ * the hook writes its record AFTER the agent boots, so a record whose `ts`
57
+ * predates the current process's start belongs to a dead predecessor. `ts` is
58
+ * Unix SECONDS (the hook stamps `date +%s`); `startedAtMs` is millis.
59
+ */
60
+ export function readStateSessionRecord(pid, startedAtMs) {
61
+ if (!pid || pid < 1)
62
+ return undefined;
63
+ let rec;
64
+ try {
65
+ rec = parseRecord(fs.readFileSync(stateSessionRecordPath(pid), 'utf8'));
66
+ }
67
+ catch {
68
+ return undefined; // absent (the common case) or unreadable
69
+ }
70
+ if (!rec)
71
+ return undefined;
72
+ if (startedAtMs !== undefined && typeof rec.ts === 'number') {
73
+ // Allow a small skew: the hook fires just after exec, and ts is second-
74
+ // granular so it can floor to just before a sub-second process start.
75
+ const SKEW_MS = 5_000;
76
+ if (rec.ts * 1000 < startedAtMs - SKEW_MS)
77
+ return undefined; // reused-pid graveyard record
78
+ }
79
+ return rec;
80
+ }
32
81
  function parseRecord(raw) {
33
82
  try {
34
83
  const o = JSON.parse(raw);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve which machine a session transcript originated on.
3
+ *
4
+ * Cross-machine sync mirrors a remote transcript to
5
+ * `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
6
+ * file on this box, so the origin is the local machine id.
7
+ *
8
+ * Kept in a leaf module (no session/db imports) so both discovery and the
9
+ * sessions DB upsert path can stamp `machine` without circular deps.
10
+ */
11
+ /** Local machine id, cached for the process lifetime. */
12
+ export declare function localMachineId(): string;
13
+ /**
14
+ * The machine a discovered session originated on.
15
+ * When the path sits under the agent's backups root, the first segment below it
16
+ * is the origin machine id; otherwise it's the local machine.
17
+ */
18
+ export declare function machineForSessionFile(filePath: string, agent: string): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve which machine a session transcript originated on.
3
+ *
4
+ * Cross-machine sync mirrors a remote transcript to
5
+ * `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
6
+ * file on this box, so the origin is the local machine id.
7
+ *
8
+ * Kept in a leaf module (no session/db imports) so both discovery and the
9
+ * sessions DB upsert path can stamp `machine` without circular deps.
10
+ */
11
+ import * as path from 'path';
12
+ import { getHistoryDir } from '../state.js';
13
+ import { machineId } from '../machine-id.js';
14
+ let _localMachineId;
15
+ /** Local machine id, cached for the process lifetime. */
16
+ export function localMachineId() {
17
+ return (_localMachineId ??= machineId());
18
+ }
19
+ /**
20
+ * The machine a discovered session originated on.
21
+ * When the path sits under the agent's backups root, the first segment below it
22
+ * is the origin machine id; otherwise it's the local machine.
23
+ */
24
+ export function machineForSessionFile(filePath, agent) {
25
+ if (!filePath)
26
+ return localMachineId();
27
+ const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
28
+ if (filePath.startsWith(base)) {
29
+ const seg = filePath.slice(base.length).split(path.sep)[0];
30
+ if (seg)
31
+ return seg;
32
+ }
33
+ return localMachineId();
34
+ }
@@ -4,6 +4,19 @@ export interface PidSessionEntry {
4
4
  /** The launch session id. Present for agents launched with a known id (Claude). */
5
5
  sessionId?: string;
6
6
  cwd?: string;
7
+ /**
8
+ * Resolved actor id (`resolveActor().id`) stamped at spawn — who initiated this
9
+ * run. A tailnet login/email for a resolved human, or `UNRESOLVED@<host>` when
10
+ * it can't be determined. Read back by the active-sessions path to surface an
11
+ * `owner` per session (RUSH-2018), so a co-located fleet shows who launched what.
12
+ */
13
+ actor?: string;
14
+ /**
15
+ * The actor's kind (`resolveActor().kind`): `'human'` for a person-initiated
16
+ * run, `'agent'` for one an agent spawned. Pairs with {@link actor} the same way
17
+ * `AGENTS_ACTOR` / `AGENTS_ACTOR_KIND` do on the exec env.
18
+ */
19
+ initiatedBy?: 'human' | 'agent';
7
20
  /**
8
21
  * The launch id minted by `ag run` and exported to the child as
9
22
  * `AGENT_LAUNCH_ID`. The agent's SessionStart hook records the SAME id in its
@@ -64,6 +64,8 @@ export interface SessionStats {
64
64
  }
65
65
  /** Compute aggregate statistics (turns, tools, tokens, duration) from session events. */
66
66
  export declare function computeSummaryStats(events: SessionEvent[]): SessionStats;
67
+ /** Strip the 'claude-' prefix and date suffix from a model identifier. */
68
+ export declare function shortenModel(model: string): string;
67
69
  /** Format a duration in milliseconds as a human-readable string (e.g. '12 min', '2h 30min'). */
68
70
  export declare function formatDuration(ms: number): string;
69
71
  /**
@@ -220,7 +220,7 @@ export function computeSummaryStats(events) {
220
220
  };
221
221
  }
222
222
  /** Strip the 'claude-' prefix and date suffix from a model identifier. */
223
- function shortenModel(model) {
223
+ export function shortenModel(model) {
224
224
  return model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
225
225
  }
226
226
  /** Format a token count as a human-readable string (e.g. 67.5K, 1.2M). */
@@ -130,6 +130,7 @@ export declare function extractTodoProgress(args?: Record<string, any>): TodoPro
130
130
  export declare function extractTodoProgressFromEvents(events: SessionEvent[]): TodoProgress | undefined;
131
131
  /** Derive a recency-ordered, de-duplicated list of directories touched by tools. */
132
132
  export declare function extractRecentDirectoriesTouched(events: SessionEvent[], cwd?: string): string[] | undefined;
133
+ export declare const WORKTREE_RE: RegExp;
133
134
  /** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
134
135
  export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
135
136
  /** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
@@ -188,7 +188,7 @@ const TICKET_BRANCH_RE = /(?:^|[/_-])([a-z]{2,6})-(\d{2,6})(?=[/_-]|$)/;
188
188
  /** Keys that look like tickets but aren't — avoid false positives from branches. */
189
189
  const TICKET_DENYLIST = new Set(['UTF', 'SHA', 'ISO', 'RFC', 'IPV', 'X86', 'ARM', 'MP', 'H']);
190
190
  const PR_URL_RE = /https:\/\/github\.com\/[^\s"'()<>]+\/pull\/(\d+)/;
191
- const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
191
+ export const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
192
192
  /** gh invocations that create/open a PR. */
193
193
  const GH_PR_CREATE_RE = /\bgh\s+pr\s+(?:create|new)\b/;
194
194
  /** gh invocation that opens an issue — the created number is read from its result. */
@@ -15,8 +15,8 @@ function resolveR2Config() {
15
15
  // The daemon monitor loop is headless by construction (no TTY, ever), so
16
16
  // isHeadlessSecretsContext() is true here and this resolves broker-only — a
17
17
  // broker miss can never pop an unattended Touch ID sheet on the user's screen
18
- // (same rationale as daemon.ts readDaemonClaudeOAuthToken). Using the shared
19
- // predicate rather than a literal keeps it consistent with the other callers
18
+ // (the same broker-only-when-headless rationale the secrets readers use). Using
19
+ // the shared predicate rather than a literal keeps it consistent with the other callers
20
20
  // and lets any interactive caller of loadR2Config still prompt.
21
21
  const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'sessions-sync', agentOnly: isHeadlessSecretsContext() });
22
22
  const accountId = env.R2_ACCOUNT_ID?.trim();
@@ -119,6 +119,8 @@ export interface SessionMeta {
119
119
  costUsd?: number;
120
120
  /** Wall-clock duration in ms (lastTs − firstTs), persisted at scan time. */
121
121
  durationMs?: number;
122
+ /** Underlying LLM model observed in the transcript, when the agent records one. */
123
+ model?: string;
122
124
  version?: string;
123
125
  account?: string;
124
126
  topic?: string;
@@ -190,6 +192,20 @@ export interface SessionMeta {
190
192
  * undefined for sessions obtained outside that path.
191
193
  */
192
194
  machine?: string;
195
+ /**
196
+ * Resolved actor id (`resolveActor().id`) who initiated this session — a
197
+ * tailnet login/email for a resolved human, or `UNRESOLVED@<host>`. Persisted
198
+ * write-once at session creation and preserved across content rescans (kept
199
+ * out of the DB upsert's ON CONFLICT set). Undefined for rows created before
200
+ * actor stamping. RUSH-2018.
201
+ */
202
+ actor?: string;
203
+ /**
204
+ * The actor's kind (`resolveActor().kind`): `'human'` for a person-initiated
205
+ * run, `'agent'` for one an agent spawned. Pairs with {@link actor}, same
206
+ * split as `AGENTS_ACTOR` / `AGENTS_ACTOR_KIND` on the exec env. RUSH-2018.
207
+ */
208
+ initiatedBy?: 'human' | 'agent';
193
209
  /**
194
210
  * True only for rows pulled from another machine over the live cross-machine
195
211
  * fan-out (`remote-list.ts`) — their transcript is on that peer's disk, so
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Device affinity for `--device auto` / `--host auto`.
3
+ *
4
+ * Picks a host from 14d session usage (launches by machine), weighted sample
5
+ * among online devices. Most-used has highest probability — not a hard lock.
6
+ * Account selection is separate (`--strategy balanced` / rotate.ts).
7
+ */
8
+ import { type AffinityRow } from './session/db.js';
9
+ /** Peakiness of usage weights; >1 amplifies the most-used option. */
10
+ export declare const DEFAULT_AFFINITY_ALPHA = 1.3;
11
+ export interface WeightedCandidate {
12
+ key: string;
13
+ launches: number;
14
+ weight: number;
15
+ }
16
+ /**
17
+ * Convert affinity rows into positive weights: launches^α (floor 1 for any
18
+ * row with launches > 0 so a single-use host still participates).
19
+ */
20
+ export declare function affinityWeights(rows: AffinityRow[], alpha?: number): WeightedCandidate[];
21
+ /**
22
+ * Weighted random sample. Returns null when candidates is empty.
23
+ * Pure — inject `rng` for tests.
24
+ */
25
+ export declare function sampleWeighted(candidates: WeightedCandidate[], rng?: () => number): string | null;
26
+ /** Online device names from the local registry (+ always include local). */
27
+ export declare function listOnlineDeviceNames(localName?: string): string[];
28
+ export interface DeviceAffinityOptions {
29
+ sinceDays?: number;
30
+ alpha?: number;
31
+ /**
32
+ * Eligible hosts (normalized). Defaults to online devices + local.
33
+ * Empty after filter → fall back to local.
34
+ */
35
+ eligibleHosts?: string[];
36
+ localMachine?: string;
37
+ /** Injected affinity (tests). When omitted, reads sessions.db. */
38
+ deviceAffinity?: AffinityRow[];
39
+ rng?: () => number;
40
+ project?: string;
41
+ }
42
+ export interface DeviceAffinityPlan {
43
+ /** null means run locally (do not pass --host). */
44
+ host: string | null;
45
+ deviceCandidates: WeightedCandidate[];
46
+ pickedDeviceKey: string | null;
47
+ }
48
+ /**
49
+ * Resolve host for `--device auto`. Does NOT pick harness or accounts.
50
+ */
51
+ export declare function resolveDeviceAffinity(opts?: DeviceAffinityOptions): DeviceAffinityPlan;
52
+ /** True when a host flag value means affinity pick. */
53
+ export declare function isDeviceAuto(value: string | undefined | null): boolean;
54
+ export type DeviceAutoHostOptions = {
55
+ host?: string;
56
+ device?: string;
57
+ on?: string;
58
+ computer?: string;
59
+ /** @deprecated Hidden alias for `--device auto`. */
60
+ smart?: boolean;
61
+ balanced?: boolean;
62
+ strategy?: string;
63
+ };
64
+ export type DeviceAutoApplyResult = {
65
+ /** True when affinity ran (success or degrade-to-local). */
66
+ attempted: boolean;
67
+ /** True when deprecated `--smart` was seen. */
68
+ deprecationSmart: boolean;
69
+ /** Affinity threw: host slots cleared → local; message for stderr. */
70
+ skipped?: string;
71
+ /** Present when affinity resolved without error. */
72
+ banner?: {
73
+ hostLabel: string;
74
+ deviceHint: string;
75
+ acctNote: string;
76
+ };
77
+ };
78
+ /**
79
+ * Apply `--device auto` / `--host auto` (and deprecated `--smart`) onto run options.
80
+ * Mutates `options` in place. On affinity failure, clears auto slots (local) and
81
+ * returns `skipped` — never throws; callers must not hard-exit.
82
+ */
83
+ export declare function applyDeviceAutoToOptions(options: DeviceAutoHostOptions, deps?: {
84
+ resolve?: () => DeviceAffinityPlan;
85
+ accountPickerRequested?: boolean;
86
+ }): DeviceAutoApplyResult;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Device affinity for `--device auto` / `--host auto`.
3
+ *
4
+ * Picks a host from 14d session usage (launches by machine), weighted sample
5
+ * among online devices. Most-used has highest probability — not a hard lock.
6
+ * Account selection is separate (`--strategy balanced` / rotate.ts).
7
+ */
8
+ import { queryAffinityRollup } from './session/db.js';
9
+ import { localMachineId } from './session/origin-machine.js';
10
+ import { loadDevicesSync } from './devices/registry.js';
11
+ import { normalizeHost } from './machine-id.js';
12
+ /** Peakiness of usage weights; >1 amplifies the most-used option. */
13
+ export const DEFAULT_AFFINITY_ALPHA = 1.3;
14
+ /**
15
+ * Convert affinity rows into positive weights: launches^α (floor 1 for any
16
+ * row with launches > 0 so a single-use host still participates).
17
+ */
18
+ export function affinityWeights(rows, alpha = DEFAULT_AFFINITY_ALPHA) {
19
+ return rows
20
+ .filter((r) => r.launches > 0 && r.key && r.key !== '(unknown)')
21
+ .map((r) => ({
22
+ key: r.key,
23
+ launches: r.launches,
24
+ weight: Math.max(1, Math.pow(r.launches, alpha)),
25
+ }));
26
+ }
27
+ /**
28
+ * Weighted random sample. Returns null when candidates is empty.
29
+ * Pure — inject `rng` for tests.
30
+ */
31
+ export function sampleWeighted(candidates, rng = Math.random) {
32
+ if (candidates.length === 0)
33
+ return null;
34
+ const total = candidates.reduce((s, c) => s + c.weight, 0);
35
+ if (total <= 0)
36
+ return candidates[0].key;
37
+ let roll = rng() * total;
38
+ for (const c of candidates) {
39
+ roll -= c.weight;
40
+ if (roll <= 0)
41
+ return c.key;
42
+ }
43
+ return candidates[candidates.length - 1].key;
44
+ }
45
+ /** Online device names from the local registry (+ always include local). */
46
+ export function listOnlineDeviceNames(localName = localMachineId()) {
47
+ const names = new Set([normalizeHost(localName)]);
48
+ try {
49
+ const reg = loadDevicesSync();
50
+ for (const [name, d] of Object.entries(reg)) {
51
+ if (!d || typeof d !== 'object')
52
+ continue;
53
+ const online = d.tailscale?.online;
54
+ // No tailscale snapshot → treat as candidate (registry-only box).
55
+ if (online === false)
56
+ continue;
57
+ names.add(normalizeHost(name));
58
+ }
59
+ }
60
+ catch {
61
+ /* registry missing — local only */
62
+ }
63
+ return [...names];
64
+ }
65
+ /**
66
+ * Resolve host for `--device auto`. Does NOT pick harness or accounts.
67
+ */
68
+ export function resolveDeviceAffinity(opts = {}) {
69
+ const local = normalizeHost(opts.localMachine ?? localMachineId());
70
+ const alpha = opts.alpha ?? DEFAULT_AFFINITY_ALPHA;
71
+ const rng = opts.rng ?? Math.random;
72
+ const sinceDays = opts.sinceDays ?? 14;
73
+ const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
74
+ const eligible = new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost));
75
+ if (eligible.size === 0)
76
+ eligible.add(local);
77
+ const deviceRows = opts.deviceAffinity ??
78
+ queryAffinityRollup({
79
+ groupBy: 'machine',
80
+ sinceMs,
81
+ excludeTeamOrigin: true,
82
+ onlyCli: true,
83
+ project: opts.project,
84
+ });
85
+ // Hosts with history get launches^α; online hosts with no history explore at weight 1.
86
+ const launchesByHost = new Map();
87
+ for (const r of deviceRows) {
88
+ const k = normalizeHost(r.key);
89
+ if (!eligible.has(k) || k === '(unknown)')
90
+ continue;
91
+ launchesByHost.set(k, (launchesByHost.get(k) ?? 0) + r.launches);
92
+ }
93
+ let deviceWeights = [...eligible].map((key) => {
94
+ const launches = launchesByHost.get(key) ?? 0;
95
+ return {
96
+ key,
97
+ launches,
98
+ weight: launches > 0 ? Math.max(1, Math.pow(launches, alpha)) : 1,
99
+ };
100
+ });
101
+ if (deviceWeights.length === 0) {
102
+ deviceWeights = [{ key: local, launches: 1, weight: 1 }];
103
+ }
104
+ const pickedDeviceKey = sampleWeighted(deviceWeights, rng);
105
+ const hostNorm = pickedDeviceKey ? normalizeHost(pickedDeviceKey) : local;
106
+ const host = hostNorm === local ? null : hostNorm;
107
+ return {
108
+ host,
109
+ deviceCandidates: deviceWeights,
110
+ pickedDeviceKey,
111
+ };
112
+ }
113
+ /** True when a host flag value means affinity pick. */
114
+ export function isDeviceAuto(value) {
115
+ return typeof value === 'string' && value.trim().toLowerCase() === 'auto';
116
+ }
117
+ const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
118
+ /**
119
+ * Apply `--device auto` / `--host auto` (and deprecated `--smart`) onto run options.
120
+ * Mutates `options` in place. On affinity failure, clears auto slots (local) and
121
+ * returns `skipped` — never throws; callers must not hard-exit.
122
+ */
123
+ export function applyDeviceAutoToOptions(options, deps = {}) {
124
+ let deprecationSmart = false;
125
+ if (options.smart) {
126
+ const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
127
+ if (!anyHost)
128
+ options.device = 'auto';
129
+ deprecationSmart = true;
130
+ }
131
+ const hasAuto = HOST_SLOTS.some((k) => isDeviceAuto(options[k]));
132
+ if (!hasAuto) {
133
+ return { attempted: false, deprecationSmart };
134
+ }
135
+ try {
136
+ const plan = (deps.resolve ?? (() => resolveDeviceAffinity({})))();
137
+ const concrete = plan.host; // null = local
138
+ for (const k of HOST_SLOTS) {
139
+ if (isDeviceAuto(options[k])) {
140
+ options[k] = concrete ?? undefined;
141
+ }
142
+ }
143
+ const accountPickerRequested = deps.accountPickerRequested ?? false;
144
+ if (!accountPickerRequested && !options.strategy && !options.balanced) {
145
+ options.balanced = true;
146
+ }
147
+ const hostLabel = concrete ?? 'local';
148
+ const deviceHint = plan.deviceCandidates
149
+ .slice(0, 4)
150
+ .map((c) => `${c.key}:${c.launches}`)
151
+ .join(', ');
152
+ const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
153
+ return {
154
+ attempted: true,
155
+ deprecationSmart,
156
+ banner: { hostLabel, deviceHint, acctNote },
157
+ };
158
+ }
159
+ catch (err) {
160
+ // Graceful degrade: clear auto slots so the run continues locally.
161
+ for (const k of HOST_SLOTS) {
162
+ if (isDeviceAuto(options[k])) {
163
+ options[k] = undefined;
164
+ }
165
+ }
166
+ return {
167
+ attempted: true,
168
+ deprecationSmart,
169
+ skipped: err.message,
170
+ };
171
+ }
172
+ }