@ai-devkit/agent-manager 0.33.0 → 0.34.0

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 (174) hide show
  1. package/dist/AgentManager.d.ts +11 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +37 -6
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +212 -5
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/capacity/codex.test.js +2 -1
  8. package/dist/__tests__/capacity/codex.test.js.map +1 -1
  9. package/dist/__tests__/capacity/index.test.js +6 -3
  10. package/dist/__tests__/capacity/index.test.js.map +1 -1
  11. package/dist/__tests__/capacity/zai.test.js +213 -0
  12. package/dist/__tests__/capacity/zai.test.js.map +1 -0
  13. package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
  14. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
  15. package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
  16. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
  17. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
  18. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
  19. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
  20. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
  21. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
  22. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
  23. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
  24. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
  25. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
  26. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
  27. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
  28. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
  29. package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
  30. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  31. package/dist/adapters/index.d.ts +3 -3
  32. package/dist/adapters/index.d.ts.map +1 -1
  33. package/dist/adapters/index.js +3 -3
  34. package/dist/adapters/index.js.map +1 -1
  35. package/dist/capacity/codex.d.ts +1 -0
  36. package/dist/capacity/codex.d.ts.map +1 -1
  37. package/dist/capacity/codex.js +9 -4
  38. package/dist/capacity/codex.js.map +1 -1
  39. package/dist/capacity/index.d.ts +8 -0
  40. package/dist/capacity/index.d.ts.map +1 -1
  41. package/dist/capacity/index.js +10 -9
  42. package/dist/capacity/index.js.map +1 -1
  43. package/dist/capacity/types.d.ts +8 -0
  44. package/dist/capacity/types.d.ts.map +1 -1
  45. package/dist/capacity/types.js.map +1 -1
  46. package/dist/capacity/zai.d.ts +14 -0
  47. package/dist/capacity/zai.d.ts.map +1 -0
  48. package/dist/capacity/zai.js +203 -0
  49. package/dist/capacity/zai.js.map +1 -0
  50. package/dist/index.d.ts +8 -6
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
  55. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
  56. package/dist/providers/copilot/CopilotAdapter.js +108 -0
  57. package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
  58. package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
  59. package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
  60. package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
  61. package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
  62. package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
  63. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
  64. package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
  65. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
  66. package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
  67. package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
  68. package/dist/providers/copilot/CopilotSessionParser.js +181 -0
  69. package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
  70. package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
  71. package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
  72. package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
  73. package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
  74. package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
  75. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
  76. package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
  77. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
  78. package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
  79. package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
  80. package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
  81. package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
  82. package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
  83. package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
  84. package/dist/providers/gemini/GeminiSessionParser.js +166 -0
  85. package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
  86. package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +6 -12
  87. package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
  88. package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
  89. package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
  90. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
  91. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
  92. package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
  93. package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
  94. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
  95. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
  96. package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
  97. package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
  98. package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
  99. package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
  100. package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
  101. package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
  102. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
  103. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
  104. package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
  105. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
  106. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
  107. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
  108. package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
  109. package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
  110. package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
  111. package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
  112. package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
  113. package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
  114. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  115. package/dist/utils/AgentRegistry.js +5 -5
  116. package/dist/utils/AgentRegistry.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/AgentManager.ts +67 -5
  119. package/src/__tests__/AgentManager.test.ts +231 -5
  120. package/src/__tests__/capacity/codex.test.ts +94 -26
  121. package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
  122. package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
  123. package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
  124. package/src/__tests__/capacity/index.test.ts +10 -3
  125. package/src/__tests__/capacity/zai.test.ts +186 -0
  126. package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
  127. package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
  128. package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
  129. package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
  130. package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
  131. package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
  132. package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
  133. package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
  134. package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
  135. package/src/adapters/index.ts +3 -3
  136. package/src/capacity/codex.ts +96 -28
  137. package/src/capacity/index.ts +29 -11
  138. package/src/capacity/types.ts +8 -0
  139. package/src/capacity/zai.ts +237 -0
  140. package/src/index.ts +18 -6
  141. package/src/providers/copilot/CopilotAdapter.ts +145 -0
  142. package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
  143. package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
  144. package/src/providers/copilot/CopilotSessionParser.ts +289 -0
  145. package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
  146. package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
  147. package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
  148. package/src/providers/gemini/GeminiSessionParser.ts +271 -0
  149. package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
  150. package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
  151. package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
  152. package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
  153. package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
  154. package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
  155. package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
  156. package/src/utils/AgentRegistry.ts +8 -6
  157. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
  158. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
  159. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -635
  160. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
  161. package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
  162. package/dist/adapters/CopilotAdapter.js +0 -329
  163. package/dist/adapters/CopilotAdapter.js.map +0 -1
  164. package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
  165. package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
  166. package/dist/adapters/GeminiCliAdapter.js +0 -513
  167. package/dist/adapters/GeminiCliAdapter.js.map +0 -1
  168. package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
  169. package/dist/adapters/OpenCodeAdapter.js +0 -286
  170. package/dist/adapters/OpenCodeAdapter.js.map +0 -1
  171. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -543
  172. package/src/adapters/CopilotAdapter.ts +0 -483
  173. package/src/adapters/GeminiCliAdapter.ts +0 -699
  174. package/src/adapters/OpenCodeAdapter.ts +0 -369
@@ -13,8 +13,19 @@ import type {
13
13
  ProcessInfo,
14
14
  } from "./adapters/AgentAdapter.js";
15
15
  import { sortAgents, type AgentSortKey } from "./utils/sortAgents.js";
16
- import { AgentRegistry, type RegistryEntry } from "./utils/AgentRegistry.js";
16
+ import {
17
+ AgentRegistry,
18
+ parseTmuxRuntimeRef,
19
+ type AgentRuntimeProvider,
20
+ type RegistryEntry,
21
+ } from "./utils/AgentRegistry.js";
17
22
  import { captureProcessSnapshot, filterByProcessNames } from "./utils/process.js";
23
+ import {
24
+ findMatchingHerdrPane,
25
+ fetchHerdrAgentPanes,
26
+ herdrPaneToRuntimeRef,
27
+ type HerdrAgentPane,
28
+ } from "./runtime/herdr/HerdrAgentDiscovery.js";
18
29
 
19
30
  type ProcessSnapshotCapture = (namePatterns: readonly string[]) => Promise<ProcessInfo[]>;
20
31
 
@@ -26,6 +37,14 @@ export interface ListAgentsOptions {
26
37
  sortBy?: AgentSortKey;
27
38
  }
28
39
 
40
+ export interface AgentManagerOptions {
41
+ runtimeProvider?:
42
+ | AgentRuntimeProvider
43
+ | (() => AgentRuntimeProvider | Promise<AgentRuntimeProvider>);
44
+ fetchHerdrAgentPanes?: () => Promise<readonly HerdrAgentPane[]>;
45
+ onRuntimeDiscoveryError?: (error: unknown) => void;
46
+ }
47
+
29
48
  export class AgentNotRunningError extends Error {
30
49
  constructor(public agentName: string) {
31
50
  super(`Agent "${agentName}" is no longer running.`);
@@ -55,6 +74,7 @@ export class AgentManager {
55
74
  constructor(
56
75
  registry: AgentRegistry = AgentRegistry.default(),
57
76
  private readonly captureSnapshot: ProcessSnapshotCapture = captureProcessSnapshot,
77
+ private readonly options: AgentManagerOptions = {},
58
78
  ) {
59
79
  this.registry = registry;
60
80
  }
@@ -193,8 +213,13 @@ export class AgentManager {
193
213
  const preExistingByIdentity = new Map(
194
214
  this.registry.list().map((entry) => [identityKey(entry.type, entry.pid), entry]),
195
215
  );
216
+ const herdrPanes = allAgents.length > 0 ? await this.resolveHerdrPanes() : [];
196
217
  const entries = allAgents.map((agent) =>
197
- this.toRegistryEntry(agent, preExistingByIdentity.get(identityKey(agent.type, agent.pid))),
218
+ this.toRegistryEntry(
219
+ agent,
220
+ preExistingByIdentity.get(identityKey(agent.type, agent.pid)),
221
+ herdrPanes,
222
+ ),
198
223
  );
199
224
  if (entries.length > 0) this.registry.registerBatch(entries);
200
225
  this.registry.pruneIfDue();
@@ -212,9 +237,33 @@ export class AgentManager {
212
237
  return sortAgents(allAgents, sortKey);
213
238
  }
214
239
 
215
- private toRegistryEntry(agent: AgentInfo, existing?: RegistryEntry): RegistryEntry {
216
- return {
217
- name: existing?.name ?? agent.name,
240
+ private async resolveHerdrPanes(): Promise<readonly HerdrAgentPane[]> {
241
+ const runtimeProvider = await this.resolveRuntimeProvider();
242
+ if (runtimeProvider !== "herdr") return [];
243
+
244
+ try {
245
+ return await (this.options.fetchHerdrAgentPanes ?? fetchHerdrAgentPanes)();
246
+ } catch (error) {
247
+ this.options.onRuntimeDiscoveryError?.(error);
248
+ return [];
249
+ }
250
+ }
251
+
252
+ private async resolveRuntimeProvider(): Promise<AgentRuntimeProvider | undefined> {
253
+ const runtimeProvider = this.options.runtimeProvider;
254
+ return typeof runtimeProvider === "function" ? await runtimeProvider() : runtimeProvider;
255
+ }
256
+
257
+ private toRegistryEntry(
258
+ agent: AgentInfo,
259
+ existing?: RegistryEntry,
260
+ herdrPanes: readonly HerdrAgentPane[] = [],
261
+ ): RegistryEntry {
262
+ const name = existing?.name ?? agent.name;
263
+ const existingHasAuthoritativeRuntimeRef =
264
+ existing?.runtime === "herdr" || Boolean(parseTmuxRuntimeRef(existing?.runtimeRef));
265
+ const baseEntry: RegistryEntry = {
266
+ name,
218
267
  type: agent.type,
219
268
  pid: agent.pid,
220
269
  runtime: existing?.runtime ?? "tmux",
@@ -225,6 +274,19 @@ export class AgentManager {
225
274
  sessionFilePath: agent.sessionFilePath ?? "",
226
275
  pinned: existing?.pinned ?? agent.pinned ?? false,
227
276
  };
277
+
278
+ if (!existingHasAuthoritativeRuntimeRef) {
279
+ const herdrPane = findMatchingHerdrPane(agent, herdrPanes);
280
+ if (herdrPane) {
281
+ return {
282
+ ...baseEntry,
283
+ runtime: "herdr",
284
+ runtimeRef: herdrPaneToRuntimeRef(herdrPane, name),
285
+ };
286
+ }
287
+ }
288
+
289
+ return baseEntry;
228
290
  }
229
291
 
230
292
  togglePin(agentName: string): boolean {
@@ -16,6 +16,7 @@ import type {
16
16
  } from "../adapters/AgentAdapter.js";
17
17
  import { AgentStatus } from "../adapters/AgentAdapter.js";
18
18
  import { AgentRegistry, type RegistryEntry } from "../utils/AgentRegistry.js";
19
+ import type { HerdrAgentPane } from "../runtime/herdr/HerdrAgentDiscovery.js";
19
20
 
20
21
  // Mock adapter for testing
21
22
  class MockAdapter implements AgentAdapter {
@@ -360,6 +361,15 @@ describe("AgentManager", () => {
360
361
  fs.rmSync(tmpDir, { recursive: true, force: true });
361
362
  });
362
363
 
364
+ function useHerdrPanes(panes: readonly HerdrAgentPane[]): ReturnType<typeof vi.fn> {
365
+ const fetchHerdrPanes = vi.fn().mockResolvedValue(panes);
366
+ scopedManager = new AgentManager(registry, undefined, {
367
+ runtimeProvider: "herdr",
368
+ fetchHerdrAgentPanes: fetchHerdrPanes,
369
+ });
370
+ return fetchHerdrPanes;
371
+ }
372
+
363
373
  it("persists every detected agent to the registry", async () => {
364
374
  scopedManager.registerAdapter(
365
375
  new MockAdapter("claude", [
@@ -390,6 +400,223 @@ describe("AgentManager", () => {
390
400
  expect(entries[0].startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
391
401
  });
392
402
 
403
+ it("persists a Herdr runtime ref for manually discovered agents matched by session id", async () => {
404
+ useHerdrPanes([
405
+ {
406
+ agent: "codex",
407
+ agentSessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
408
+ cwd: "/cwd/ai-devkit",
409
+ paneId: "w31:p1",
410
+ workspaceId: "w31",
411
+ tabId: "w31:t1",
412
+ },
413
+ ]);
414
+ scopedManager.registerAdapter(
415
+ new MockAdapter("codex", [
416
+ createMockAgent({
417
+ name: "ai-devkit-44290",
418
+ type: "codex",
419
+ pid: process.pid,
420
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
421
+ sessionFilePath: "/path/codex.jsonl",
422
+ projectPath: "/cwd/ai-devkit",
423
+ }),
424
+ ]),
425
+ );
426
+
427
+ await scopedManager.listAgents();
428
+
429
+ expect(registry.list()[0]).toMatchObject({
430
+ name: "ai-devkit-44290",
431
+ type: "codex",
432
+ pid: process.pid,
433
+ runtime: "herdr",
434
+ runtimeRef: {
435
+ session: "default",
436
+ paneId: "w31:p1",
437
+ workspaceId: "w31",
438
+ tabId: "w31:t1",
439
+ agentName: "ai-devkit-44290",
440
+ },
441
+ });
442
+ });
443
+
444
+ it("upgrades an existing tmux fallback row when Herdr later matches by session id", async () => {
445
+ useHerdrPanes([
446
+ {
447
+ agent: "codex",
448
+ agentSessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
449
+ cwd: "/cwd/ai-devkit",
450
+ paneId: "w31:p1",
451
+ },
452
+ ]);
453
+ registry.register({
454
+ name: "ai-devkit-44290",
455
+ type: "codex",
456
+ pid: process.pid,
457
+ runtime: "tmux",
458
+ runtimeRef: null,
459
+ cwd: "/cwd/ai-devkit",
460
+ startedAt: "2026-09-14T00:00:00.000Z",
461
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
462
+ sessionFilePath: "/path/codex.jsonl",
463
+ });
464
+ scopedManager.registerAdapter(
465
+ new MockAdapter("codex", [
466
+ createMockAgent({
467
+ name: "ai-devkit-44290",
468
+ type: "codex",
469
+ pid: process.pid,
470
+ sessionId: "01a0a05e-751f-7850-9fd7-247e297f9ac5",
471
+ sessionFilePath: "/path/codex.jsonl",
472
+ projectPath: "/cwd/ai-devkit",
473
+ }),
474
+ ]),
475
+ );
476
+
477
+ await scopedManager.listAgents();
478
+
479
+ expect(registry.lookup("ai-devkit-44290")).toMatchObject({
480
+ runtime: "herdr",
481
+ runtimeRef: {
482
+ session: "default",
483
+ paneId: "w31:p1",
484
+ agentName: "ai-devkit-44290",
485
+ },
486
+ });
487
+ });
488
+
489
+ it("upgrades an existing tmux fallback row when Herdr matches by foreground pid", async () => {
490
+ useHerdrPanes([
491
+ {
492
+ agent: "codex",
493
+ cwd: "/cwd/ai-devkit",
494
+ paneId: "w31:p1",
495
+ foregroundPids: [44290],
496
+ },
497
+ {
498
+ agent: "codex",
499
+ cwd: "/cwd/ai-devkit",
500
+ paneId: "w32:p1",
501
+ foregroundPids: [process.pid],
502
+ },
503
+ ]);
504
+ registry.register({
505
+ name: "ai-devkit-64904",
506
+ type: "codex",
507
+ pid: process.pid,
508
+ runtime: "tmux",
509
+ runtimeRef: null,
510
+ cwd: "/cwd/ai-devkit",
511
+ startedAt: "2026-09-14T00:00:00.000Z",
512
+ sessionId: `pid-${process.pid}`,
513
+ sessionFilePath: "",
514
+ });
515
+ scopedManager.registerAdapter(
516
+ new MockAdapter("codex", [
517
+ createMockAgent({
518
+ name: "ai-devkit-64904",
519
+ type: "codex",
520
+ pid: process.pid,
521
+ sessionId: `pid-${process.pid}`,
522
+ sessionFilePath: undefined,
523
+ projectPath: "/cwd/ai-devkit",
524
+ }),
525
+ ]),
526
+ );
527
+
528
+ await scopedManager.listAgents();
529
+
530
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
531
+ runtime: "herdr",
532
+ runtimeRef: {
533
+ session: "default",
534
+ paneId: "w32:p1",
535
+ agentName: "ai-devkit-64904",
536
+ },
537
+ });
538
+ });
539
+
540
+ it("fetches Herdr panes automatically when the configured runtime is Herdr", async () => {
541
+ const fetchHerdrPanes = useHerdrPanes([
542
+ {
543
+ agent: "codex",
544
+ cwd: "/cwd/ai-devkit",
545
+ paneId: "w32:p1",
546
+ foregroundPids: [process.pid],
547
+ },
548
+ ]);
549
+ scopedManager.registerAdapter(
550
+ new MockAdapter("codex", [
551
+ createMockAgent({
552
+ name: "ai-devkit-64904",
553
+ type: "codex",
554
+ pid: process.pid,
555
+ sessionId: `pid-${process.pid}`,
556
+ sessionFilePath: undefined,
557
+ projectPath: "/cwd/ai-devkit",
558
+ }),
559
+ ]),
560
+ );
561
+
562
+ await scopedManager.listAgents();
563
+
564
+ expect(fetchHerdrPanes).toHaveBeenCalledOnce();
565
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
566
+ runtime: "herdr",
567
+ runtimeRef: {
568
+ session: "default",
569
+ paneId: "w32:p1",
570
+ agentName: "ai-devkit-64904",
571
+ },
572
+ });
573
+ });
574
+
575
+ it("upgrades an existing tmux row with a malformed runtime ref when Herdr matches", async () => {
576
+ useHerdrPanes([
577
+ {
578
+ agent: "codex",
579
+ cwd: "/cwd/ai-devkit",
580
+ paneId: "w32:p1",
581
+ foregroundPids: [process.pid],
582
+ },
583
+ ]);
584
+ registry.register({
585
+ name: "ai-devkit-64904",
586
+ type: "codex",
587
+ pid: process.pid,
588
+ runtime: "tmux",
589
+ runtimeRef: {},
590
+ cwd: "/cwd/ai-devkit",
591
+ startedAt: "2026-09-14T00:00:00.000Z",
592
+ sessionId: `pid-${process.pid}`,
593
+ sessionFilePath: "",
594
+ });
595
+ scopedManager.registerAdapter(
596
+ new MockAdapter("codex", [
597
+ createMockAgent({
598
+ name: "ai-devkit-64904",
599
+ type: "codex",
600
+ pid: process.pid,
601
+ sessionId: `pid-${process.pid}`,
602
+ sessionFilePath: undefined,
603
+ projectPath: "/cwd/ai-devkit",
604
+ }),
605
+ ]),
606
+ );
607
+
608
+ await scopedManager.listAgents();
609
+
610
+ expect(registry.lookup("ai-devkit-64904")).toMatchObject({
611
+ runtime: "herdr",
612
+ runtimeRef: {
613
+ session: "default",
614
+ paneId: "w32:p1",
615
+ agentName: "ai-devkit-64904",
616
+ },
617
+ });
618
+ });
619
+
393
620
  it("prunes entries for dead pids", async () => {
394
621
  registry.register({
395
622
  name: "dead",
@@ -620,7 +847,7 @@ describe("AgentManager", () => {
620
847
  expect(agents[0].lastActive.toISOString()).toBe("2026-01-01T00:00:00.000Z");
621
848
  });
622
849
 
623
- it("persists changed fields once in one write transaction", async () => {
850
+ it("does not persist passive cwd changes when an existing cwd is present", async () => {
624
851
  const adapter = new MockAdapter("claude", [
625
852
  createMockAgent({ name: "changing", pid: process.pid, projectPath: "/cwd/before" }),
626
853
  ]);
@@ -636,10 +863,9 @@ describe("AgentManager", () => {
636
863
 
637
864
  const upserts = databaseOperations.filter((sql) => /^\s*INSERT INTO agents/i.test(sql));
638
865
  const transactions = databaseOperations.filter((sql) => /^\s*(BEGIN|COMMIT)/i.test(sql));
639
- expect(upserts).toHaveLength(1);
640
- expect(upserts[0]).toContain("'2026-08-14T10:00:01.000Z'");
641
- expect(transactions).toHaveLength(2);
642
- expect(registry.lookup("changing")?.cwd).toBe("/cwd/after");
866
+ expect(upserts).toHaveLength(0);
867
+ expect(transactions).toHaveLength(0);
868
+ expect(registry.lookup("changing")?.cwd).toBe("/cwd/before");
643
869
  });
644
870
 
645
871
  it("prunes newly dead entries only when the passive cadence is due", async () => {
@@ -13,7 +13,11 @@ const context = { installed: true, checkedAt };
13
13
  function apiUsage(overrides: Record<string, unknown> = {}) {
14
14
  return {
15
15
  rate_limit: {
16
- primary_window: { used_percent: 20, limit_window_seconds: 18_000, reset_at: 1_787_220_000 },
16
+ primary_window: {
17
+ used_percent: 20,
18
+ limit_window_seconds: 18_000,
19
+ reset_at: 1_787_220_000,
20
+ },
17
21
  secondary_window: {
18
22
  used_percent: 60,
19
23
  limit_window_seconds: 604_800,
@@ -39,13 +43,18 @@ function apiUsage(overrides: Record<string, unknown> = {}) {
39
43
 
40
44
  describe("Codex auth resolution", () => {
41
45
  it("uses CODEX_HOME before HOME", () => {
42
- expect(resolveCodexAuthPath({ CODEX_HOME: "/custom/codex", HOME: "/users/test" })).toBe(
43
- "/custom/codex/auth.json",
44
- );
46
+ expect(
47
+ resolveCodexAuthPath({
48
+ CODEX_HOME: "/custom/codex",
49
+ HOME: "/users/test",
50
+ }),
51
+ ).toBe("/custom/codex/auth.json");
45
52
  });
46
53
 
47
54
  it("falls back to ~/.codex/auth.json", () => {
48
- expect(resolveCodexAuthPath({ HOME: "/users/test" })).toBe("/users/test/.codex/auth.json");
55
+ expect(resolveCodexAuthPath({ HOME: "/users/test" })).toBe(
56
+ "/users/test/.codex/auth.json",
57
+ );
49
58
  });
50
59
  });
51
60
 
@@ -53,7 +62,11 @@ describe("Codex API usage mapping", () => {
53
62
  it("converts an API window without treating missing data as zero", () => {
54
63
  expect(
55
64
  toRateWindow(
56
- { used_percent: 25, limit_window_seconds: 18_000, reset_at: 1_787_220_000 },
65
+ {
66
+ used_percent: 25,
67
+ limit_window_seconds: 18_000,
68
+ reset_at: 1_787_220_000,
69
+ },
57
70
  "session",
58
71
  "Session",
59
72
  ),
@@ -64,7 +77,9 @@ describe("Codex API usage mapping", () => {
64
77
  usedPercent: 25,
65
78
  resetsAt: "2026-08-20T10:00:00.000Z",
66
79
  });
67
- expect(toRateWindow({}, "session", "Session")).toMatchObject({ usedPercent: null });
80
+ expect(toRateWindow({}, "session", "Session")).toMatchObject({
81
+ usedPercent: null,
82
+ });
68
83
  });
69
84
 
70
85
  it("maps session, weekly, credits, and extra limits", () => {
@@ -89,16 +104,23 @@ describe("tiered Codex probing", () => {
89
104
  const fetch = vi
90
105
  .fn()
91
106
  .mockResolvedValueOnce(
92
- new Response(JSON.stringify({ chatgpt_account_id: "acct-1" }), { status: 200 }),
107
+ new Response(JSON.stringify({ chatgpt_account_id: "acct-1" }), {
108
+ status: 200,
109
+ }),
93
110
  )
94
- .mockResolvedValueOnce(new Response(JSON.stringify(apiUsage()), { status: 200 }));
111
+ .mockResolvedValueOnce(
112
+ new Response(JSON.stringify(apiUsage()), { status: 200 }),
113
+ );
95
114
  const rpc = vi.fn();
96
115
  const result = await probeCodexCapacity({
97
116
  ...context,
98
117
  readFile: async () =>
99
118
  JSON.stringify({
100
119
  personal_access_token: "pat-secret",
101
- tokens: { access_token: "ignored-oauth", account_id: "ignored-account" },
120
+ tokens: {
121
+ access_token: "ignored-oauth",
122
+ account_id: "ignored-account",
123
+ },
102
124
  }),
103
125
  fetch,
104
126
  rpc,
@@ -107,14 +129,17 @@ describe("tiered Codex probing", () => {
107
129
  expect(fetch.mock.calls[0][0]).toBe(
108
130
  "https://auth.openai.com/api/accounts/v1/user-auth-credential/whoami",
109
131
  );
110
- expect(fetch.mock.calls[1][0]).toBe("https://chatgpt.com/backend-api/wham/usage");
132
+ expect(fetch.mock.calls[1][0]).toBe(
133
+ "https://chatgpt.com/backend-api/wham/usage",
134
+ );
111
135
  expect(fetch.mock.calls[1][1].headers).toMatchObject({
112
136
  Authorization: "Bearer pat-secret",
113
137
  "ChatGPT-Account-Id": "acct-1",
114
138
  });
115
139
  expect(rpc).not.toHaveBeenCalled();
116
140
  expect(result).toMatchObject({
117
- provider: "codex",
141
+ harness: "codex",
142
+ provider: "openai",
118
143
  available: "yes",
119
144
  creditsRemaining: 12.5,
120
145
  authenticated: true,
@@ -129,12 +154,18 @@ describe("tiered Codex probing", () => {
129
154
  it("selects a fresh OAuth token without calling whoami", async () => {
130
155
  const fetch = vi
131
156
  .fn()
132
- .mockResolvedValue(new Response(JSON.stringify(apiUsage()), { status: 200 }));
157
+ .mockResolvedValue(
158
+ new Response(JSON.stringify(apiUsage()), { status: 200 }),
159
+ );
133
160
  const result = await probeCodexCapacity({
134
161
  ...context,
135
162
  readFile: async () =>
136
163
  JSON.stringify({
137
- tokens: { access_token: "oauth-secret", account_id: "acct-2", expires_at: 1_800_000_000 },
164
+ tokens: {
165
+ access_token: "oauth-secret",
166
+ account_id: "acct-2",
167
+ expires_at: 1_800_000_000,
168
+ },
138
169
  }),
139
170
  fetch,
140
171
  now: () => new Date("2026-08-20T10:00:00.000Z"),
@@ -158,23 +189,35 @@ describe("tiered Codex probing", () => {
158
189
  "stale OAuth token",
159
190
  async () =>
160
191
  JSON.stringify({
161
- tokens: { access_token: "stale-secret", account_id: "acct", expires_at: 1 },
192
+ tokens: {
193
+ access_token: "stale-secret",
194
+ account_id: "acct",
195
+ expires_at: 1,
196
+ },
162
197
  }),
163
198
  ],
164
199
  [
165
200
  "OAuth 401",
166
201
  async () =>
167
202
  JSON.stringify({
168
- tokens: { access_token: "oauth-secret", account_id: "acct", expires_at: 1_800_000_000 },
203
+ tokens: {
204
+ access_token: "oauth-secret",
205
+ account_id: "acct",
206
+ expires_at: 1_800_000_000,
207
+ },
169
208
  }),
170
209
  ],
171
210
  ])("falls back to the CLI for %s", async (name, readFile) => {
172
211
  const fetch = vi
173
212
  .fn()
174
- .mockResolvedValue(new Response("", { status: name === "OAuth 401" ? 401 : 200 }));
213
+ .mockResolvedValue(
214
+ new Response("", { status: name === "OAuth 401" ? 401 : 200 }),
215
+ );
175
216
  const rpc = vi.fn(async () => ({
176
217
  rateLimits: {
177
- rateLimits: { primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null } },
218
+ rateLimits: {
219
+ primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null },
220
+ },
178
221
  },
179
222
  account: { account: { type: "chatgpt" } },
180
223
  }));
@@ -190,10 +233,14 @@ describe("tiered Codex probing", () => {
190
233
  });
191
234
 
192
235
  it("falls back to CLI if PAT requests fail", async () => {
193
- const rpc = vi.fn(async () => ({ rateLimits: {}, account: { account: null } }));
236
+ const rpc = vi.fn(async () => ({
237
+ rateLimits: {},
238
+ account: { account: null },
239
+ }));
194
240
  const result = await probeCodexCapacity({
195
241
  ...context,
196
- readFile: async () => JSON.stringify({ personal_access_token: "pat-secret" }),
242
+ readFile: async () =>
243
+ JSON.stringify({ personal_access_token: "pat-secret" }),
197
244
  fetch: vi.fn().mockRejectedValue(new Error("network failure pat-secret")),
198
245
  rpc,
199
246
  });
@@ -205,14 +252,20 @@ describe("tiered Codex probing", () => {
205
252
  const fetch = vi
206
253
  .fn()
207
254
  .mockRejectedValueOnce(new Error("PAT failed"))
208
- .mockResolvedValueOnce(new Response(JSON.stringify(apiUsage()), { status: 200 }));
255
+ .mockResolvedValueOnce(
256
+ new Response(JSON.stringify(apiUsage()), { status: 200 }),
257
+ );
209
258
  const rpc = vi.fn();
210
259
  const result = await probeCodexCapacity({
211
260
  ...context,
212
261
  readFile: async () =>
213
262
  JSON.stringify({
214
263
  personal_access_token: "pat-secret",
215
- tokens: { access_token: "oauth-secret", account_id: "acct", expires_at: 1_800_000_000 },
264
+ tokens: {
265
+ access_token: "oauth-secret",
266
+ account_id: "acct",
267
+ expires_at: 1_800_000_000,
268
+ },
216
269
  }),
217
270
  fetch,
218
271
  rpc,
@@ -226,7 +279,9 @@ describe("tiered Codex probing", () => {
226
279
  it("uses hardened read-only app-server arguments and both account methods", async () => {
227
280
  const rpc = vi.fn(async () => ({
228
281
  rateLimits: {
229
- rateLimits: { primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null } },
282
+ rateLimits: {
283
+ primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null },
284
+ },
230
285
  },
231
286
  account: { account: { type: "chatgpt" } },
232
287
  }));
@@ -239,7 +294,13 @@ describe("tiered Codex probing", () => {
239
294
  "account/read",
240
295
  ]);
241
296
  expect(JSON.stringify(messages)).not.toMatch(/prompt|turn\/start/);
242
- expect(CODEX_APP_SERVER_ARGS).toEqual(["-s", "read-only", "-a", "untrusted", "app-server"]);
297
+ expect(CODEX_APP_SERVER_ARGS).toEqual([
298
+ "-s",
299
+ "read-only",
300
+ "-a",
301
+ "untrusted",
302
+ "app-server",
303
+ ]);
243
304
  });
244
305
 
245
306
  it("uses account/read to distinguish logged-out CLI state", async () => {
@@ -248,11 +309,18 @@ describe("tiered Codex probing", () => {
248
309
  readFile: async () => "{}",
249
310
  rpc: async () => ({ rateLimits: {}, account: { account: null } }),
250
311
  });
251
- expect(result).toMatchObject({ authenticated: false, available: "unknown" });
312
+ expect(result).toMatchObject({
313
+ authenticated: false,
314
+ available: "unknown",
315
+ });
252
316
  });
253
317
 
254
318
  it("never exposes tokens or raw auth content through failures", async () => {
255
- const secrets = ["pat-secret-value", "oauth-secret-value", "refresh-secret-value"];
319
+ const secrets = [
320
+ "pat-secret-value",
321
+ "oauth-secret-value",
322
+ "refresh-secret-value",
323
+ ];
256
324
  const result = await probeCodexCapacity({
257
325
  ...context,
258
326
  readFile: async () =>
@@ -0,0 +1,6 @@
1
+ {
2
+ "zai": {
3
+ "type": "api_key",
4
+ "key": "pi-zai-test-key"
5
+ }
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "success": true,
3
+ "code": 200,
4
+ "data": {
5
+ "limits": {}
6
+ }
7
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "success": true,
3
+ "code": 200,
4
+ "data": {
5
+ "limits": [
6
+ {
7
+ "type": "TOKENS_LIMIT",
8
+ "unit": 3,
9
+ "number": 5,
10
+ "percentage": 99,
11
+ "usage": 1000,
12
+ "currentValue": 200,
13
+ "remaining": 700,
14
+ "nextResetTime": 1787220000000
15
+ },
16
+ {
17
+ "type": "CREDIT_LIMIT",
18
+ "unit": 6,
19
+ "number": 1,
20
+ "percentage": 5,
21
+ "usage": 200,
22
+ "currentValue": 80,
23
+ "remaining": 150,
24
+ "nextResetTime": 1787824800000
25
+ },
26
+ {
27
+ "type": "TIME_LIMIT",
28
+ "unit": 5,
29
+ "number": 1,
30
+ "percentage": 25,
31
+ "usage": 100,
32
+ "currentValue": 20,
33
+ "remaining": 90,
34
+ "nextResetTime": 1789812000000
35
+ }
36
+ ]
37
+ }
38
+ }