@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.
- package/dist/AgentManager.d.ts +11 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +37 -6
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +212 -5
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/capacity/codex.test.js +2 -1
- package/dist/__tests__/capacity/codex.test.js.map +1 -1
- package/dist/__tests__/capacity/index.test.js +6 -3
- package/dist/__tests__/capacity/index.test.js.map +1 -1
- package/dist/__tests__/capacity/zai.test.js +213 -0
- package/dist/__tests__/capacity/zai.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
- package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
- package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
- package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
- package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
- package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
- package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/adapters/index.d.ts +3 -3
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +1 -0
- package/dist/capacity/codex.d.ts.map +1 -1
- package/dist/capacity/codex.js +9 -4
- package/dist/capacity/codex.js.map +1 -1
- package/dist/capacity/index.d.ts +8 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +10 -9
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/types.d.ts +8 -0
- package/dist/capacity/types.d.ts.map +1 -1
- package/dist/capacity/types.js.map +1 -1
- package/dist/capacity/zai.d.ts +14 -0
- package/dist/capacity/zai.d.ts.map +1 -0
- package/dist/capacity/zai.js +203 -0
- package/dist/capacity/zai.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
- package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAdapter.js +108 -0
- package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
- package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
- package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
- package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
- package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
- package/dist/providers/copilot/CopilotSessionParser.js +181 -0
- package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
- package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
- package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
- package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
- package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
- package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
- package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
- package/dist/providers/gemini/GeminiSessionParser.js +166 -0
- package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
- package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +6 -12
- package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
- package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
- package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
- package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
- package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
- package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
- package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
- package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
- package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +5 -5
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +67 -5
- package/src/__tests__/AgentManager.test.ts +231 -5
- package/src/__tests__/capacity/codex.test.ts +94 -26
- package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
- package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
- package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
- package/src/__tests__/capacity/index.test.ts +10 -3
- package/src/__tests__/capacity/zai.test.ts +186 -0
- package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
- package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
- package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
- package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
- package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
- package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
- package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
- package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
- package/src/adapters/index.ts +3 -3
- package/src/capacity/codex.ts +96 -28
- package/src/capacity/index.ts +29 -11
- package/src/capacity/types.ts +8 -0
- package/src/capacity/zai.ts +237 -0
- package/src/index.ts +18 -6
- package/src/providers/copilot/CopilotAdapter.ts +145 -0
- package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
- package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
- package/src/providers/copilot/CopilotSessionParser.ts +289 -0
- package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
- package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
- package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
- package/src/providers/gemini/GeminiSessionParser.ts +271 -0
- package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
- package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
- package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
- package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
- package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
- package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
- package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
- package/src/utils/AgentRegistry.ts +8 -6
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -635
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
- package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
- package/dist/adapters/CopilotAdapter.js +0 -329
- package/dist/adapters/CopilotAdapter.js.map +0 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
- package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
- package/dist/adapters/GeminiCliAdapter.js +0 -513
- package/dist/adapters/GeminiCliAdapter.js.map +0 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/OpenCodeAdapter.js +0 -286
- package/dist/adapters/OpenCodeAdapter.js.map +0 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -543
- package/src/adapters/CopilotAdapter.ts +0 -483
- package/src/adapters/GeminiCliAdapter.ts +0 -699
- package/src/adapters/OpenCodeAdapter.ts +0 -369
package/src/AgentManager.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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("
|
|
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(
|
|
640
|
-
expect(
|
|
641
|
-
expect(
|
|
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: {
|
|
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(
|
|
43
|
-
|
|
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(
|
|
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
|
-
{
|
|
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({
|
|
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" }), {
|
|
107
|
+
new Response(JSON.stringify({ chatgpt_account_id: "acct-1" }), {
|
|
108
|
+
status: 200,
|
|
109
|
+
}),
|
|
93
110
|
)
|
|
94
|
-
.mockResolvedValueOnce(
|
|
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: {
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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(
|
|
213
|
+
.mockResolvedValue(
|
|
214
|
+
new Response("", { status: name === "OAuth 401" ? 401 : 200 }),
|
|
215
|
+
);
|
|
175
216
|
const rpc = vi.fn(async () => ({
|
|
176
217
|
rateLimits: {
|
|
177
|
-
rateLimits: {
|
|
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 () => ({
|
|
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 () =>
|
|
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(
|
|
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: {
|
|
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: {
|
|
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([
|
|
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({
|
|
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 = [
|
|
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,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
|
+
}
|