agent-shim 8.33.0 → 8.35.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/cli.cjs CHANGED
@@ -18047,7 +18047,7 @@ var require_api_pipeline = __commonJS({
18047
18047
  var {
18048
18048
  Readable: Readable2,
18049
18049
  Duplex,
18050
- PassThrough
18050
+ PassThrough: PassThrough2
18051
18051
  } = require("node:stream");
18052
18052
  var assert2 = require("node:assert");
18053
18053
  var { AsyncResource } = require("node:async_hooks");
@@ -18234,7 +18234,7 @@ var require_api_pipeline = __commonJS({
18234
18234
  this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler);
18235
18235
  return pipelineHandler.ret;
18236
18236
  } catch (err) {
18237
- return new PassThrough().destroy(err);
18237
+ return new PassThrough2().destroy(err);
18238
18238
  }
18239
18239
  }
18240
18240
  module2.exports = pipeline;
@@ -57101,13 +57101,21 @@ var CodexProviderConfigSchema = external_exports.strictObject({
57101
57101
  /** Which login the upstream call is made with; see `CODEX_LOGINS`. */
57102
57102
  login: external_exports.enum(CODEX_LOGINS).optional()
57103
57103
  });
57104
+ var ProviderRouteSchema = external_exports.strictObject({
57105
+ /** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
57106
+ when: WhenSchema,
57107
+ /** The provider a matching request is sent to, by the name a provider file carries. */
57108
+ provider: external_exports.string().min(1)
57109
+ });
57104
57110
  var HttpProviderSchema = external_exports.strictObject({
57105
57111
  $schema: external_exports.string().optional(),
57106
57112
  kind: external_exports.literal("http").optional(),
57107
57113
  displayName: external_exports.string().min(1),
57108
57114
  baseUrl: external_exports.url(),
57109
57115
  credential: ProviderCredentialSchema,
57110
- env: ProviderEnvSchema.optional()
57116
+ env: ProviderEnvSchema.optional(),
57117
+ /** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
57118
+ routes: external_exports.array(ProviderRouteSchema).optional()
57111
57119
  });
57112
57120
  var CodexProviderSchema = external_exports.strictObject({
57113
57121
  $schema: external_exports.string().optional(),
@@ -57135,11 +57143,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
57135
57143
  var PoolNameSchema = external_exports.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
57136
57144
  var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
57137
57145
  var IdentitySelectorSchema = external_exports.string().min(1).regex(SELECTOR_RE);
57138
- var PoolMemberSchema = external_exports.string().min(1).regex(SELECTOR_RE);
57146
+ var PoolMemberSchema = external_exports.union([external_exports.string().min(1).regex(SELECTOR_RE), external_exports.strictObject({ identity: external_exports.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
57147
+ function poolMemberIdentity(member) {
57148
+ return typeof member === "string" ? member : member.identity;
57149
+ }
57139
57150
  var POOL_PREFERENCES = ["score", "listed"];
57140
57151
  var PoolPreferenceSchema = external_exports.enum(POOL_PREFERENCES);
57141
57152
  var PoolSchema = external_exports.strictObject({
57142
- identities: external_exports.array(PoolMemberSchema).min(1).refine((members2) => new Set(members2).size === members2.length, { message: "pool members must be unique" }),
57153
+ identities: external_exports.array(PoolMemberSchema).min(1).refine((members2) => new Set(members2.map(poolMemberIdentity)).size === members2.length, { message: "pool members must be unique" }),
57143
57154
  preference: PoolPreferenceSchema.optional()
57144
57155
  });
57145
57156
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
@@ -60153,7 +60164,7 @@ function writePools(paths, pools) {
60153
60164
  function poolCycleOf(pools, name) {
60154
60165
  const walk = (current, path46) => {
60155
60166
  for (const entry of pools[current]?.identities ?? []) {
60156
- const nested = poolNameOf(entry);
60167
+ const nested = poolNameOf(poolMemberIdentity(entry));
60157
60168
  if (nested === void 0) {
60158
60169
  continue;
60159
60170
  }
@@ -60174,9 +60185,10 @@ function poolCycleOf(pools, name) {
60174
60185
  }
60175
60186
  function validateMembers(pools, name, members2) {
60176
60187
  for (const entry of members2) {
60177
- const nested = poolNameOf(entry);
60188
+ const identity = poolMemberIdentity(entry);
60189
+ const nested = poolNameOf(identity);
60178
60190
  if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
60179
- throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
60191
+ throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
60180
60192
  }
60181
60193
  }
60182
60194
  const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members2] } }, name);
@@ -60915,12 +60927,20 @@ function parseDuration(value2) {
60915
60927
  }
60916
60928
  var evaluator = createSyncEvaluator({});
60917
60929
  function resolversOf(context) {
60930
+ const pool = context.pool;
60918
60931
  const values = {
60919
60932
  now: context.nowMs,
60920
60933
  ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
60921
60934
  ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
60922
60935
  ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
60923
- "repo.detached": context.branchDetached ?? false
60936
+ "repo.detached": context.branchDetached ?? false,
60937
+ ...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
60938
+ ...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
60939
+ ...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
60940
+ ...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
60941
+ ...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
60942
+ ...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
60943
+ ...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
60924
60944
  };
60925
60945
  return {
60926
60946
  resolveValue: (key) => {
@@ -63655,7 +63675,7 @@ function registerIdentityCommand(program2, deps) {
63655
63675
  // package.json
63656
63676
  var package_default = {
63657
63677
  name: "agent-shim",
63658
- version: "8.33.0",
63678
+ version: "8.35.0",
63659
63679
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
63660
63680
  license: "MIT",
63661
63681
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -64744,12 +64764,48 @@ function assessIdentity(member, nowMs) {
64744
64764
  ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
64745
64765
  };
64746
64766
  }
64767
+ function poolFactsOf(member, nowMs, resuming, sticky) {
64768
+ const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
64769
+ const window = (which) => {
64770
+ const raw = unified?.[which];
64771
+ if (raw?.utilization === void 0) {
64772
+ return void 0;
64773
+ }
64774
+ if (raw.resetsAt === void 0) {
64775
+ return void 0;
64776
+ }
64777
+ return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
64778
+ };
64779
+ const burn = windowBurn(member.records, "fiveHour");
64780
+ return {
64781
+ ...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
64782
+ ...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
64783
+ ...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
64784
+ resuming,
64785
+ ...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
64786
+ };
64787
+ }
64747
64788
  function rankPool(input2) {
64748
64789
  const { members: members2, nowMs } = input2;
64749
- const assessments = members2.map((member) => assess(member, nowMs));
64790
+ const assessments = members2.map((member) => ({ member, ...assess(member, nowMs) }));
64750
64791
  const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
64751
64792
  const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
64752
- const candidates = assessments.map((assessment) => {
64793
+ const guarded = assessments.map((assessment) => {
64794
+ const { candidate } = assessment;
64795
+ if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
64796
+ return assessment;
64797
+ }
64798
+ const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input2.resuming, input2.sticky) };
64799
+ const verdict = evaluateWhen(assessment.member.policy, context);
64800
+ if (verdict.status === "indeterminate") {
64801
+ return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
64802
+ }
64803
+ if (verdict.passed) {
64804
+ return assessment;
64805
+ }
64806
+ return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
64807
+ });
64808
+ const candidates = guarded.map((assessment) => {
64753
64809
  const { candidate, fiveHour, burn } = assessment;
64754
64810
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
64755
64811
  const { sevenDay, sevenDayBurn } = assessment;
@@ -64775,7 +64831,7 @@ function rankPool(input2) {
64775
64831
  return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
64776
64832
  });
64777
64833
  const ordered = input2.preference === "listed" ? [...candidates] : [...candidates].sort(
64778
- (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)
64834
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
64779
64835
  );
64780
64836
  const sticky = input2.sticky;
64781
64837
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
@@ -65104,27 +65160,27 @@ function rankNamedPool(input2, poolName, stack, sticky) {
65104
65160
  throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
65105
65161
  }
65106
65162
  const childStack = [...stack, poolName];
65107
- const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
65108
- const missing = direct.filter((entry) => !input2.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
65109
- const loaded = new Map(loadPoolMembers(input2.fs, input2.paths, direct.filter(input2.identityExists), input2.nowMs).map((member) => [member.identity, member]));
65163
+ const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
65164
+ const missing = direct.filter((entry) => !input2.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
65165
+ const loaded = new Map(loadPoolMembers(input2.fs, input2.paths, direct.filter((entry) => input2.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input2.nowMs).map((member) => [member.identity, member]));
65110
65166
  const members2 = [];
65111
65167
  for (const entry of pool.identities) {
65112
- const nestedName = poolNameOf(entry);
65168
+ const nestedName = poolNameOf(poolMemberIdentity(entry));
65113
65169
  if (nestedName === void 0) {
65114
- const member = loaded.get(entry);
65170
+ const member = loaded.get(poolMemberIdentity(entry));
65115
65171
  if (member !== void 0) {
65116
- members2.push(member);
65172
+ members2.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
65117
65173
  }
65118
65174
  continue;
65119
65175
  }
65120
65176
  if (input2.pools[nestedName] === void 0) {
65121
- throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
65177
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
65122
65178
  }
65123
65179
  if (childStack.includes(nestedName)) {
65124
- 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.`);
65180
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
65125
65181
  }
65126
65182
  const nested = nestedMember(input2, nestedName, childStack, sticky);
65127
- members2.push(nested.member);
65183
+ members2.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
65128
65184
  missing.push(...nested.missing);
65129
65185
  }
65130
65186
  const ranking = rankPool({
@@ -70139,16 +70195,16 @@ function runDoctor(params) {
70139
70195
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
70140
70196
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
70141
70197
  for (const [poolName, pool] of Object.entries(pools)) {
70142
- const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
70198
+ const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
70143
70199
  const undefinedPools = pool.identities.flatMap((member) => {
70144
- const nested = poolNameOf(member);
70200
+ const nested = poolNameOf(poolMemberIdentity(member));
70145
70201
  return nested === void 0 || nested in pools ? [] : [nested];
70146
70202
  });
70147
70203
  if (missing.length === 0 && undefinedPools.length === 0) {
70148
- push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
70204
+ push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
70149
70205
  } else {
70150
70206
  const problems = [
70151
- ...missing.map((member) => `identity "${member}", which does not exist`),
70207
+ ...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
70152
70208
  ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
70153
70209
  ];
70154
70210
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
@@ -77336,6 +77392,74 @@ function createPassthroughRoute(name, target) {
77336
77392
  };
77337
77393
  }
77338
77394
 
77395
+ // src/frontdoor/requestScan.ts
77396
+ var import_node_stream2 = require("node:stream");
77397
+ var BYTES_PER_KIB = 1024;
77398
+ var SCAN_KIB = 64;
77399
+ var REQUEST_SCAN_CAP_BYTES = SCAN_KIB * BYTES_PER_KIB;
77400
+ async function scanRequestHead(request7) {
77401
+ const body = request7.body;
77402
+ const head = [];
77403
+ let seen = 0;
77404
+ let text2 = "";
77405
+ let decided = false;
77406
+ let ended = false;
77407
+ let error62;
77408
+ const replay = new import_node_stream2.PassThrough();
77409
+ const modelIn = () => /"model"\s*:\s*"([^"]+)"/.exec(text2)?.[1];
77410
+ const settled = new Promise((resolve) => {
77411
+ const settle2 = () => {
77412
+ const model = modelIn();
77413
+ const hasImage = /"type"\s*:\s*"image"/.test(text2);
77414
+ for (const chunk of head) {
77415
+ replay.write(chunk);
77416
+ }
77417
+ resolve({ ...model === void 0 ? {} : { model }, hasImage, replayed: { ...request7, body: replay } });
77418
+ };
77419
+ body.on("data", (chunk) => {
77420
+ if (decided) {
77421
+ if (!replay.write(chunk)) {
77422
+ body.pause();
77423
+ replay.once("drain", () => {
77424
+ body.resume();
77425
+ });
77426
+ }
77427
+ return;
77428
+ }
77429
+ head.push(chunk);
77430
+ seen += chunk.length;
77431
+ text2 += chunk.toString("utf8");
77432
+ if (modelIn() !== void 0 || seen >= REQUEST_SCAN_CAP_BYTES) {
77433
+ decided = true;
77434
+ body.pause();
77435
+ replay.once("resume", () => {
77436
+ body.resume();
77437
+ });
77438
+ settle2();
77439
+ }
77440
+ });
77441
+ body.on("end", () => {
77442
+ ended = true;
77443
+ if (!decided) {
77444
+ decided = true;
77445
+ settle2();
77446
+ }
77447
+ replay.end();
77448
+ });
77449
+ body.on("error", (cause) => {
77450
+ error62 = cause;
77451
+ replay.destroy(cause);
77452
+ });
77453
+ body.on("close", () => {
77454
+ if (!decided && !ended && error62 === void 0) {
77455
+ decided = true;
77456
+ settle2();
77457
+ }
77458
+ });
77459
+ });
77460
+ return await settled;
77461
+ }
77462
+
77339
77463
  // src/frontdoor/providerRoute.ts
77340
77464
  function createProviderRouteResolver(deps) {
77341
77465
  const isRcSelfHostPath = (path46) => path46 === "/v1/code/sessions" || path46.startsWith("/v1/code/sessions/") || path46 === "/v1/sessions" || path46.startsWith("/v1/sessions/");
@@ -77400,7 +77524,64 @@ function createProviderRouteResolver(deps) {
77400
77524
  )
77401
77525
  };
77402
77526
  };
77403
- return async (request7) => await Promise.resolve(resolve(request7));
77527
+ const servingRewritten = (route, url2, body) => ({
77528
+ name: route.name,
77529
+ headroomEligible: route.headroomEligible,
77530
+ headroomUpstream: route.headroomUpstream,
77531
+ // The pipeline serves the route with the request it received; the routing layer's decisions (the rewritten target path, the replayed body) are substituted here, so the route sees exactly the request the resolution acted on.
77532
+ serve: async (request7, response) => {
77533
+ await route.serve({ ...request7, url: url2, body }, response);
77534
+ }
77535
+ });
77536
+ return async (request7) => {
77537
+ let current = request7;
77538
+ let replay;
77539
+ const visited = /* @__PURE__ */ new Set();
77540
+ for (; ; ) {
77541
+ const path46 = new URL(current.url, "http://127.0.0.1").pathname;
77542
+ const scoped = parseProviderPath(path46);
77543
+ const providerName = scoped?.provider ?? current.session.provider;
77544
+ if (providerName !== void 0 && visited.has(providerName)) {
77545
+ return { ok: false, status: HTTP_STATUS.internalServerError, message: `provider routing cycle: ${[...visited, providerName].join(" -> ")}` };
77546
+ }
77547
+ if (providerName !== void 0) {
77548
+ visited.add(providerName);
77549
+ }
77550
+ let routes;
77551
+ try {
77552
+ const definition = providerName === void 0 ? void 0 : loadProvider(deps.providersDir, providerName, deps.fs);
77553
+ routes = definition === void 0 || isCodexProvider(definition) ? void 0 : definition.routes;
77554
+ } catch {
77555
+ routes = void 0;
77556
+ }
77557
+ if (routes === void 0 || routes.length === 0) {
77558
+ const resolution = resolve(current);
77559
+ return resolution.ok && replay !== void 0 ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
77560
+ }
77561
+ const scan = await scanRequestHead(current);
77562
+ if (scan.replayed === void 0) {
77563
+ const resolution = resolve(current);
77564
+ return resolution.ok && replay !== void 0 ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
77565
+ }
77566
+ replay = scan.replayed;
77567
+ const context = { nowMs: Date.now(), env: deps.env, ...scan.model === void 0 && scan.hasImage === void 0 ? {} : { request: { ...scan.model === void 0 ? {} : { model: scan.model }, hasImage: scan.hasImage } } };
77568
+ let matched;
77569
+ for (const entry of routes) {
77570
+ const verdict = evaluateWhen(entry.when, context);
77571
+ if (verdict.status === "definite" && verdict.passed) {
77572
+ matched = entry.provider;
77573
+ break;
77574
+ }
77575
+ }
77576
+ if (matched === void 0) {
77577
+ const resolution = resolve(current);
77578
+ return resolution.ok ? { ok: true, route: servingRewritten(resolution.route, current.url, replay.body) } : resolution;
77579
+ }
77580
+ const rest = scoped === void 0 ? path46 : path46.slice(`${PROVIDER_PATH_PREFIX}${encodeURIComponent(scoped.provider)}`.length);
77581
+ const query = current.url.includes("?") ? current.url.slice(current.url.indexOf("?")) : "";
77582
+ current = { ...current, url: `${PROVIDER_PATH_PREFIX}${encodeURIComponent(matched)}${rest}${query}`, body: replay.body };
77583
+ }
77584
+ };
77404
77585
  }
77405
77586
  function providerCredentialHeaders(credential) {
77406
77587
  return credential.target === "apiKey" ? { "x-api-key": credential.token } : { authorization: `Bearer ${credential.token}` };
@@ -81780,12 +81961,12 @@ function registerPoolCommand(program2, deps) {
81780
81961
  }
81781
81962
  const members2 = options.identity === void 0 ? existing.identities : membersOf(options);
81782
81963
  if (options.identity !== void 0) {
81783
- requireIdentities(paths, members2);
81964
+ requireIdentities(paths, membersOf(options));
81784
81965
  }
81785
81966
  const preference = options.preference === void 0 ? existing.preference : options.preference === false ? void 0 : options.preference;
81786
81967
  const updated = setPool(paths, name, members2, preference);
81787
81968
  reportMutation(options.json, { action: "updated", kind: "pool", name, value: updated }, () => {
81788
- console.log(`Pool "${name}" now has ${members2.join(", ")}.`);
81969
+ console.log(`Pool "${name}" now has ${members2.map(poolMemberIdentity).join(", ")}.`);
81789
81970
  });
81790
81971
  }),
81791
81972
  ["agent-shim pool set subs --identity work --identity personal --identity spare", "agent-shim pool set subs --preference listed"]
@@ -81805,7 +81986,7 @@ function registerPoolCommand(program2, deps) {
81805
81986
  }
81806
81987
  for (const name of names) {
81807
81988
  const entry = pools[name];
81808
- console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(entry?.identities ?? []).join(", ")}${entry?.preference === void 0 ? "" : " (preference: listed)"}`);
81989
+ console.log(`${active === `${POOL_SELECTOR_PREFIX}${name}` ? "*" : " "} ${name}: ${(entry?.identities ?? []).map(poolMemberIdentity).join(", ")}${entry?.preference === void 0 ? "" : " (preference: listed)"}`);
81809
81990
  }
81810
81991
  }),
81811
81992
  ["agent-shim pool list", "agent-shim pool list --json"]
@@ -81818,7 +81999,7 @@ function registerPoolCommand(program2, deps) {
81818
81999
  return;
81819
82000
  }
81820
82001
  console.log(`Pool: ${name}`);
81821
- console.log(`Members: ${entry.identities.join(", ")}`);
82002
+ console.log(`Members: ${entry.identities.map(poolMemberIdentity).join(", ")}`);
81822
82003
  if (entry.preference !== void 0) {
81823
82004
  console.log(`Preference: ${entry.preference}`);
81824
82005
  }
package/dist/index.cjs CHANGED
@@ -589,13 +589,21 @@ var CodexProviderConfigSchema = import_zod.z.strictObject({
589
589
  /** Which login the upstream call is made with; see `CODEX_LOGINS`. */
590
590
  login: import_zod.z.enum(CODEX_LOGINS).optional()
591
591
  });
592
+ var ProviderRouteSchema = import_zod.z.strictObject({
593
+ /** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
594
+ when: WhenSchema,
595
+ /** The provider a matching request is sent to, by the name a provider file carries. */
596
+ provider: import_zod.z.string().min(1)
597
+ });
592
598
  var HttpProviderSchema = import_zod.z.strictObject({
593
599
  $schema: import_zod.z.string().optional(),
594
600
  kind: import_zod.z.literal("http").optional(),
595
601
  displayName: import_zod.z.string().min(1),
596
602
  baseUrl: import_zod.z.url(),
597
603
  credential: ProviderCredentialSchema,
598
- env: ProviderEnvSchema.optional()
604
+ env: ProviderEnvSchema.optional(),
605
+ /** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
606
+ routes: import_zod.z.array(ProviderRouteSchema).optional()
599
607
  });
600
608
  var CodexProviderSchema = import_zod.z.strictObject({
601
609
  $schema: import_zod.z.string().optional(),
@@ -623,11 +631,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
623
631
  var PoolNameSchema = import_zod.z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
624
632
  var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
625
633
  var IdentitySelectorSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
626
- var PoolMemberSchema = import_zod.z.string().min(1).regex(SELECTOR_RE);
634
+ var PoolMemberSchema = import_zod.z.union([import_zod.z.string().min(1).regex(SELECTOR_RE), import_zod.z.strictObject({ identity: import_zod.z.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
635
+ function poolMemberIdentity(member) {
636
+ return typeof member === "string" ? member : member.identity;
637
+ }
627
638
  var POOL_PREFERENCES = ["score", "listed"];
628
639
  var PoolPreferenceSchema = import_zod.z.enum(POOL_PREFERENCES);
629
640
  var PoolSchema = import_zod.z.strictObject({
630
- identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
641
+ identities: import_zod.z.array(PoolMemberSchema).min(1).refine((members) => new Set(members.map(poolMemberIdentity)).size === members.length, { message: "pool members must be unique" }),
631
642
  preference: PoolPreferenceSchema.optional()
632
643
  });
633
644
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
@@ -1004,12 +1015,20 @@ function matchBranch(pattern, branch, detached = false) {
1004
1015
  }
1005
1016
  var evaluator = (0, import_trilean2.createSyncEvaluator)({});
1006
1017
  function resolversOf(context) {
1018
+ const pool = context.pool;
1007
1019
  const values = {
1008
1020
  now: context.nowMs,
1009
1021
  ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
1010
1022
  ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
1011
1023
  ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
1012
- "repo.detached": context.branchDetached ?? false
1024
+ "repo.detached": context.branchDetached ?? false,
1025
+ ...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
1026
+ ...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
1027
+ ...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
1028
+ ...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
1029
+ ...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
1030
+ ...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
1031
+ ...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
1013
1032
  };
1014
1033
  return {
1015
1034
  resolveValue: (key) => {
@@ -5390,7 +5409,7 @@ var import_undici = require("undici");
5390
5409
  // package.json
5391
5410
  var package_default = {
5392
5411
  name: "agent-shim",
5393
- version: "8.33.0",
5412
+ version: "8.35.0",
5394
5413
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5395
5414
  license: "MIT",
5396
5415
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -6682,12 +6701,48 @@ function assessIdentity(member, nowMs) {
6682
6701
  ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
6683
6702
  };
6684
6703
  }
6704
+ function poolFactsOf(member, nowMs, resuming, sticky) {
6705
+ const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
6706
+ const window = (which) => {
6707
+ const raw = unified?.[which];
6708
+ if (raw?.utilization === void 0) {
6709
+ return void 0;
6710
+ }
6711
+ if (raw.resetsAt === void 0) {
6712
+ return void 0;
6713
+ }
6714
+ return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
6715
+ };
6716
+ const burn = windowBurn(member.records, "fiveHour");
6717
+ return {
6718
+ ...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
6719
+ ...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
6720
+ ...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
6721
+ resuming,
6722
+ ...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
6723
+ };
6724
+ }
6685
6725
  function rankPool(input) {
6686
6726
  const { members, nowMs } = input;
6687
- const assessments = members.map((member) => assess(member, nowMs));
6727
+ const assessments = members.map((member) => ({ member, ...assess(member, nowMs) }));
6688
6728
  const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
6689
6729
  const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
6690
- const candidates = assessments.map((assessment) => {
6730
+ const guarded = assessments.map((assessment) => {
6731
+ const { candidate } = assessment;
6732
+ if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
6733
+ return assessment;
6734
+ }
6735
+ const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input.resuming, input.sticky) };
6736
+ const verdict = evaluateWhen(assessment.member.policy, context);
6737
+ if (verdict.status === "indeterminate") {
6738
+ return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
6739
+ }
6740
+ if (verdict.passed) {
6741
+ return assessment;
6742
+ }
6743
+ return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
6744
+ });
6745
+ const candidates = guarded.map((assessment) => {
6691
6746
  const { candidate, fiveHour, burn } = assessment;
6692
6747
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6693
6748
  const { sevenDay, sevenDayBurn } = assessment;
@@ -6713,7 +6768,7 @@ function rankPool(input) {
6713
6768
  return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
6714
6769
  });
6715
6770
  const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6716
- (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)
6771
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6717
6772
  );
6718
6773
  const sticky = input.sticky;
6719
6774
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
@@ -6884,27 +6939,27 @@ function rankNamedPool(input, poolName, stack, sticky) {
6884
6939
  throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6885
6940
  }
6886
6941
  const childStack = [...stack, poolName];
6887
- const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
6888
- const missing = direct.filter((entry) => !input.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
6889
- const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
6942
+ const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
6943
+ const missing = direct.filter((entry) => !input.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
6944
+ const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter((entry) => input.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input.nowMs).map((member) => [member.identity, member]));
6890
6945
  const members = [];
6891
6946
  for (const entry of pool.identities) {
6892
- const nestedName = poolNameOf(entry);
6947
+ const nestedName = poolNameOf(poolMemberIdentity(entry));
6893
6948
  if (nestedName === void 0) {
6894
- const member = loaded.get(entry);
6949
+ const member = loaded.get(poolMemberIdentity(entry));
6895
6950
  if (member !== void 0) {
6896
- members.push(member);
6951
+ members.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
6897
6952
  }
6898
6953
  continue;
6899
6954
  }
6900
6955
  if (input.pools[nestedName] === void 0) {
6901
- throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
6956
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
6902
6957
  }
6903
6958
  if (childStack.includes(nestedName)) {
6904
- 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.`);
6959
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6905
6960
  }
6906
6961
  const nested = nestedMember(input, nestedName, childStack, sticky);
6907
- members.push(nested.member);
6962
+ members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
6908
6963
  missing.push(...nested.missing);
6909
6964
  }
6910
6965
  const ranking = rankPool({
@@ -7165,7 +7220,7 @@ function writePools(paths, pools) {
7165
7220
  function poolCycleOf(pools, name) {
7166
7221
  const walk = (current, path32) => {
7167
7222
  for (const entry of pools[current]?.identities ?? []) {
7168
- const nested = poolNameOf(entry);
7223
+ const nested = poolNameOf(poolMemberIdentity(entry));
7169
7224
  if (nested === void 0) {
7170
7225
  continue;
7171
7226
  }
@@ -7186,9 +7241,10 @@ function poolCycleOf(pools, name) {
7186
7241
  }
7187
7242
  function validateMembers(pools, name, members) {
7188
7243
  for (const entry of members) {
7189
- const nested = poolNameOf(entry);
7244
+ const identity = poolMemberIdentity(entry);
7245
+ const nested = poolNameOf(identity);
7190
7246
  if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
7191
- throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
7247
+ throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
7192
7248
  }
7193
7249
  }
7194
7250
  const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
@@ -9893,16 +9949,16 @@ function runDoctor(params) {
9893
9949
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
9894
9950
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
9895
9951
  for (const [poolName, pool] of Object.entries(pools)) {
9896
- const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
9952
+ const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
9897
9953
  const undefinedPools = pool.identities.flatMap((member) => {
9898
- const nested = poolNameOf(member);
9954
+ const nested = poolNameOf(poolMemberIdentity(member));
9899
9955
  return nested === void 0 || nested in pools ? [] : [nested];
9900
9956
  });
9901
9957
  if (missing.length === 0 && undefinedPools.length === 0) {
9902
- push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
9958
+ push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
9903
9959
  } else {
9904
9960
  const problems = [
9905
- ...missing.map((member) => `identity "${member}", which does not exist`),
9961
+ ...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
9906
9962
  ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
9907
9963
  ];
9908
9964
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
package/dist/index.mjs CHANGED
@@ -277,13 +277,21 @@ var CodexProviderConfigSchema = z.strictObject({
277
277
  /** Which login the upstream call is made with; see `CODEX_LOGINS`. */
278
278
  login: z.enum(CODEX_LOGINS).optional()
279
279
  });
280
+ var ProviderRouteSchema = z.strictObject({
281
+ /** The condition, the same `when` union a configuration cascade entry takes: the object form or a trilean predicate tree over the request facts the door resolves (`request.model` and kin). */
282
+ when: WhenSchema,
283
+ /** The provider a matching request is sent to, by the name a provider file carries. */
284
+ provider: z.string().min(1)
285
+ });
280
286
  var HttpProviderSchema = z.strictObject({
281
287
  $schema: z.string().optional(),
282
288
  kind: z.literal("http").optional(),
283
289
  displayName: z.string().min(1),
284
290
  baseUrl: z.url(),
285
291
  credential: ProviderCredentialSchema,
286
- env: ProviderEnvSchema.optional()
292
+ env: ProviderEnvSchema.optional(),
293
+ /** Per-request routing: when present, each request this provider would serve is matched against the list in order, and the first definite match is sent to that entry's provider instead (itself resolved exactly as a directly dialled request would be). Unmatched requests stay on this provider. */
294
+ routes: z.array(ProviderRouteSchema).optional()
287
295
  });
288
296
  var CodexProviderSchema = z.strictObject({
289
297
  $schema: z.string().optional(),
@@ -311,11 +319,14 @@ var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
311
319
  var PoolNameSchema = z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
312
320
  var SELECTOR_RE = new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`);
313
321
  var IdentitySelectorSchema = z.string().min(1).regex(SELECTOR_RE);
314
- var PoolMemberSchema = z.string().min(1).regex(SELECTOR_RE);
322
+ var PoolMemberSchema = z.union([z.string().min(1).regex(SELECTOR_RE), z.strictObject({ identity: z.string().min(1).regex(SELECTOR_RE), when: WhenSchema })]);
323
+ function poolMemberIdentity(member) {
324
+ return typeof member === "string" ? member : member.identity;
325
+ }
315
326
  var POOL_PREFERENCES = ["score", "listed"];
316
327
  var PoolPreferenceSchema = z.enum(POOL_PREFERENCES);
317
328
  var PoolSchema = z.strictObject({
318
- identities: z.array(PoolMemberSchema).min(1).refine((members) => new Set(members).size === members.length, { message: "pool members must be unique" }),
329
+ identities: z.array(PoolMemberSchema).min(1).refine((members) => new Set(members.map(poolMemberIdentity)).size === members.length, { message: "pool members must be unique" }),
319
330
  preference: PoolPreferenceSchema.optional()
320
331
  });
321
332
  var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
@@ -692,12 +703,20 @@ function matchBranch(pattern, branch, detached = false) {
692
703
  }
693
704
  var evaluator = createSyncEvaluator({});
694
705
  function resolversOf(context) {
706
+ const pool = context.pool;
695
707
  const values = {
696
708
  now: context.nowMs,
697
709
  ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
698
710
  ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
699
711
  ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
700
- "repo.detached": context.branchDetached ?? false
712
+ "repo.detached": context.branchDetached ?? false,
713
+ ...pool?.fiveHour === void 0 ? {} : { "quota.fiveHour.remaining": pool.fiveHour.remaining, "quota.fiveHour.utilization": pool.fiveHour.utilization, "quota.fiveHour.hoursUntilReset": pool.fiveHour.hoursUntilReset },
714
+ ...pool?.sevenDay === void 0 ? {} : { "quota.sevenDay.remaining": pool.sevenDay.remaining, "quota.sevenDay.utilization": pool.sevenDay.utilization, "quota.sevenDay.hoursUntilReset": pool.sevenDay.hoursUntilReset },
715
+ ...pool?.burnPerHour === void 0 ? {} : { "quota.burnPerHour": pool.burnPerHour },
716
+ ...pool?.resuming === void 0 ? {} : { "session.resuming": pool.resuming },
717
+ ...pool?.lastPickHoursAgo === void 0 ? {} : { "session.lastPickHoursAgo": pool.lastPickHoursAgo },
718
+ ...context.request?.model === void 0 ? {} : { "request.model": context.request.model },
719
+ ...context.request?.hasImage === void 0 ? {} : { "request.hasImage": context.request.hasImage }
701
720
  };
702
721
  return {
703
722
  resolveValue: (key) => {
@@ -5078,7 +5097,7 @@ import { Agent as Agent3, fetch as undiciFetch } from "undici";
5078
5097
  // package.json
5079
5098
  var package_default = {
5080
5099
  name: "agent-shim",
5081
- version: "8.33.0",
5100
+ version: "8.35.0",
5082
5101
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5083
5102
  license: "MIT",
5084
5103
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -6370,12 +6389,48 @@ function assessIdentity(member, nowMs) {
6370
6389
  ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
6371
6390
  };
6372
6391
  }
6392
+ function poolFactsOf(member, nowMs, resuming, sticky) {
6393
+ const unified = member.snapshot?.providers[ANTHROPIC_PROVIDER]?.rateLimit?.unified;
6394
+ const window = (which) => {
6395
+ const raw = unified?.[which];
6396
+ if (raw?.utilization === void 0) {
6397
+ return void 0;
6398
+ }
6399
+ if (raw.resetsAt === void 0) {
6400
+ return void 0;
6401
+ }
6402
+ return { remaining: Math.max(0, 1 - raw.utilization), utilization: raw.utilization, hoursUntilReset: Math.max(0, (Date.parse(raw.resetsAt) - nowMs) / MS_PER_HOUR) };
6403
+ };
6404
+ const burn = windowBurn(member.records, "fiveHour");
6405
+ return {
6406
+ ...window("fiveHour") === void 0 ? {} : { fiveHour: window("fiveHour") },
6407
+ ...window("sevenDay") === void 0 ? {} : { sevenDay: window("sevenDay") },
6408
+ ...burn === void 0 ? {} : { burnPerHour: burn * MS_PER_HOUR },
6409
+ resuming,
6410
+ ...sticky?.identity === member.identity ? { lastPickHoursAgo: Math.max(0, (nowMs - Date.parse(sticky.at)) / MS_PER_HOUR) } : {}
6411
+ };
6412
+ }
6373
6413
  function rankPool(input) {
6374
6414
  const { members, nowMs } = input;
6375
- const assessments = members.map((member) => assess(member, nowMs));
6415
+ const assessments = members.map((member) => ({ member, ...assess(member, nowMs) }));
6376
6416
  const demands = assessments.flatMap(({ candidate, burn }) => burn === void 0 ? [] : [burn * capacityOf(candidate.plan)]);
6377
6417
  const sharedDemand = demands.length === 0 ? void 0 : Math.max(...demands);
6378
- const candidates = assessments.map((assessment) => {
6418
+ const guarded = assessments.map((assessment) => {
6419
+ const { candidate } = assessment;
6420
+ if (candidate.class === "ineligible" || assessment.member.policy === void 0) {
6421
+ return assessment;
6422
+ }
6423
+ const context = { nowMs, env: {}, pool: poolFactsOf(assessment.member, nowMs, input.resuming, input.sticky) };
6424
+ const verdict = evaluateWhen(assessment.member.policy, context);
6425
+ if (verdict.status === "indeterminate") {
6426
+ return { ...assessment, candidate: { ...candidate, policyDemoted: true, reasons: [`policy undecided (${verdict.checked.join(", ") === "predicate" ? "predicate" : verdict.checked.join(", ")}): ${verdict.reason}`, ...candidate.reasons] } };
6427
+ }
6428
+ if (verdict.passed) {
6429
+ return assessment;
6430
+ }
6431
+ return { ...assessment, candidate: { ...candidate, class: "ineligible", reasons: [`skipped by policy (${verdict.checked.join(", ")}: did not hold)`, ...candidate.reasons] } };
6432
+ });
6433
+ const candidates = guarded.map((assessment) => {
6379
6434
  const { candidate, fiveHour, burn } = assessment;
6380
6435
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6381
6436
  const { sevenDay, sevenDayBurn } = assessment;
@@ -6401,7 +6456,7 @@ function rankPool(input) {
6401
6456
  return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
6402
6457
  });
6403
6458
  const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6404
- (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)
6459
+ (left, right) => CLASS_ORDER[left.class] - CLASS_ORDER[right.class] || Number(left.policyDemoted ?? false) - Number(right.policyDemoted ?? false) || Number(right.feasible) - Number(left.feasible) || (right.score ?? 0) - (left.score ?? 0) || left.identity.localeCompare(right.identity)
6405
6460
  );
6406
6461
  const sticky = input.sticky;
6407
6462
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
@@ -6572,27 +6627,27 @@ function rankNamedPool(input, poolName, stack, sticky) {
6572
6627
  throw new PoolGraphError(`No pool named "${poolName}". Run \`agent-shim pool add ${poolName} --identity <name>...\` first.`);
6573
6628
  }
6574
6629
  const childStack = [...stack, poolName];
6575
- const direct = pool.identities.filter((entry) => poolNameOf(entry) === void 0);
6576
- const missing = direct.filter((entry) => !input.identityExists(entry)).map((identity) => ({ pool: poolName, identity }));
6577
- const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter(input.identityExists), input.nowMs).map((member) => [member.identity, member]));
6630
+ const direct = pool.identities.filter((entry) => poolNameOf(poolMemberIdentity(entry)) === void 0);
6631
+ const missing = direct.filter((entry) => !input.identityExists(poolMemberIdentity(entry))).map((entry) => ({ pool: poolName, identity: poolMemberIdentity(entry) }));
6632
+ const loaded = new Map(loadPoolMembers(input.fs, input.paths, direct.filter((entry) => input.identityExists(poolMemberIdentity(entry))).map(poolMemberIdentity), input.nowMs).map((member) => [member.identity, member]));
6578
6633
  const members = [];
6579
6634
  for (const entry of pool.identities) {
6580
- const nestedName = poolNameOf(entry);
6635
+ const nestedName = poolNameOf(poolMemberIdentity(entry));
6581
6636
  if (nestedName === void 0) {
6582
- const member = loaded.get(entry);
6637
+ const member = loaded.get(poolMemberIdentity(entry));
6583
6638
  if (member !== void 0) {
6584
- members.push(member);
6639
+ members.push(typeof entry === "string" ? member : { ...member, policy: entry.when });
6585
6640
  }
6586
6641
  continue;
6587
6642
  }
6588
6643
  if (input.pools[nestedName] === void 0) {
6589
- throw new PoolGraphError(`Pool "${poolName}" member "${entry}" names a pool that is not defined.`);
6644
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" names a pool that is not defined.`);
6590
6645
  }
6591
6646
  if (childStack.includes(nestedName)) {
6592
- 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.`);
6647
+ throw new PoolGraphError(`Pool "${poolName}" member "${poolMemberIdentity(entry)}" closes a cycle: ${[...childStack.slice(childStack.indexOf(nestedName)), nestedName].join(" -> ")}. A pool cannot nest itself, directly or through another pool.`);
6593
6648
  }
6594
6649
  const nested = nestedMember(input, nestedName, childStack, sticky);
6595
- members.push(nested.member);
6650
+ members.push(typeof entry === "string" ? nested.member : { ...nested.member, policy: entry.when });
6596
6651
  missing.push(...nested.missing);
6597
6652
  }
6598
6653
  const ranking = rankPool({
@@ -6853,7 +6908,7 @@ function writePools(paths, pools) {
6853
6908
  function poolCycleOf(pools, name) {
6854
6909
  const walk = (current, path32) => {
6855
6910
  for (const entry of pools[current]?.identities ?? []) {
6856
- const nested = poolNameOf(entry);
6911
+ const nested = poolNameOf(poolMemberIdentity(entry));
6857
6912
  if (nested === void 0) {
6858
6913
  continue;
6859
6914
  }
@@ -6874,9 +6929,10 @@ function poolCycleOf(pools, name) {
6874
6929
  }
6875
6930
  function validateMembers(pools, name, members) {
6876
6931
  for (const entry of members) {
6877
- const nested = poolNameOf(entry);
6932
+ const identity = poolMemberIdentity(entry);
6933
+ const nested = poolNameOf(identity);
6878
6934
  if (nested !== void 0 && nested !== name && pools[nested] === void 0) {
6879
- throw new UsageError(`Member "${entry}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
6935
+ throw new UsageError(`Member "${identity}" of pool "${name}" names a pool that is not defined. Run \`agent-shim pool add ${nested} --identity <name>...\` first.`);
6880
6936
  }
6881
6937
  }
6882
6938
  const cycle = poolCycleOf({ ...pools, [name]: { identities: [...members] } }, name);
@@ -9581,16 +9637,16 @@ function runDoctor(params) {
9581
9637
  const validatedGlobalConfig = params.globalConfig.raw === void 0 ? void 0 : validateJson(GlobalConfigSchema, params.globalConfig);
9582
9638
  const pools = validatedGlobalConfig?.ok === true ? validatedGlobalConfig.data.pools ?? {} : {};
9583
9639
  for (const [poolName, pool] of Object.entries(pools)) {
9584
- const missing = pool.identities.filter((member) => poolNameOf(member) === void 0 && !validIdentityNames.has(member));
9640
+ const missing = pool.identities.filter((member) => poolNameOf(poolMemberIdentity(member)) === void 0 && !validIdentityNames.has(poolMemberIdentity(member)));
9585
9641
  const undefinedPools = pool.identities.flatMap((member) => {
9586
- const nested = poolNameOf(member);
9642
+ const nested = poolNameOf(poolMemberIdentity(member));
9587
9643
  return nested === void 0 || nested in pools ? [] : [nested];
9588
9644
  });
9589
9645
  if (missing.length === 0 && undefinedPools.length === 0) {
9590
- push("pool", "pass", `${poolName} is valid (${pool.identities.join(", ")}).`, poolName);
9646
+ push("pool", "pass", `${poolName} is valid (${pool.identities.map(poolMemberIdentity).join(", ")}).`, poolName);
9591
9647
  } else {
9592
9648
  const problems = [
9593
- ...missing.map((member) => `identity "${member}", which does not exist`),
9649
+ ...missing.map((member) => `identity "${poolMemberIdentity(member)}", which does not exist`),
9594
9650
  ...undefinedPools.map((nested) => `pool "${nested}", which is not defined`)
9595
9651
  ];
9596
9652
  push("pool", "fail", `Pool "${poolName}" names ${problems.join(" and ")}.`, poolName);
@@ -4,7 +4,7 @@ import * as zod from 'zod';
4
4
  import { z } from 'zod';
5
5
  import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
6
6
  import * as net from 'node:net';
7
- import { Duplex } from 'node:stream';
7
+ import { Duplex, Readable } from 'node:stream';
8
8
  import * as _orpc_client from '@orpc/client';
9
9
  import * as zod_v4_core from 'zod/v4/core';
10
10
  import * as _orpc_server from '@orpc/server';
@@ -446,6 +446,81 @@ declare const ProviderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
446
446
  }>>;
447
447
  }, z.core.$strict>;
448
448
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
449
+ routes: z.ZodOptional<z.ZodArray<z.ZodObject<{
450
+ when: z.ZodUnion<readonly [z.ZodObject<{
451
+ newerThan: z.ZodOptional<z.ZodString>;
452
+ olderThan: z.ZodOptional<z.ZodString>;
453
+ maxSizeBytes: z.ZodOptional<z.ZodInt>;
454
+ branch: z.ZodOptional<z.ZodString>;
455
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
456
+ }, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
457
+ kind: z.ZodLiteral<"not">;
458
+ operand: typeof PredicateNodeSchema;
459
+ }, z.core.$strip>, z.ZodObject<{
460
+ kind: z.ZodLiteral<"and">;
461
+ left: typeof PredicateNodeSchema;
462
+ right: typeof PredicateNodeSchema;
463
+ }, z.core.$strip>, z.ZodObject<{
464
+ kind: z.ZodLiteral<"or">;
465
+ left: typeof PredicateNodeSchema;
466
+ right: typeof PredicateNodeSchema;
467
+ }, z.core.$strip>, z.ZodObject<{
468
+ kind: z.ZodLiteral<"allOf">;
469
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
470
+ }, z.core.$strip>, z.ZodObject<{
471
+ kind: z.ZodLiteral<"anyOf">;
472
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
473
+ }, z.core.$strip>, z.ZodObject<{
474
+ kind: z.ZodLiteral<"compare">;
475
+ op: z.ZodEnum<{
476
+ gt: "gt";
477
+ gte: "gte";
478
+ lt: "lt";
479
+ lte: "lte";
480
+ eq: "eq";
481
+ neq: "neq";
482
+ }>;
483
+ left: typeof trilean.ExpressionNodeSchema;
484
+ right: typeof trilean.ExpressionNodeSchema;
485
+ }, z.core.$strip>, z.ZodObject<{
486
+ kind: z.ZodLiteral<"textCompare">;
487
+ op: z.ZodEnum<{
488
+ equals: "equals";
489
+ notEquals: "notEquals";
490
+ matches: "matches";
491
+ notMatches: "notMatches";
492
+ portableMatches: "portableMatches";
493
+ portableNotMatches: "portableNotMatches";
494
+ }>;
495
+ left: typeof trilean.ExpressionNodeSchema;
496
+ right: typeof trilean.ExpressionNodeSchema;
497
+ }, z.core.$strip>, z.ZodObject<{
498
+ kind: z.ZodLiteral<"memberOf">;
499
+ op: z.ZodEnum<{
500
+ in: "in";
501
+ notIn: "notIn";
502
+ }>;
503
+ operand: typeof trilean.ExpressionNodeSchema;
504
+ candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
505
+ }, z.core.$strip>, z.ZodObject<{
506
+ kind: z.ZodLiteral<"exists">;
507
+ operand: typeof trilean.ExpressionNodeSchema;
508
+ }, z.core.$strip>, z.ZodObject<{
509
+ kind: z.ZodLiteral<"some">;
510
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
511
+ item: typeof PredicateNodeSchema;
512
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
513
+ }, z.core.$strip>, z.ZodObject<{
514
+ kind: z.ZodLiteral<"every">;
515
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
516
+ item: typeof PredicateNodeSchema;
517
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
518
+ }, z.core.$strip>, z.ZodObject<{
519
+ kind: z.ZodLiteral<"treeReference">;
520
+ key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
521
+ }, z.core.$strip>], "kind">]>;
522
+ provider: z.ZodString;
523
+ }, z.core.$strict>>>;
449
524
  }, z.core.$strict>, z.ZodObject<{
450
525
  $schema: z.ZodOptional<z.ZodString>;
451
526
  kind: z.ZodLiteral<"codex">;
@@ -630,7 +705,81 @@ type PoolPreference = z.infer<typeof PoolPreferenceSchema>;
630
705
  * `preference` decides how the member is chosen, and an absent field means "score". "score" ranks members by plan-size-weighted remaining quota per hour until reset, so the quota closest to expiring is spent first (use-it-or-lose-it). "listed" ranks members strictly in the order the pool lists them, skipping only a member that is currently refused, so a pool can pin one identity first and fall back to the others only while that one is refused.
631
706
  */
632
707
  declare const PoolSchema: z.ZodObject<{
633
- identities: z.ZodArray<z.ZodString>;
708
+ identities: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
709
+ identity: z.ZodString;
710
+ when: z.ZodUnion<readonly [z.ZodObject<{
711
+ newerThan: z.ZodOptional<z.ZodString>;
712
+ olderThan: z.ZodOptional<z.ZodString>;
713
+ maxSizeBytes: z.ZodOptional<z.ZodInt>;
714
+ branch: z.ZodOptional<z.ZodString>;
715
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
716
+ }, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
717
+ kind: z.ZodLiteral<"not">;
718
+ operand: typeof PredicateNodeSchema;
719
+ }, z.core.$strip>, z.ZodObject<{
720
+ kind: z.ZodLiteral<"and">;
721
+ left: typeof PredicateNodeSchema;
722
+ right: typeof PredicateNodeSchema;
723
+ }, z.core.$strip>, z.ZodObject<{
724
+ kind: z.ZodLiteral<"or">;
725
+ left: typeof PredicateNodeSchema;
726
+ right: typeof PredicateNodeSchema;
727
+ }, z.core.$strip>, z.ZodObject<{
728
+ kind: z.ZodLiteral<"allOf">;
729
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
730
+ }, z.core.$strip>, z.ZodObject<{
731
+ kind: z.ZodLiteral<"anyOf">;
732
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
733
+ }, z.core.$strip>, z.ZodObject<{
734
+ kind: z.ZodLiteral<"compare">;
735
+ op: z.ZodEnum<{
736
+ gt: "gt";
737
+ gte: "gte";
738
+ lt: "lt";
739
+ lte: "lte";
740
+ eq: "eq";
741
+ neq: "neq";
742
+ }>;
743
+ left: typeof trilean.ExpressionNodeSchema;
744
+ right: typeof trilean.ExpressionNodeSchema;
745
+ }, z.core.$strip>, z.ZodObject<{
746
+ kind: z.ZodLiteral<"textCompare">;
747
+ op: z.ZodEnum<{
748
+ equals: "equals";
749
+ notEquals: "notEquals";
750
+ matches: "matches";
751
+ notMatches: "notMatches";
752
+ portableMatches: "portableMatches";
753
+ portableNotMatches: "portableNotMatches";
754
+ }>;
755
+ left: typeof trilean.ExpressionNodeSchema;
756
+ right: typeof trilean.ExpressionNodeSchema;
757
+ }, z.core.$strip>, z.ZodObject<{
758
+ kind: z.ZodLiteral<"memberOf">;
759
+ op: z.ZodEnum<{
760
+ in: "in";
761
+ notIn: "notIn";
762
+ }>;
763
+ operand: typeof trilean.ExpressionNodeSchema;
764
+ candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
765
+ }, z.core.$strip>, z.ZodObject<{
766
+ kind: z.ZodLiteral<"exists">;
767
+ operand: typeof trilean.ExpressionNodeSchema;
768
+ }, z.core.$strip>, z.ZodObject<{
769
+ kind: z.ZodLiteral<"some">;
770
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
771
+ item: typeof PredicateNodeSchema;
772
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
773
+ }, z.core.$strip>, z.ZodObject<{
774
+ kind: z.ZodLiteral<"every">;
775
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
776
+ item: typeof PredicateNodeSchema;
777
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
778
+ }, z.core.$strip>, z.ZodObject<{
779
+ kind: z.ZodLiteral<"treeReference">;
780
+ key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
781
+ }, z.core.$strip>], "kind">]>;
782
+ }, z.core.$strict>]>>;
634
783
  preference: z.ZodOptional<z.ZodEnum<{
635
784
  score: "score";
636
785
  listed: "listed";
@@ -1332,7 +1481,81 @@ declare const GlobalConfigSchema: z.ZodObject<{
1332
1481
  }>>;
1333
1482
  }, z.core.$strict>>;
1334
1483
  pools: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
1335
- identities: z.ZodArray<z.ZodString>;
1484
+ identities: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
1485
+ identity: z.ZodString;
1486
+ when: z.ZodUnion<readonly [z.ZodObject<{
1487
+ newerThan: z.ZodOptional<z.ZodString>;
1488
+ olderThan: z.ZodOptional<z.ZodString>;
1489
+ maxSizeBytes: z.ZodOptional<z.ZodInt>;
1490
+ branch: z.ZodOptional<z.ZodString>;
1491
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1492
+ }, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
1493
+ kind: z.ZodLiteral<"not">;
1494
+ operand: typeof PredicateNodeSchema;
1495
+ }, z.core.$strip>, z.ZodObject<{
1496
+ kind: z.ZodLiteral<"and">;
1497
+ left: typeof PredicateNodeSchema;
1498
+ right: typeof PredicateNodeSchema;
1499
+ }, z.core.$strip>, z.ZodObject<{
1500
+ kind: z.ZodLiteral<"or">;
1501
+ left: typeof PredicateNodeSchema;
1502
+ right: typeof PredicateNodeSchema;
1503
+ }, z.core.$strip>, z.ZodObject<{
1504
+ kind: z.ZodLiteral<"allOf">;
1505
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
1506
+ }, z.core.$strip>, z.ZodObject<{
1507
+ kind: z.ZodLiteral<"anyOf">;
1508
+ operands: z.ZodArray<typeof PredicateNodeSchema>;
1509
+ }, z.core.$strip>, z.ZodObject<{
1510
+ kind: z.ZodLiteral<"compare">;
1511
+ op: z.ZodEnum<{
1512
+ gt: "gt";
1513
+ gte: "gte";
1514
+ lt: "lt";
1515
+ lte: "lte";
1516
+ eq: "eq";
1517
+ neq: "neq";
1518
+ }>;
1519
+ left: typeof trilean.ExpressionNodeSchema;
1520
+ right: typeof trilean.ExpressionNodeSchema;
1521
+ }, z.core.$strip>, z.ZodObject<{
1522
+ kind: z.ZodLiteral<"textCompare">;
1523
+ op: z.ZodEnum<{
1524
+ equals: "equals";
1525
+ notEquals: "notEquals";
1526
+ matches: "matches";
1527
+ notMatches: "notMatches";
1528
+ portableMatches: "portableMatches";
1529
+ portableNotMatches: "portableNotMatches";
1530
+ }>;
1531
+ left: typeof trilean.ExpressionNodeSchema;
1532
+ right: typeof trilean.ExpressionNodeSchema;
1533
+ }, z.core.$strip>, z.ZodObject<{
1534
+ kind: z.ZodLiteral<"memberOf">;
1535
+ op: z.ZodEnum<{
1536
+ in: "in";
1537
+ notIn: "notIn";
1538
+ }>;
1539
+ operand: typeof trilean.ExpressionNodeSchema;
1540
+ candidates: z.ZodArray<typeof trilean.ExpressionNodeSchema>;
1541
+ }, z.core.$strip>, z.ZodObject<{
1542
+ kind: z.ZodLiteral<"exists">;
1543
+ operand: typeof trilean.ExpressionNodeSchema;
1544
+ }, z.core.$strip>, z.ZodObject<{
1545
+ kind: z.ZodLiteral<"some">;
1546
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
1547
+ item: typeof PredicateNodeSchema;
1548
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
1549
+ }, z.core.$strip>, z.ZodObject<{
1550
+ kind: z.ZodLiteral<"every">;
1551
+ collection: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
1552
+ item: typeof PredicateNodeSchema;
1553
+ filter: z.ZodOptional<typeof PredicateNodeSchema>;
1554
+ }, z.core.$strip>, z.ZodObject<{
1555
+ kind: z.ZodLiteral<"treeReference">;
1556
+ key: z.ZodType<trilean.JsonValue, unknown, z.core.$ZodTypeInternals<trilean.JsonValue, unknown>>;
1557
+ }, z.core.$strip>], "kind">]>;
1558
+ }, z.core.$strict>]>>;
1336
1559
  preference: z.ZodOptional<z.ZodEnum<{
1337
1560
  score: "score";
1338
1561
  listed: "listed";
@@ -2517,8 +2740,8 @@ interface RoutedRequest {
2517
2740
  readonly url: string;
2518
2741
  /** The request's headers with every internal header already stripped: exactly what a route may forward onwards. */
2519
2742
  readonly headers: IncomingHttpHeaders;
2520
- /** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. */
2521
- readonly body: IncomingMessage;
2743
+ /** The request's body stream: a route either pipes it onward untouched or reads it whole, never both. A readable, not the socket-bound type the pipeline received it as, because the routing layer's scan hands routes a replayed body (the scanned head re-emitted in front of the unread remainder) that is a plain stream. */
2744
+ readonly body: Readable;
2522
2745
  /** Aborted the moment the client goes away; a route aborts its own upstream work with it. */
2523
2746
  readonly signal: AbortSignal;
2524
2747
  readonly session: SessionIdentity;
@@ -6784,6 +7007,8 @@ interface PoolMember {
6784
7007
  readonly records: readonly UsageRecord[];
6785
7008
  /** Present when this member stands for a nested `pool:<name>` entry rather than a direct identity. */
6786
7009
  readonly nested?: NestedContribution;
7010
+ /** The member entry's policy condition, from the object form of a pool member: definite false skips the member as ineligible by policy, indeterminate demotes it to last within its class, naming the missing fact either way. */
7011
+ readonly policy?: WhenCondition;
6787
7012
  }
6788
7013
  /** The member last picked for this directory, kept so a conversation stays on the account whose prompt cache is warm. */
6789
7014
  interface StickyPick {
@@ -6811,6 +7036,8 @@ interface Candidate {
6811
7036
  /** When an `ineligible` candidate can be used again. */
6812
7037
  readonly blockedUntilMs?: number;
6813
7038
  readonly plan: PlanClass;
7039
+ /** True when the member's policy condition could not be decided and it is ranked last within its class, naming the missing fact in its reasons. */
7040
+ readonly policyDemoted?: boolean;
6814
7041
  /** Human-readable facts behind the class and score, in the order they matter. */
6815
7042
  readonly reasons: readonly string[];
6816
7043
  }
@@ -6852,6 +7079,15 @@ declare function collectPoolPick(params: Readonly<{
6852
7079
  nowMs: number;
6853
7080
  }>): PoolPickReport;
6854
7081
 
7082
+ /** One quota window's pool-selection facts, in the units a policy reads: fractions of the window, and hours until it resets. */
7083
+ interface PoolWindowFacts {
7084
+ /** The fraction of the window still unused, between zero and one. */
7085
+ readonly remaining: number;
7086
+ /** The fraction of the window already used, between zero and one. */
7087
+ readonly utilization: number;
7088
+ /** How long until the window resets, in hours. */
7089
+ readonly hoursUntilReset: number;
7090
+ }
6855
7091
  /** Everything a `when` condition can be evaluated against, injected rather than read. */
6856
7092
  interface ConditionContext {
6857
7093
  readonly nowMs: number;
@@ -6862,6 +7098,23 @@ interface ConditionContext {
6862
7098
  /** True when the repository is in detached-HEAD state, in which case no `branch` condition can match. */
6863
7099
  readonly branchDetached?: boolean;
6864
7100
  readonly env: Readonly<Record<string, string | undefined>>;
7101
+ /**
7102
+ * The pool-selection facts a member entry's policy condition reads. Present only when the condition guards a pool member, so the same reference namespace serves the cascade and the pool: `quota.fiveHour.remaining`, `quota.fiveHour.utilization`, `quota.fiveHour.hoursUntilReset`, the same three under `quota.sevenDay`, `quota.burnPerHour` (the five-hour window's observed utilisation pace, in fractions per hour), `session.resuming` (this launch resumes or continues a conversation) and `session.lastPickHoursAgo` (how long ago this directory last picked this member). Conversation context is deliberately not provided: nothing the door holds knows it, so a condition referencing it is indeterminate, naming the missing fact, rather than guessed.
7103
+ */
7104
+ readonly pool?: {
7105
+ readonly fiveHour?: PoolWindowFacts;
7106
+ readonly sevenDay?: PoolWindowFacts;
7107
+ readonly burnPerHour?: number;
7108
+ readonly resuming?: boolean;
7109
+ readonly lastPickHoursAgo?: number;
7110
+ };
7111
+ /**
7112
+ * The request facts a provider's route condition reads, present only when the condition routes a request the door is resolving: `request.model` (the Messages body's own model field) and `request.hasImage` (an image block anywhere in the scanned head of the body). A request the door could not read that far carries neither, so a condition naming them is indeterminate and the routing falls through, never to a cheaper provider by accident.
7113
+ */
7114
+ readonly request?: {
7115
+ readonly model?: string;
7116
+ readonly hasImage?: boolean;
7117
+ };
6865
7118
  }
6866
7119
  /**
6867
7120
  * The result of evaluating one `when`: definite, with which condition fields were present and which of those did not hold, or indeterminate, when a datum the condition needed is missing. Indeterminate is not false: a missing fact has not failed the condition, it has left it undecided, and the consumer decides what an undecided entry does (the resolver keeps today's behaviour of not passing it, so nothing changes for existing configurations; the pool ranking of #258 instead keeps such a member but ranks it last, naming the missing fact).
@@ -7215,6 +7468,9 @@ declare function updateProvider(paths: LayoutPaths, name: string, input: UpdateP
7215
7468
  /** Deletes a provider definition. Throws `ProviderNotFoundError` when it does not exist. */
7216
7469
  declare function removeProvider(paths: LayoutPaths, name: string): void;
7217
7470
 
7471
+ /** One pool member entry, whichever form it takes: the plain selector string, or the object form carrying its policy condition. */
7472
+ type PoolMemberEntry = Pool["identities"][number];
7473
+
7218
7474
  /** Raised when a command names a pool that is not defined in the global config. */
7219
7475
  declare class PoolNotFoundError extends CliError {
7220
7476
  readonly poolName: string;
@@ -7225,9 +7481,9 @@ declare function readPools(paths: LayoutPaths): Readonly<Record<string, Pool>>;
7225
7481
  /** The pool named `name`; throws `PoolNotFoundError` when none is defined. */
7226
7482
  declare function requirePool(paths: LayoutPaths, name: string): Pool;
7227
7483
  /** Defines a new pool. Throws `InvalidPoolNameError` for a name `PoolNameSchema` rejects, `PoolAlreadyExistsError` for one already defined, and `UsageError` for a nested member that names an undefined pool or a cycle. */
7228
- declare function addPool(paths: LayoutPaths, name: string, identities: readonly string[], preference?: PoolPreference): Pool;
7484
+ declare function addPool(paths: LayoutPaths, name: string, identities: readonly PoolMemberEntry[], preference?: PoolPreference): Pool;
7229
7485
  /** Replaces an existing pool's members and preference. Throws `PoolNotFoundError` when it is not defined, and `UsageError` for a nested member that names an undefined pool or a cycle. */
7230
- declare function setPool(paths: LayoutPaths, name: string, identities: readonly string[], preference?: PoolPreference): Pool;
7486
+ declare function setPool(paths: LayoutPaths, name: string, identities: readonly PoolMemberEntry[], preference?: PoolPreference): Pool;
7231
7487
  /** Removes a pool. Throws `PoolNotFoundError` when it is not defined. Anything that selected it (an active-identity file, a directory rule) is left for `doctor` to report, as with a removed identity. */
7232
7488
  declare function removePool(paths: LayoutPaths, name: string): void;
7233
7489
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.33.0",
3
+ "version": "8.35.0",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",