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.mjs CHANGED
@@ -307,9 +307,14 @@ var POOL_SELECTOR_PREFIX = "pool:";
307
307
  var POOL_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._-]*";
308
308
  var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
309
309
  var PoolNameSchema = z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
310
- var IdentitySelectorSchema = z.string().min(1).regex(new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`));
310
+ var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
311
+ var IdentitySelectorSchema = z.string().min(1).regex(SELECTOR_RE);
312
+ var PoolMemberSchema = z.string().min(1).regex(SELECTOR_RE);
313
+ var POOL_PREFERENCES = ["score", "listed"];
314
+ var PoolPreferenceSchema = z.enum(POOL_PREFERENCES);
311
315
  var PoolSchema = z.strictObject({
312
- identities: z.array(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" })
316
+ identities: z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
317
+ preference: PoolPreferenceSchema.optional()
313
318
  });
314
319
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
315
320
  path: z.string().min(1),
@@ -5352,54 +5357,6 @@ function resolveLayoutPaths() {
5352
5357
  return buildLayoutPaths(resolveAgentShimHome());
5353
5358
  }
5354
5359
 
5355
- // src/usage/preflight.ts
5356
- var MS_PER_SECOND2 = 1e3;
5357
- var SECONDS_PER_MINUTE = 60;
5358
- var MINUTES_PER_HOUR = 60;
5359
- var HOURS_PER_DAY = 24;
5360
- var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
5361
- function formatAge(ms) {
5362
- const minutes = Math.floor(ms / (MS_PER_SECOND2 * SECONDS_PER_MINUTE));
5363
- if (minutes < MINUTES_PER_HOUR) {
5364
- return `${String(Math.max(minutes, 0))}m`;
5365
- }
5366
- const hours = Math.floor(minutes / MINUTES_PER_HOUR);
5367
- return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
5368
- }
5369
- function effectiveWindow(window, nowMs) {
5370
- const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
5371
- if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
5372
- return { reset: true, utilization: 0 };
5373
- }
5374
- return {
5375
- reset: false,
5376
- ...window.utilization === void 0 ? {} : { utilization: window.utilization },
5377
- ...resetsAtMs === void 0 ? {} : { resetsAtMs },
5378
- ...window.status === void 0 ? {} : { status: window.status }
5379
- };
5380
- }
5381
- function quotaWarnings(snapshot, provider, nowMs) {
5382
- const rateLimit = snapshot?.providers[provider]?.rateLimit;
5383
- const unified = rateLimit?.unified;
5384
- if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
5385
- return [];
5386
- }
5387
- const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
5388
- const windows = [
5389
- { name: "five-hour", window: unified.fiveHour },
5390
- { name: "seven-day", window: unified.sevenDay }
5391
- ];
5392
- return windows.flatMap(({ name, window }) => {
5393
- const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
5394
- if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
5395
- return [];
5396
- }
5397
- const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
5398
- const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
5399
- return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
5400
- });
5401
- }
5402
-
5403
5360
  // src/usage/poolPick.ts
5404
5361
  import path17 from "node:path";
5405
5362
  import { z as z9 } from "zod";
@@ -5646,13 +5603,13 @@ function planOf(account) {
5646
5603
  }
5647
5604
 
5648
5605
  // src/usage/rateLimit.ts
5649
- var MS_PER_SECOND3 = 1e3;
5606
+ var MS_PER_SECOND2 = 1e3;
5650
5607
  function epochSecondsToIso(value) {
5651
5608
  if (value === void 0) {
5652
5609
  return void 0;
5653
5610
  }
5654
5611
  const seconds = Number(value);
5655
- return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND3).toISOString() : void 0;
5612
+ return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * MS_PER_SECOND2).toISOString() : void 0;
5656
5613
  }
5657
5614
  function nonNegativeNumber(value) {
5658
5615
  if (value === void 0 || value.trim() === "") {
@@ -5708,6 +5665,54 @@ var ErrorObjectSchema = z8.looseObject({
5708
5665
  // src/usage/middleware.ts
5709
5666
  var ANTHROPIC_PROVIDER = "anthropic";
5710
5667
 
5668
+ // src/usage/preflight.ts
5669
+ var MS_PER_SECOND3 = 1e3;
5670
+ var SECONDS_PER_MINUTE = 60;
5671
+ var MINUTES_PER_HOUR = 60;
5672
+ var HOURS_PER_DAY = 24;
5673
+ var WARNING_STATUSES = /* @__PURE__ */ new Set(["allowed_warning", "rejected"]);
5674
+ function formatAge(ms) {
5675
+ const minutes = Math.floor(ms / (MS_PER_SECOND3 * SECONDS_PER_MINUTE));
5676
+ if (minutes < MINUTES_PER_HOUR) {
5677
+ return `${String(Math.max(minutes, 0))}m`;
5678
+ }
5679
+ const hours = Math.floor(minutes / MINUTES_PER_HOUR);
5680
+ return hours < HOURS_PER_DAY ? `${String(hours)}h` : `${String(Math.floor(hours / HOURS_PER_DAY))}d`;
5681
+ }
5682
+ function effectiveWindow(window, nowMs) {
5683
+ const resetsAtMs = window.resetsAt === void 0 ? void 0 : Date.parse(window.resetsAt);
5684
+ if (resetsAtMs !== void 0 && resetsAtMs <= nowMs) {
5685
+ return { reset: true, utilization: 0 };
5686
+ }
5687
+ return {
5688
+ reset: false,
5689
+ ...window.utilization === void 0 ? {} : { utilization: window.utilization },
5690
+ ...resetsAtMs === void 0 ? {} : { resetsAtMs },
5691
+ ...window.status === void 0 ? {} : { status: window.status }
5692
+ };
5693
+ }
5694
+ function quotaWarnings(snapshot, provider, nowMs) {
5695
+ const rateLimit = snapshot?.providers[provider]?.rateLimit;
5696
+ const unified = rateLimit?.unified;
5697
+ if (snapshot === void 0 || rateLimit === void 0 || unified === void 0) {
5698
+ return [];
5699
+ }
5700
+ const age = formatAge(nowMs - Date.parse(rateLimit.observedAt));
5701
+ const windows = [
5702
+ { name: "five-hour", window: unified.fiveHour },
5703
+ { name: "seven-day", window: unified.sevenDay }
5704
+ ];
5705
+ return windows.flatMap(({ name, window }) => {
5706
+ const effective = window === void 0 ? void 0 : effectiveWindow(window, nowMs);
5707
+ if (window === void 0 || effective?.status === void 0 || !WARNING_STATUSES.has(effective.status)) {
5708
+ return [];
5709
+ }
5710
+ const reset = window.resetsAt === void 0 ? "" : `, resets ${window.resetsAt}`;
5711
+ const verdict = effective.status === "rejected" ? "exhausted" : "nearly used";
5712
+ return [`agent-shim: identity ${snapshot.identity}: the ${name} quota is ${verdict}${reset} (last seen ${age} ago)`];
5713
+ });
5714
+ }
5715
+
5711
5716
  // src/usage/pick.ts
5712
5717
  var MS_PER_SECOND4 = 1e3;
5713
5718
  var MS_PER_HOUR = 36e5;
@@ -5769,7 +5774,30 @@ function describeWindow(label, window, effective, nowMs) {
5769
5774
  function capacityOf(plan) {
5770
5775
  return plan.kind === "subscription" ? plan.capacity : 1;
5771
5776
  }
5777
+ function assessRefusedNested(member, nested, nowMs) {
5778
+ 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 };
5779
+ return {
5780
+ candidate: {
5781
+ identity: member.identity,
5782
+ class: "ineligible",
5783
+ ...refusal.blockedUntilMs === void 0 ? {} : { blockedUntilMs: refusal.blockedUntilMs },
5784
+ plan: planOf(member.account),
5785
+ reasons: [refusal.reason]
5786
+ }
5787
+ };
5788
+ }
5772
5789
  function assess(member, nowMs) {
5790
+ const nested = member.nested;
5791
+ if (nested === void 0) {
5792
+ return assessIdentity(member, nowMs);
5793
+ }
5794
+ if (nested.kind === "refused") {
5795
+ return assessRefusedNested(member, nested, nowMs);
5796
+ }
5797
+ const assessment = assessIdentity(member, nowMs);
5798
+ return { ...assessment, candidate: { ...assessment.candidate, reasons: [`picked by pool "${nested.pool}": ${nested.reasons.join("; ")}`, ...assessment.candidate.reasons] } };
5799
+ }
5800
+ function assessIdentity(member, nowMs) {
5773
5801
  const plan = planOf(member.account);
5774
5802
  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`];
5775
5803
  const base = { identity: member.identity, plan };
@@ -5836,7 +5864,7 @@ function rankPool(input) {
5836
5864
  }
5837
5865
  return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
5838
5866
  });
5839
- const ordered = [...candidates].sort(
5867
+ const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
5840
5868
  (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)
5841
5869
  );
5842
5870
  const sticky = input.sticky;
@@ -5964,6 +5992,12 @@ var DAYS_IN_WEEK = 7;
5964
5992
  var USAGE_RETENTION_MS = DAYS_IN_WEEK * DAY_MS2;
5965
5993
 
5966
5994
  // src/usage/poolPick.ts
5995
+ var PoolGraphError = class extends CliError {
5996
+ constructor(message) {
5997
+ super(message);
5998
+ this.name = "PoolGraphError";
5999
+ }
6000
+ };
5967
6001
  var SCHEMA_VERSION = 1;
5968
6002
  var PickEntrySchema = z9.strictObject({ identity: z9.string().min(1), at: z9.iso.datetime() });
5969
6003
  var PicksFileSchema = z9.strictObject({
@@ -5990,13 +6024,63 @@ function loadPoolMembers(fs12, paths, identities, nowMs) {
5990
6024
  }
5991
6025
  });
5992
6026
  }
5993
- function rankPoolFromStore(params) {
5994
- return rankPool({
5995
- members: loadPoolMembers(params.fs, params.paths, params.identities, params.nowMs),
5996
- nowMs: params.nowMs,
5997
- resuming: params.resuming,
5998
- ...params.sticky === void 0 ? {} : { sticky: params.sticky }
6027
+ function rankPoolGraph(input) {
6028
+ return rankNamedPool(input, input.poolName, [], input.sticky);
6029
+ }
6030
+ function rankNamedPool(input, poolName, stack, sticky) {
6031
+ const pool = input.pools[poolName];
6032
+ if (pool === void 0) {
6033
+ throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6034
+ }
6035
+ const childStack = [...stack, poolName];
6036
+ const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
6037
+ const missing = direct.filter((entry) => !input.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
6038
+ const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
6039
+ const members = [];
6040
+ for (const entry of pool.identities) {
6041
+ const nestedName = poolNameOf(entry);
6042
+ if (nestedName === void 0) {
6043
+ const member = loaded.get(entry);
6044
+ if (member !== void 0) {
6045
+ members.push(member);
6046
+ }
6047
+ continue;
6048
+ }
6049
+ if (input.pools[nestedName] === void 0) {
6050
+ throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
6051
+ }
6052
+ if (childStack.includes(nestedName)) {
6053
+ 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.`);
6054
+ }
6055
+ const nested = nestedMember(input, nestedName, childStack, sticky);
6056
+ members.push(nested.member);
6057
+ missing.push(...nested.missing);
6058
+ }
6059
+ const ranking = rankPool({
6060
+ members,
6061
+ nowMs: input.nowMs,
6062
+ resuming: input.resuming,
6063
+ ...sticky === void 0 ? {} : { sticky },
6064
+ ...pool.preference === void 0 ? {} : { preference: pool.preference }
5999
6065
  });
6066
+ return { ranking, missing };
6067
+ }
6068
+ function nestedMember(input, nestedName, childStack, sticky) {
6069
+ const nested = rankNamedPool(input, nestedName, childStack, sticky);
6070
+ const pick = nested.ranking.pick;
6071
+ if (pick !== void 0) {
6072
+ const [member] = loadPoolMembers(input.fs, input.paths, [pick.identity], input.nowMs);
6073
+ return { member: { ...member ?? { identity: pick.identity, records: [] }, nested: { kind: "pick", pool: nestedName, reasons: pick.reasons } }, missing: nested.missing };
6074
+ }
6075
+ const earliestReturn = nested.ranking.earliestReturn;
6076
+ return {
6077
+ member: {
6078
+ identity: earliestReturn?.identity ?? `pool:${nestedName}`,
6079
+ records: [],
6080
+ nested: { kind: "refused", pool: nestedName, ...earliestReturn === void 0 ? {} : { earliestReturn } }
6081
+ },
6082
+ missing: nested.missing
6083
+ };
6000
6084
  }
6001
6085
  function readPicksFile(fs12, file) {
6002
6086
  const empty = { schemaVersion: SCHEMA_VERSION, picks: {} };
@@ -6027,59 +6111,6 @@ function recordStickyPick(fs12, picksFile, directory, identity, nowMs) {
6027
6111
  `);
6028
6112
  }
6029
6113
 
6030
- // src/launcher/pool.ts
6031
- var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
6032
- var TERMINATOR = "--";
6033
- var REASONS_LOGGED = 3;
6034
- function splitMembers(pool, paths, fs12) {
6035
- const exists = (name) => loadIdentity(paths.identitiesDir, name, fs12) !== void 0;
6036
- return { present: pool.identities.filter(exists), missing: pool.identities.filter((name) => !exists(name)) };
6037
- }
6038
- function isResuming(passthrough) {
6039
- const end = passthrough.indexOf(TERMINATOR);
6040
- return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
6041
- }
6042
- function resolvePoolLaunch(params) {
6043
- const { poolName, log } = params;
6044
- const pool = params.pools?.[poolName];
6045
- if (pool === void 0) {
6046
- return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
6047
- }
6048
- const { present: identities, missing } = splitMembers(pool, params.paths, params.fs);
6049
- for (const name of missing) {
6050
- log.warn(`agent-shim: pool "${poolName}" names identity "${name}", which does not exist; skipping it`);
6051
- }
6052
- if (identities.length === 0) {
6053
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
6054
- }
6055
- const resuming = isResuming(params.passthrough);
6056
- const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
6057
- if (stickyRead.problem !== void 0) {
6058
- log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
6059
- }
6060
- for (; ; ) {
6061
- const nowMs = params.now();
6062
- const ranking = rankPoolFromStore({ fs: params.usageFs, paths: params.paths, identities, nowMs, resuming, ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky } });
6063
- const { pick, earliestReturn } = ranking;
6064
- if (pick !== void 0) {
6065
- recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
6066
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
6067
- }
6068
- if (earliestReturn === void 0) {
6069
- return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
6070
- }
6071
- const until = new Date(earliestReturn.atMs).toISOString();
6072
- if (!params.wait) {
6073
- return {
6074
- ok: false,
6075
- 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.`
6076
- };
6077
- }
6078
- log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
6079
- params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
6080
- }
6081
- }
6082
-
6083
6114
  // src/poolPickReport.ts
6084
6115
  function candidateView(candidate) {
6085
6116
  return {
@@ -6093,14 +6124,15 @@ function candidateView(candidate) {
6093
6124
  };
6094
6125
  }
6095
6126
  function collectPoolPick(params) {
6096
- const { present, missing } = splitMembers(params.pool, params.paths, params.fs);
6097
6127
  const sticky = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.directory);
6098
- const ranking = rankPoolFromStore({
6128
+ const { ranking, missing } = rankPoolGraph({
6099
6129
  fs: params.usageFs,
6100
6130
  paths: params.paths,
6101
- identities: present,
6131
+ pools: params.pools,
6132
+ poolName: params.poolName,
6102
6133
  nowMs: params.nowMs,
6103
6134
  resuming: false,
6135
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
6104
6136
  ...sticky.sticky === void 0 ? {} : { sticky: sticky.sticky }
6105
6137
  });
6106
6138
  return {
@@ -6108,7 +6140,7 @@ function collectPoolPick(params) {
6108
6140
  directory: params.directory,
6109
6141
  ...ranking.pick === void 0 ? {} : { pick: ranking.pick.identity },
6110
6142
  candidates: ranking.candidates.map(candidateView),
6111
- missing: [...missing],
6143
+ missing: missing.map(({ identity }) => identity),
6112
6144
  ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6113
6145
  ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
6114
6146
  };
@@ -6278,7 +6310,41 @@ function requirePool(paths, name) {
6278
6310
  function writePools(paths, pools) {
6279
6311
  applyPatch(paths.globalConfigFile, GlobalConfigSchema, { pools }, { defaults: {} });
6280
6312
  }
6281
- function addPool(paths, name, identities) {
6313
+ function poolCycleOf(pools, name) {
6314
+ const walk = (current, path32) => {
6315
+ for (const entry of pools[current]?.identities ?? []) {
6316
+ const nested = poolNameOf(entry);
6317
+ if (nested === void 0) {
6318
+ continue;
6319
+ }
6320
+ if (nested === name) {
6321
+ return [...path32, nested];
6322
+ }
6323
+ if (path32.includes(nested)) {
6324
+ continue;
6325
+ }
6326
+ const found = walk(nested, [...path32, nested]);
6327
+ if (found !== void 0) {
6328
+ return found;
6329
+ }
6330
+ }
6331
+ return void 0;
6332
+ };
6333
+ return walk(name, [name]);
6334
+ }
6335
+ function validateMembers(pools, name, members) {
6336
+ for (const entry of members) {
6337
+ const nested = poolNameOf(entry);
6338
+ if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
6339
+ throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
6340
+ }
6341
+ }
6342
+ const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
6343
+ if (cycle !== void 0) {
6344
+ 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.`);
6345
+ }
6346
+ }
6347
+ function addPool(paths, name, identities, preference) {
6282
6348
  if (!PoolNameSchema.safeParse(name).success) {
6283
6349
  throw new InvalidPoolNameError(name);
6284
6350
  }
@@ -6286,13 +6352,15 @@ function addPool(paths, name, identities) {
6286
6352
  if (pools[name] !== void 0) {
6287
6353
  throw new PoolAlreadyExistsError(name);
6288
6354
  }
6289
- const pool = { identities: [...identities] };
6355
+ validateMembers(pools, name, identities);
6356
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
6290
6357
  writePools(paths, { ...pools, [name]: pool });
6291
6358
  return pool;
6292
6359
  }
6293
- function setPool(paths, name, identities) {
6360
+ function setPool(paths, name, identities, preference) {
6294
6361
  requirePool(paths, name);
6295
- const pool = { identities: [...identities] };
6362
+ validateMembers(readPools(paths), name, identities);
6363
+ const pool = { identities: [...identities], ...preference === void 0 ? {} : { preference } };
6296
6364
  writePools(paths, { ...readPools(paths), [name]: pool });
6297
6365
  return pool;
6298
6366
  }
@@ -8203,11 +8271,11 @@ function collectCheckReport(params) {
8203
8271
  });
8204
8272
  let poolPick;
8205
8273
  if (decidedIdentity.pool !== void 0) {
8206
- const pool = loaded.globalConfig?.pools?.[decidedIdentity.pool];
8207
- if (pool === void 0) {
8274
+ const pools = loaded.globalConfig?.pools ?? {};
8275
+ if (pools[decidedIdentity.pool] === void 0) {
8208
8276
  throw new PoolNotFoundError(decidedIdentity.pool);
8209
8277
  }
8210
- poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pool, directory: cwd, nowMs: Date.now() });
8278
+ poolPick = collectPoolPick({ paths, fs: realFsPort, usageFs: realFarmFs, poolName: decidedIdentity.pool, pools, directory: cwd, nowMs: Date.now() });
8211
8279
  }
8212
8280
  const identityDecision = poolPick?.pick === void 0 ? decidedIdentity : { ...decidedIdentity, name: poolPick.pick };
8213
8281
  const loadedIdentity = identityDecision.name === void 0 ? void 0 : loadIdentity(paths.identitiesDir, identityDecision.name, realFsPort);
@@ -8341,7 +8409,7 @@ var PoolPickReportSchema = z13.strictObject({
8341
8409
  /** The member a launch would run as, absent when every member is refused. */
8342
8410
  pick: z13.string().optional(),
8343
8411
  candidates: z13.array(PoolPickCandidateViewSchema),
8344
- /** Members that name an identity that does not exist; a launch skips them. */
8412
+ /** Identity members anywhere in the pool's graph that do not exist; a launch skips them. */
8345
8413
  missing: z13.array(z13.string()),
8346
8414
  /** When nothing can be picked, the soonest any member returns. */
8347
8415
  earliestReturn: z13.strictObject({ identity: z13.string(), at: z13.iso.datetime() }).optional(),
@@ -8971,12 +9039,33 @@ function runDoctor(params) {
8971
9039
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
8972
9040
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
8973
9041
  for (const [poolName, pool] of Object.entries(pools)) {
8974
- const missing = pool.identities.filter((member) => !validIdentityNames.has(member));
8975
- if (missing.length === 0) {
9042
+ const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
9043
+ const undefinedPools = pool.identities.flatMap((member) => {
9044
+ const nested = poolNameOf(member);
9045
+ return nested === void 0 || nested in pools ? [] : [nested];
9046
+ });
9047
+ if (missing.length === 0 && undefinedPools.length === 0) {
8976
9048
  push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
8977
9049
  } else {
8978
- push("pool", "fail", `Pool "${poolName}" names ${missing.map((member) => `identity "${member}"`).join(" and ")}, which ${missing.length === 1 ? "does" : "do"} not exist.`, poolName);
9050
+ const problems = [
9051
+ ...missing.map((member) => `identity "${member}", which does not exist`),
9052
+ ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
9053
+ ];
9054
+ push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
9055
+ }
9056
+ }
9057
+ const reportedCycles = /* @__PURE__ */ new Set();
9058
+ for (const poolName of Object.keys(pools).sort()) {
9059
+ const chain = poolCycleOf(pools, poolName);
9060
+ if (chain === void 0) {
9061
+ continue;
9062
+ }
9063
+ const key = [...chain].slice(0, -1).sort().join(">");
9064
+ if (reportedCycles.has(key)) {
9065
+ continue;
8979
9066
  }
9067
+ reportedCycles.add(key);
9068
+ push("pool", "fail", `Pool cycle: ${chain.join(" -> ")}. A pool cannot nest itself, directly or through another pool.`, chain[0]);
8980
9069
  }
8981
9070
  const selectionExists = (selector) => {
8982
9071
  const poolName = poolNameOf(selector);
@@ -10779,7 +10868,7 @@ var LAUNCHER_FLAG_NAMES = [
10779
10868
  ...BOOLEAN_FLAGS.flatMap(({ flag }) => [flag, `--no-${flag.slice(2)}`])
10780
10869
  ];
10781
10870
  var NATIVE_FLAG = "--native";
10782
- var TERMINATOR2 = "--";
10871
+ var TERMINATOR = "--";
10783
10872
  function matchValuedFlag(token) {
10784
10873
  for (const flag of VALUED_FLAGS) {
10785
10874
  if (token === flag) {
@@ -10821,7 +10910,7 @@ function parseLauncherArgv(argv) {
10821
10910
  if (token === void 0) {
10822
10911
  continue;
10823
10912
  }
10824
- if (token === TERMINATOR2) {
10913
+ if (token === TERMINATOR) {
10825
10914
  rest.push(...remaining.slice(index));
10826
10915
  break;
10827
10916
  }
@@ -10848,7 +10937,7 @@ function parseLauncherArgv(argv) {
10848
10937
  value = matched.inlineValue;
10849
10938
  } else {
10850
10939
  const next = remaining[index + 1];
10851
- if (next === void 0 || next === TERMINATOR2) {
10940
+ if (next === void 0 || next === TERMINATOR) {
10852
10941
  rest.push(token);
10853
10942
  continue;
10854
10943
  }
@@ -11132,6 +11221,76 @@ function splitExtraFlags(value) {
11132
11221
  return trimmed.split(/\s+/);
11133
11222
  }
11134
11223
 
11224
+ // src/launcher/pool.ts
11225
+ var RESUME_FLAGS = /* @__PURE__ */ new Set(["--continue", "-c", "--resume", "-r"]);
11226
+ var TERMINATOR2 = "--";
11227
+ var REASONS_LOGGED = 3;
11228
+ function isResuming(passthrough) {
11229
+ const end = passthrough.indexOf(TERMINATOR2);
11230
+ return (end === -1 ? passthrough : passthrough.slice(0, end)).some((token) => RESUME_FLAGS.has(token));
11231
+ }
11232
+ function resolvePoolLaunch(params) {
11233
+ const { poolName, log } = params;
11234
+ const pool = params.pools?.[poolName];
11235
+ if (pool === void 0) {
11236
+ return { ok: false, message: `agent-shim: no pool named "${poolName}" (selected via ${params.selectedVia}). Run \`agent-shim pool add ${poolName} --identity <name>...\` first.` };
11237
+ }
11238
+ const resuming = isResuming(params.passthrough);
11239
+ const stickyRead = readStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd);
11240
+ if (stickyRead.problem !== void 0) {
11241
+ log.warn(`agent-shim: ${stickyRead.problem}; ignoring the last-pick record`);
11242
+ }
11243
+ const warned = /* @__PURE__ */ new Set();
11244
+ for (; ; ) {
11245
+ const nowMs = params.now();
11246
+ let graph;
11247
+ try {
11248
+ graph = rankPoolGraph({
11249
+ fs: params.usageFs,
11250
+ paths: params.paths,
11251
+ pools: params.pools ?? {},
11252
+ poolName,
11253
+ nowMs,
11254
+ resuming,
11255
+ identityExists: (name) => loadIdentity(params.paths.identitiesDir, name, params.fs) !== void 0,
11256
+ ...stickyRead.sticky === void 0 ? {} : { sticky: stickyRead.sticky }
11257
+ });
11258
+ } catch (error) {
11259
+ if (error instanceof PoolGraphError) {
11260
+ return { ok: false, message: `agent-shim: ${error.message}` };
11261
+ }
11262
+ throw error;
11263
+ }
11264
+ for (const gone of graph.missing) {
11265
+ const key = `${gone.pool}:${gone.identity}`;
11266
+ if (!warned.has(key)) {
11267
+ warned.add(key);
11268
+ log.warn(`agent-shim: pool "${gone.pool}" names identity "${gone.identity}", which does not exist; skipping it`);
11269
+ }
11270
+ }
11271
+ if (graph.ranking.candidates.length === 0) {
11272
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
11273
+ }
11274
+ const { pick, earliestReturn } = graph.ranking;
11275
+ if (pick !== void 0) {
11276
+ recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
11277
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
11278
+ }
11279
+ if (earliestReturn === void 0) {
11280
+ return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
11281
+ }
11282
+ const until = new Date(earliestReturn.atMs).toISOString();
11283
+ if (!params.wait) {
11284
+ return {
11285
+ ok: false,
11286
+ 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.`
11287
+ };
11288
+ }
11289
+ log.info(`agent-shim: every member of pool "${poolName}" is refused; waiting until ${until} for ${earliestReturn.identity}`);
11290
+ params.sleep(Math.max(earliestReturn.atMs - nowMs, 0));
11291
+ }
11292
+ }
11293
+
11135
11294
  // src/launcher/spawn.ts
11136
11295
  import os6 from "node:os";
11137
11296
  var SIGNAL_EXIT_CODE_OFFSET = 128;