agent-shim 8.18.0 → 8.20.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/index.cjs CHANGED
@@ -571,9 +571,14 @@ var POOL_SELECTOR_PREFIX = "pool:";
571
571
  var POOL_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._-]*";
572
572
  var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
573
573
  var PoolNameSchema = import_zod.z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
574
- var IdentitySelectorSchema = import_zod.z.string().min(1).regex(new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`));
574
+ var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
575
+ var IdentitySelectorSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
576
+ var PoolMemberSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
577
+ var POOL_PREFERENCES = ["score", "listed"];
578
+ var PoolPreferenceSchema = import_zod.z.enum(POOL_PREFERENCES);
575
579
  var PoolSchema = import_zod.z.strictObject({
576
- identities: import_zod.z.array(import_zod.z.string().min(1).regex(new RegExp(`^${IDENTITY_NAME_PATTERN}$`))).min(1).refine((names) => new Set(names).size === names.length, { message: "pool members must be unique" })
580
+ identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
581
+ preference: PoolPreferenceSchema.optional()
577
582
  });
578
583
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
579
584
  path: import_zod.z.string().min(1),
@@ -5616,54 +5621,6 @@ function resolveLayoutPaths() {
5616
5621
  return buildLayoutPaths(resolveAgentShimHome());
5617
5622
  }
5618
5623
 
5619
- // src/usage/preflight.ts
5620
- var MS_PER_SECOND2 = 1e3;
5621
- var SECONDS_PER_MINUTE = 60;
5622
- var MINUTES_PER_HOUR = 60;
5623
- var HOURS_PER_DAY = 24;
5624
- var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
5625
- function formatAge(ms) {
5626
- const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
5627
- if (minutes < MINUTES_PER_HOUR) {
5628
- return `${String(Math.max(minutes, 0))}m`;
5629
- }
5630
- const hours = Math.floor(minutes / MINUTES_PER_HOUR);
5631
- return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
5632
- }
5633
- function effectiveWindow(window, nowMs) {
5634
- const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
5635
- if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
5636
- return { reset: true, utilization: 0 };
5637
- }
5638
- return {
5639
- reset: false,
5640
- ...window.utilization === void 0 ? {} : { utilization: window.utilization },
5641
- ...resetsAtMs === void 0 ? {} : { resetsAtMs },
5642
- ...window.status === void 0 ? {} : { status: window.status }
5643
- };
5644
- }
5645
- function quotaWarnings(snapshot, provider, nowMs) {
5646
- const rateLimit = snapshot?.providers[provider]?.rateLimit;
5647
- const unified = rateLimit?.unified;
5648
- if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
5649
- return [];
5650
- }
5651
- const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
5652
- const windows = [
5653
- { name: "five-hour", window: unified.fiveHour },
5654
- { name: "seven-day", window: unified.sevenDay }
5655
- ];
5656
- return windows.flatMap(({ name, window }) => {
5657
- const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
5658
- if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
5659
- return [];
5660
- }
5661
- const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
5662
- const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
5663
- return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
5664
- });
5665
- }
5666
-
5667
5624
  // src/usage/poolPick.ts
5668
5625
  var import_node_path17 = __toESM(require("node:path"), 1);
5669
5626
  var import_zod9 = require("zod");
@@ -5910,13 +5867,13 @@ function planOf(account) {
5910
5867
  }
5911
5868
 
5912
5869
  // src/usage/rateLimit.ts
5913
- var MS_PER_SECOND3 = 1e3;
5870
+ var MS_PER_SECOND2 = 1e3;
5914
5871
  function epochSecondsToIso(value) {
5915
5872
  if (value === void 0) {
5916
5873
  return void 0;
5917
5874
  }
5918
5875
  const seconds = Number(value);
5919
- return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
5876
+ return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND2).toISOString() : void 0;
5920
5877
  }
5921
5878
  function nonNegativeNumber(value) {
5922
5879
  if (value === void 0 || value.trim() === "") {
@@ -5972,6 +5929,54 @@ var ErrorObjectSchema = import_zod8.z.looseObject({
5972
5929
  // src/usage/middleware.ts
5973
5930
  var ANTHROPIC_PROVIDER = "anthropic";
5974
5931
 
5932
+ // src/usage/preflight.ts
5933
+ var MS_PER_SECOND3 = 1e3;
5934
+ var SECONDS_PER_MINUTE = 60;
5935
+ var MINUTES_PER_HOUR = 60;
5936
+ var HOURS_PER_DAY = 24;
5937
+ var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
5938
+ function formatAge(ms) {
5939
+ const minutes = Math.floor(ms / (MS_PER_SECOND3 * SECONDS_PER_MINUTE));
5940
+ if (minutes < MINUTES_PER_HOUR) {
5941
+ return `${String(Math.max(minutes, 0))}m`;
5942
+ }
5943
+ const hours = Math.floor(minutes / MINUTES_PER_HOUR);
5944
+ return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
5945
+ }
5946
+ function effectiveWindow(window, nowMs) {
5947
+ const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
5948
+ if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
5949
+ return { reset: true, utilization: 0 };
5950
+ }
5951
+ return {
5952
+ reset: false,
5953
+ ...window.utilization === void 0 ? {} : { utilization: window.utilization },
5954
+ ...resetsAtMs === void 0 ? {} : { resetsAtMs },
5955
+ ...window.status === void 0 ? {} : { status: window.status }
5956
+ };
5957
+ }
5958
+ function quotaWarnings(snapshot, provider, nowMs) {
5959
+ const rateLimit = snapshot?.providers[provider]?.rateLimit;
5960
+ const unified = rateLimit?.unified;
5961
+ if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
5962
+ return [];
5963
+ }
5964
+ const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
5965
+ const windows = [
5966
+ { name: "five-hour", window: unified.fiveHour },
5967
+ { name: "seven-day", window: unified.sevenDay }
5968
+ ];
5969
+ return windows.flatMap(({ name, window }) => {
5970
+ const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
5971
+ if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
5972
+ return [];
5973
+ }
5974
+ const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
5975
+ const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
5976
+ return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
5977
+ });
5978
+ }
5979
+
5975
5980
  // src/usage/pick.ts
5976
5981
  var MS_PER_SECOND4 = 1e3;
5977
5982
  var MS_PER_HOUR = 36e5;
@@ -6033,7 +6038,30 @@ function describeWindow(label, window, effective, nowMs) {
6033
6038
  function capacityOf(plan) {
6034
6039
  return plan.kind === "subscription" ? plan.capacity : 1;
6035
6040
  }
6041
+ function assessRefusedNested(member, nested, nowMs) {
6042
+ const refusal = nested.earliestReturn === void 0 ? { reason: `no member of pool "${nested.pool}" can be picked` } : { reason: `every member of pool "${nested.pool}" is refused; ${nested.earliestReturn.identity} returns at ${new Date(nested.earliestReturn.atMs).toISOString()} (in ${formatAge(nested.earliestReturn.atMs - nowMs)})`, blockedUntilMs: nested.earliestReturn.atMs };
6043
+ return {
6044
+ candidate: {
6045
+ identity: member.identity,
6046
+ class: "ineligible",
6047
+ ...refusal.blockedUntilMs === void 0 ? {} : { blockedUntilMs: refusal.blockedUntilMs },
6048
+ plan: planOf(member.account),
6049
+ reasons: [refusal.reason]
6050
+ }
6051
+ };
6052
+ }
6036
6053
  function assess(member, nowMs) {
6054
+ const nested = member.nested;
6055
+ if (nested === void 0) {
6056
+ return assessIdentity(member, nowMs);
6057
+ }
6058
+ if (nested.kind === "refused") {
6059
+ return assessRefusedNested(member, nested, nowMs);
6060
+ }
6061
+ const assessment = assessIdentity(member, nowMs);
6062
+ return { ...assessment, candidate: { ...assessment.candidate, reasons: [`picked by pool "${nested.pool}": ${nested.reasons.join("; ")}`, ...assessment.candidate.reasons] } };
6063
+ }
6064
+ function assessIdentity(member, nowMs) {
6037
6065
  const plan = planOf(member.account);
6038
6066
  const planReasons = plan.kind === "pay-per-use" ? ["billed by use (no plan allowance)"] : plan.recognised ? [`${String(plan.capacity)}x plan`] : [`plan tier ${plan.tier === void 0 ? "not known" : `"${plan.tier}" not recognised`}, counted as 1x`];
6039
6067
  const base = { identity: member.identity, plan };
@@ -6100,7 +6128,7 @@ function rankPool(input) {
6100
6128
  }
6101
6129
  return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
6102
6130
  });
6103
- const ordered = [...candidates].sort(
6131
+ const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6104
6132
  (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6105
6133
  );
6106
6134
  const sticky = input.sticky;
@@ -6228,6 +6256,12 @@ var DAYS_IN_WEEK = 7;
6228
6256
  var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
6229
6257
 
6230
6258
  // src/usage/poolPick.ts
6259
+ var PoolGraphError = class extends CliError {
6260
+ constructor(message) {
6261
+ super(message);
6262
+ this.name = "PoolGraphError";
6263
+ }
6264
+ };
6231
6265
  var SCHEMA_VERSION = 1;
6232
6266
  var PickEntrySchema = import_zod9.z.strictObject({ identity: import_zod9.z.string().min(1), at: import_zod9.z.iso.datetime() });
6233
6267
  var PicksFileSchema = import_zod9.z.strictObject({
@@ -6254,13 +6288,63 @@ function loadPoolMembers(fs12, paths, identities, nowMs) {
6254
6288
  }
6255
6289
  });
6256
6290
  }
6257
- function rankPoolFromStore(params) {
6258
- return rankPool({
6259
- members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
6260
- nowMs: params.nowMs,
6261
- resuming: params.resuming,
6262
- ...params.sticky === void 0 ? {} : { sticky: params.sticky }
6291
+ function rankPoolGraph(input) {
6292
+ return rankNamedPool(input, input.poolName, [], input.sticky);
6293
+ }
6294
+ function rankNamedPool(input, poolName, stack, sticky) {
6295
+ const pool = input.pools[poolName];
6296
+ if (pool === void 0) {
6297
+ throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6298
+ }
6299
+ const childStack = [...stack, poolName];
6300
+ const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
6301
+ const missing = direct.filter((entry) => !input.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
6302
+ const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
6303
+ const members = [];
6304
+ for (const entry of pool.identities) {
6305
+ const nestedName = poolNameOf(entry);
6306
+ if (nestedName === void 0) {
6307
+ const member = loaded.get(entry);
6308
+ if (member !== void 0) {
6309
+ members.push(member);
6310
+ }
6311
+ continue;
6312
+ }
6313
+ if (input.pools[nestedName] === void 0) {
6314
+ throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
6315
+ }
6316
+ if (childStack.includes(nestedName)) {
6317
+ throw new PoolGraphError(`Pool "${poolName}" member "${entry}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6318
+ }
6319
+ const nested = nestedMember(input, nestedName, childStack, sticky);
6320
+ members.push(nested.member);
6321
+ missing.push(...nested.missing);
6322
+ }
6323
+ const ranking = rankPool({
6324
+ members,
6325
+ nowMs: input.nowMs,
6326
+ resuming: input.resuming,
6327
+ ...sticky === void 0 ? {} : { sticky },
6328
+ ...pool.preference === void 0 ? {} : { preference: pool.preference }
6263
6329
  });
6330
+ return { ranking, missing };
6331
+ }
6332
+ function nestedMember(input, nestedName, childStack, sticky) {
6333
+ const nested = rankNamedPool(input, nestedName, childStack, sticky);
6334
+ const pick = nested.ranking.pick;
6335
+ if (pick !== void 0) {
6336
+ const [member] = loadPoolMembers(input.fs, input.paths, [pick.identity], input.nowMs);
6337
+ return { member: { ...member ?? { identity: pick.identity, records: [] }, nested: { kind: "pick", pool: nestedName, reasons: pick.reasons } }, missing: nested.missing };
6338
+ }
6339
+ const earliestReturn = nested.ranking.earliestReturn;
6340
+ return {
6341
+ member: {
6342
+ identity: earliestReturn?.identity ?? `pool:${nestedName}`,
6343
+ records: [],
6344
+ nested: { kind: "refused", pool: nestedName, ...earliestReturn === void 0 ? {} : { earliestReturn } }
6345
+ },
6346
+ missing: nested.missing
6347
+ };
6264
6348
  }
6265
6349
  function readPicksFile(fs12, file) {
6266
6350
  const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
@@ -6291,59 +6375,6 @@ function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
6291
6375
  `);
6292
6376
  }
6293
6377
 
6294
- // src/launcher/pool.ts
6295
- var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
6296
- var TERMINATOR = "--";
6297
- var REASONS_LOGGED = 3;
6298
- function splitMembers(pool, paths, fs12) {
6299
- const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
6300
- return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
6301
- }
6302
- function isResuming(passthrough) {
6303
- const end = passthrough.indexOf(TERMINATOR);
6304
- return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
6305
- }
6306
- function resolvePoolLaunch(params) {
6307
- const { poolName, log } = params;
6308
- const pool = params.pools?.[poolName];
6309
- if (pool === void 0) {
6310
- return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
6311
- }
6312
- const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
6313
- for (const name of missing) {
6314
- log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
6315
- }
6316
- if (identities.length === 0) {
6317
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
6318
- }
6319
- const resuming = isResuming(params.passthrough);
6320
- const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
6321
- if (stickyRead.problem !== void 0) {
6322
- log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
6323
- }
6324
- for (; ; ) {
6325
- const nowMs = params.now();
6326
- const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
6327
- const { pick, earliestReturn } = ranking;
6328
- if (pick !== void 0) {
6329
- recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
6330
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
6331
- }
6332
- if (earliestReturn === void 0) {
6333
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
6334
- }
6335
- const until = new Date(earliestReturn.atMs).toISOString();
6336
- if (!params.wait) {
6337
- return {
6338
- ok: false,
6339
- message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
6340
- };
6341
- }
6342
- log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
6343
- params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
6344
- }
6345
- }
6346
-
6347
6378
  // src/poolPickReport.ts
6348
6379
  function candidateView(candidate) {
6349
6380
  return {
@@ -6357,14 +6388,15 @@ function candidateView(candidate) {
6357
6388
  };
6358
6389
  }
6359
6390
  function collectPoolPick(params) {
6360
- const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
6361
6391
  const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
6362
- const ranking = rankPoolFromStore({
6392
+ const { ranking, missing } = rankPoolGraph({
6363
6393
  fs: params.usageFs,
6364
6394
  paths: params.paths,
6365
- identities: present,
6395
+ pools: params.pools,
6396
+ poolName: params.poolName,
6366
6397
  nowMs: params.nowMs,
6367
6398
  resuming: false,
6399
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
6368
6400
  ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
6369
6401
  });
6370
6402
  return {
@@ -6372,7 +6404,7 @@ function collectPoolPick(params) {
6372
6404
  directory: params.directory,
6373
6405
  ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
6374
6406
  candidates: ranking.candidates.map(candidateView),
6375
- missing: [...missing],
6407
+ missing: missing.map(({ identity }) => identity),
6376
6408
  ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6377
6409
  ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
6378
6410
  };
@@ -6542,7 +6574,41 @@ function requirePool(paths, name) {
6542
6574
  function writePools(paths, pools) {
6543
6575
  applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
6544
6576
  }
6545
- function addPool(paths, name, identities) {
6577
+ function poolCycleOf(pools, name) {
6578
+ const walk = (current, path32) => {
6579
+ for (const entry of pools[current]?.identities ?? []) {
6580
+ const nested = poolNameOf(entry);
6581
+ if (nested === void 0) {
6582
+ continue;
6583
+ }
6584
+ if (nested === name) {
6585
+ return [...path32, nested];
6586
+ }
6587
+ if (path32.includes(nested)) {
6588
+ continue;
6589
+ }
6590
+ const found = walk(nested, [...path32, nested]);
6591
+ if (found !== void 0) {
6592
+ return found;
6593
+ }
6594
+ }
6595
+ return void 0;
6596
+ };
6597
+ return walk(name, [name]);
6598
+ }
6599
+ function validateMembers(pools, name, members) {
6600
+ for (const entry of members) {
6601
+ const nested = poolNameOf(entry);
6602
+ if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
6603
+ throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
6604
+ }
6605
+ }
6606
+ const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
6607
+ if (cycle !== void 0) {
6608
+ throw new UsageError(`Member "pool:${cycle[1] ?? name}" would make pool "${name}" reach itself: ${cycle.join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6609
+ }
6610
+ }
6611
+ function addPool(paths, name, identities, preference) {
6546
6612
  if (!PoolNameSchema.safeParse(name).success) {
6547
6613
  throw new InvalidPoolNameError(name);
6548
6614
  }
@@ -6550,13 +6616,15 @@ function addPool(paths, name, identities) {
6550
6616
  if (pools[name] !== void 0) {
6551
6617
  throw new PoolAlreadyExistsError(name);
6552
6618
  }
6553
- const pool = { identities: [...identities] };
6619
+ validateMembers(pools, name, identities);
6620
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
6554
6621
  writePools(paths, { ...pools, [name]: pool });
6555
6622
  return pool;
6556
6623
  }
6557
- function setPool(paths, name, identities) {
6624
+ function setPool(paths, name, identities, preference) {
6558
6625
  requirePool(paths, name);
6559
- const pool = { identities: [...identities] };
6626
+ validateMembers(readPools(paths), name, identities);
6627
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
6560
6628
  writePools(paths, { ...readPools(paths), [name]: pool });
6561
6629
  return pool;
6562
6630
  }
@@ -8467,11 +8535,11 @@ function collectCheckReport(params) {
8467
8535
  });
8468
8536
  let poolPick;
8469
8537
  if (decidedIdentity.pool !== void 0) {
8470
- const pool = loaded.globalConfig?.pools?.[decidedIdentity.pool];
8471
- if (pool === void 0) {
8538
+ const pools = loaded.globalConfig?.pools ?? {};
8539
+ if (pools[decidedIdentity.pool] === void 0) {
8472
8540
  throw new PoolNotFoundError(decidedIdentity.pool);
8473
8541
  }
8474
- poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pool, directory: cwd, nowMs: Date.now() });
8542
+ poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pools, directory: cwd, nowMs: Date.now() });
8475
8543
  }
8476
8544
  const identityDecision = poolPick?.pick === void 0 ? decidedIdentity : { ...decidedIdentity, name: poolPick.pick };
8477
8545
  const loadedIdentity = identityDecision.name === void 0 ? void 0 : loadIdentity(paths.identitiesDir, identityDecision.name, realFsPort);
@@ -8605,7 +8673,7 @@ var PoolPickReportSchema = import_zod13.z.strictObject({
8605
8673
  /** The member a launch would run as, absent when every member is refused. */
8606
8674
  pick: import_zod13.z.string().optional(),
8607
8675
  candidates: import_zod13.z.array(PoolPickCandidateViewSchema),
8608
- /** Members that name an identity that does not exist; a launch skips them. */
8676
+ /** Identity members anywhere in the pool's graph that do not exist; a launch skips them. */
8609
8677
  missing: import_zod13.z.array(import_zod13.z.string()),
8610
8678
  /** When nothing can be picked, the soonest any member returns. */
8611
8679
  earliestReturn: import_zod13.z.strictObject({ identity: import_zod13.z.string(), at: import_zod13.z.iso.datetime() }).optional(),
@@ -9235,12 +9303,33 @@ function runDoctor(params) {
9235
9303
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
9236
9304
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
9237
9305
  for (const [poolName, pool] of Object.entries(pools)) {
9238
- const missing = pool.identities.filter((member) => !validIdentityNames.has(member));
9239
- if (missing.length === 0) {
9306
+ const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
9307
+ const undefinedPools = pool.identities.flatMap((member) => {
9308
+ const nested = poolNameOf(member);
9309
+ return nested === void 0 || nested in pools ? [] : [nested];
9310
+ });
9311
+ if (missing.length === 0 && undefinedPools.length === 0) {
9240
9312
  push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
9241
9313
  } else {
9242
- push("pool", "fail", `Pool "${poolName}" names ${missing.map((member) => `identity "${member}"`).join(" and ")}, which ${missing.length === 1 ? "does" : "do"} not exist.`, poolName);
9314
+ const problems = [
9315
+ ...missing.map((member) => `identity "${member}", which does not exist`),
9316
+ ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
9317
+ ];
9318
+ push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
9319
+ }
9320
+ }
9321
+ const reportedCycles = /* @__PURE__ */ new Set();
9322
+ for (const poolName of Object.keys(pools).sort()) {
9323
+ const chain = poolCycleOf(pools, poolName);
9324
+ if (chain === void 0) {
9325
+ continue;
9326
+ }
9327
+ const key = [...chain].slice(0, -1).sort().join(">");
9328
+ if (reportedCycles.has(key)) {
9329
+ continue;
9243
9330
  }
9331
+ reportedCycles.add(key);
9332
+ push("pool", "fail", `Pool cycle: ${chain.join(" -> ")}. A pool cannot nest itself, directly or through another pool.`, chain[0]);
9244
9333
  }
9245
9334
  const selectionExists = (selector) => {
9246
9335
  const poolName = poolNameOf(selector);
@@ -11043,7 +11132,7 @@ var LAUNCHER_FLAG_NAMES = [
11043
11132
  ...BOOLEAN_FLAGS.flatMap(({ flag }) => [flag, `--no-${flag.slice(2)}`])
11044
11133
  ];
11045
11134
  var NATIVE_FLAG = "--native";
11046
- var TERMINATOR2 = "--";
11135
+ var TERMINATOR = "--";
11047
11136
  function matchValuedFlag(token) {
11048
11137
  for (const flag of VALUED_FLAGS) {
11049
11138
  if (token === flag) {
@@ -11085,7 +11174,7 @@ function parseLauncherArgv(argv) {
11085
11174
  if (token === void 0) {
11086
11175
  continue;
11087
11176
  }
11088
- if (token === TERMINATOR2) {
11177
+ if (token === TERMINATOR) {
11089
11178
  rest.push(...remaining.slice(index));
11090
11179
  break;
11091
11180
  }
@@ -11112,7 +11201,7 @@ function parseLauncherArgv(argv) {
11112
11201
  value = matched.inlineValue;
11113
11202
  } else {
11114
11203
  const next = remaining[index + 1];
11115
- if (next === void 0 || next === TERMINATOR2) {
11204
+ if (next === void 0 || next === TERMINATOR) {
11116
11205
  rest.push(token);
11117
11206
  continue;
11118
11207
  }
@@ -11396,6 +11485,76 @@ function splitExtraFlags(value) {
11396
11485
  return trimmed.split(/\s+/);
11397
11486
  }
11398
11487
 
11488
+ // src/launcher/pool.ts
11489
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
11490
+ var TERMINATOR2 = "--";
11491
+ var REASONS_LOGGED = 3;
11492
+ function isResuming(passthrough) {
11493
+ const end = passthrough.indexOf(TERMINATOR2);
11494
+ return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
11495
+ }
11496
+ function resolvePoolLaunch(params) {
11497
+ const { poolName, log } = params;
11498
+ const pool = params.pools?.[poolName];
11499
+ if (pool === void 0) {
11500
+ return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
11501
+ }
11502
+ const resuming = isResuming(params.passthrough);
11503
+ const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
11504
+ if (stickyRead.problem !== void 0) {
11505
+ log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
11506
+ }
11507
+ const warned = /* @__PURE__ */ new Set();
11508
+ for (; ; ) {
11509
+ const nowMs = params.now();
11510
+ let graph;
11511
+ try {
11512
+ graph = rankPoolGraph({
11513
+ fs: params.usageFs,
11514
+ paths: params.paths,
11515
+ pools: params.pools ?? {},
11516
+ poolName,
11517
+ nowMs,
11518
+ resuming,
11519
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
11520
+ ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky }
11521
+ });
11522
+ } catch (error) {
11523
+ if (error instanceof PoolGraphError) {
11524
+ return { ok: false, message: `agent-shim: ${error.message}` };
11525
+ }
11526
+ throw error;
11527
+ }
11528
+ for (const gone of graph.missing) {
11529
+ const key = `${gone.pool}:${gone.identity}`;
11530
+ if (!warned.has(key)) {
11531
+ warned.add(key);
11532
+ log.warn(`agent-shim: pool "${gone.pool}" names identity "${gone.identity}", which does not exist; skipping it`);
11533
+ }
11534
+ }
11535
+ if (graph.ranking.candidates.length === 0) {
11536
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
11537
+ }
11538
+ const { pick, earliestReturn } = graph.ranking;
11539
+ if (pick !== void 0) {
11540
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
11541
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
11542
+ }
11543
+ if (earliestReturn === void 0) {
11544
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
11545
+ }
11546
+ const until = new Date(earliestReturn.atMs).toISOString();
11547
+ if (!params.wait) {
11548
+ return {
11549
+ ok: false,
11550
+ message: `agent-shim: every member of pool "${poolName}" is refused right now; the earliest, ${earliestReturn.identity}, returns at ${until} (in ${formatAge(earliestReturn.atMs - nowMs)}). Pass --wait to sleep until then.`
11551
+ };
11552
+ }
11553
+ log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
11554
+ params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
11555
+ }
11556
+ }
11557
+
11399
11558
  // src/launcher/spawn.ts
11400
11559
  var import_node_os5 = __toESM(require("node:os"), 1);
11401
11560
  var SIGNAL_EXIT_CODE_OFFSET = 128;