agent-shim 8.31.0 → 8.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -368,6 +368,7 @@ var categories_default_default = {
368
368
  // src/config/schema.ts
369
369
  var import_node_path2 = __toESM(require("node:path"), 1);
370
370
  var import_zod = require("zod");
371
+ var import_trilean = require("trilean");
371
372
 
372
373
  // src/versionDiscovery.ts
373
374
  var import_node_os = __toESM(require("node:os"), 1);
@@ -498,13 +499,14 @@ var CategoryMapSchema = import_zod.z.strictObject({
498
499
  }).transform((input) => expandAllCategoryKey(input));
499
500
  var DURATION_RE = /^(?:0|[1-9][0-9]*)(?:ms|s|m|h|d|w)$/;
500
501
  var DurationSchema = import_zod.z.string().regex(DURATION_RE);
501
- var WhenSchema = import_zod.z.strictObject({
502
+ var WhenConditionObjectSchema = import_zod.z.strictObject({
502
503
  newerThan: DurationSchema.optional(),
503
504
  olderThan: DurationSchema.optional(),
504
505
  maxSizeBytes: import_zod.z.int().positive().optional(),
505
506
  branch: import_zod.z.string().min(1).optional(),
506
507
  env: import_zod.z.record(import_zod.z.string().min(1), import_zod.z.string()).optional()
507
508
  });
509
+ var WhenSchema = import_zod.z.union([WhenConditionObjectSchema, import_trilean.PredicateNodeSchema]);
508
510
  var EntryValueSchema = import_zod.z.union([
509
511
  import_zod.z.boolean(),
510
512
  import_zod.z.strictObject({ value: import_zod.z.boolean(), when: WhenSchema })
@@ -969,6 +971,7 @@ function classifyEntries(names, options) {
969
971
 
970
972
  // src/resolve/conditions.ts
971
973
  var import_picomatch2 = __toESM(require("picomatch"), 1);
974
+ var import_trilean2 = require("trilean");
972
975
  var MILLISECONDS_PER_UNIT = Object.freeze({
973
976
  ms: 1,
974
977
  s: 1e3,
@@ -999,52 +1002,81 @@ function matchBranch(pattern, branch, detached = false) {
999
1002
  }
1000
1003
  return (0, import_picomatch2.default)(pattern, { dot: true, nocase: false })(branch);
1001
1004
  }
1005
+ var evaluator = (0, import_trilean2.createSyncEvaluator)({});
1006
+ function resolversOf(context) {
1007
+ const values = {
1008
+ now: context.nowMs,
1009
+ ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
1010
+ ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
1011
+ ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
1012
+ "repo.detached": context.branchDetached ?? false
1013
+ };
1014
+ return {
1015
+ resolveValue: (key) => {
1016
+ const wrap = (value2) => ({ found: true, value: typeof value2 === "number" ? { kind: "number", value: value2 } : typeof value2 === "string" ? { kind: "text", value: value2 } : { kind: "boolean", value: value2 } });
1017
+ if (typeof key !== "string") {
1018
+ return { found: false };
1019
+ }
1020
+ if (key.startsWith("env.")) {
1021
+ const value2 = context.env[key.slice("env.".length)];
1022
+ return value2 === void 0 ? { found: false } : wrap(value2);
1023
+ }
1024
+ const value = values[key];
1025
+ return value === void 0 ? { found: false } : wrap(value);
1026
+ },
1027
+ // This embedding provides the reference namespace only: a tree that asks for a lookup or a collection finds neither, which the evaluator carries as indeterminate rather than letting the tree pretend an answer.
1028
+ resolveLookup: () => ({ found: false }),
1029
+ resolveCollection: () => []
1030
+ };
1031
+ }
1032
+ function fieldPredicate(field, when, context) {
1033
+ switch (field) {
1034
+ case "newerThan": {
1035
+ const window = parseDuration(when.newerThan ?? "");
1036
+ return { kind: "compare", op: "gte", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
1037
+ }
1038
+ case "olderThan": {
1039
+ const window = parseDuration(when.olderThan ?? "");
1040
+ return { kind: "compare", op: "lt", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
1041
+ }
1042
+ case "maxSizeBytes":
1043
+ return { kind: "compare", op: "lte", left: { kind: "reference", key: "entry.totalSizeBytes" }, right: { kind: "numberLiteral", value: when.maxSizeBytes ?? 0 } };
1044
+ case "branch":
1045
+ return { kind: "allOf", operands: [{ kind: "textCompare", op: "portableMatches", left: { kind: "reference", key: "repo.branch" }, right: { kind: "textLiteral", value: when.branch ?? "" } }, { kind: "compare", op: "eq", left: { kind: "reference", key: "repo.detached" }, right: { kind: "booleanLiteral", value: false } }] };
1046
+ case "env": {
1047
+ const entries = Object.entries(when.env ?? {});
1048
+ return { kind: "allOf", operands: entries.map(([name, expected]) => ({ kind: "textCompare", op: "equals", left: { kind: "reference", key: `env.${name}` }, right: { kind: "textLiteral", value: expected } })) };
1049
+ }
1050
+ }
1051
+ return { kind: "allOf", operands: [] };
1052
+ }
1002
1053
  function evaluateWhen(when, context) {
1003
1054
  if (when === void 0) {
1004
- return { passed: true, checked: [], failed: [] };
1055
+ return { status: "definite", passed: true, checked: [], failed: [] };
1056
+ }
1057
+ if ("kind" in when) {
1058
+ const result = evaluator.evaluatePredicate(when, void 0, resolversOf(context));
1059
+ return result.status === "indeterminate" ? { status: "indeterminate", reason: result.reason.message, checked: ["predicate"] } : { status: "definite", passed: result.value, checked: ["predicate"], failed: result.value ? [] : ["predicate"] };
1005
1060
  }
1006
1061
  const checked = [];
1007
1062
  const failed = [];
1008
- if (when.newerThan !== void 0) {
1009
- checked.push("newerThan");
1010
- const window = parseDuration(when.newerThan);
1011
- const latest = context.fact?.latestMtimeMs;
1012
- if (latest === void 0 || context.nowMs - latest > window) {
1013
- failed.push("newerThan");
1014
- }
1015
- }
1016
- if (when.olderThan !== void 0) {
1017
- checked.push("olderThan");
1018
- const window = parseDuration(when.olderThan);
1019
- const latest = context.fact?.latestMtimeMs;
1020
- if (latest === void 0 || context.nowMs - latest <= window) {
1021
- failed.push("olderThan");
1022
- }
1023
- }
1024
- if (when.maxSizeBytes !== void 0) {
1025
- checked.push("maxSizeBytes");
1026
- const total = context.fact?.totalSizeBytes;
1027
- if (total === void 0 || total > when.maxSizeBytes) {
1028
- failed.push("maxSizeBytes");
1063
+ for (const field of ["newerThan", "olderThan", "maxSizeBytes", "branch", "env"]) {
1064
+ if (when[field] === void 0) {
1065
+ continue;
1029
1066
  }
1030
- }
1031
- if (when.branch !== void 0) {
1032
- checked.push("branch");
1033
- if (!matchBranch(when.branch, context.branch, context.branchDetached ?? false)) {
1034
- failed.push("branch");
1067
+ checked.push(field);
1068
+ const result = evaluator.evaluatePredicate(fieldPredicate(field, when, context), void 0, resolversOf(context));
1069
+ if (result.status === "indeterminate") {
1070
+ return { status: "indeterminate", reason: result.reason.message, checked };
1035
1071
  }
1036
- }
1037
- if (when.env !== void 0) {
1038
- checked.push("env");
1039
- const mismatch = Object.entries(when.env).some(([name, expected]) => context.env[name] !== expected);
1040
- if (mismatch) {
1041
- failed.push("env");
1072
+ if (!result.value) {
1073
+ failed.push(field);
1042
1074
  }
1043
1075
  }
1044
- return { passed: failed.length === 0, checked, failed };
1076
+ return { status: "definite", passed: failed.length === 0, checked, failed };
1045
1077
  }
1046
1078
  function isVacuousWhen(when) {
1047
- return when !== void 0 && Object.keys(when).length === 0;
1079
+ return when !== void 0 && !("kind" in when) && Object.keys(when).length === 0;
1048
1080
  }
1049
1081
 
1050
1082
  // src/resolve/match.ts
@@ -1415,10 +1447,10 @@ function selectRule(relPath, flattened, context) {
1415
1447
  ...context.branchDetached === void 0 ? {} : { branchDetached: context.branchDetached },
1416
1448
  env: context.env
1417
1449
  });
1418
- if (evaluation.passed) {
1450
+ if (evaluation.status === "definite" && evaluation.passed) {
1419
1451
  return { rule, eliminated };
1420
1452
  }
1421
- eliminated.push({ rule, failed: evaluation.failed });
1453
+ eliminated.push({ rule, failed: evaluation.status === "definite" ? evaluation.failed : [`${evaluation.checked.join(", ")} could not be decided: ${evaluation.reason}`] });
1422
1454
  }
1423
1455
  return { eliminated };
1424
1456
  }
@@ -1652,7 +1684,10 @@ function planFarm(params) {
1652
1684
  var ENTER_EVERYTHING = () => true;
1653
1685
  function readsSubtreeAggregates(rule) {
1654
1686
  const when = rule.when;
1655
- return when !== void 0 && (when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0);
1687
+ if (when === void 0) {
1688
+ return false;
1689
+ }
1690
+ return "kind" in when || when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0;
1656
1691
  }
1657
1692
  function hasUnboundedDepth(pattern) {
1658
1693
  return pattern.includes("**") || /[{(]/.test(pattern);
@@ -5355,7 +5390,7 @@ var import_undici = require("undici");
5355
5390
  // package.json
5356
5391
  var package_default = {
5357
5392
  name: "agent-shim",
5358
- version: "8.31.0",
5393
+ version: "8.33.0",
5359
5394
  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.",
5360
5395
  license: "MIT",
5361
5396
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -5411,6 +5446,7 @@ var package_default = {
5411
5446
  commander: "^15.0.0",
5412
5447
  cosmiconfig: "^10.0.1",
5413
5448
  picomatch: "^4.0.7",
5449
+ trilean: "1.7.1",
5414
5450
  undici: "7.29.1",
5415
5451
  zod: "^4.6.1"
5416
5452
  },
@@ -6537,10 +6573,10 @@ function windowBlockedUntil(windows) {
6537
6573
  const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
6538
6574
  return resets.length === 0 ? void 0 : Math.max(...resets);
6539
6575
  }
6540
- function fiveHourBurn(records) {
6576
+ function windowBurn(records, which) {
6541
6577
  const points = [];
6542
6578
  for (const record of records) {
6543
- const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
6579
+ const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.[which];
6544
6580
  if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
6545
6581
  points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
6546
6582
  }
@@ -6634,11 +6670,16 @@ function assessIdentity(member, nowMs) {
6634
6670
  const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
6635
6671
  const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
6636
6672
  const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
6637
- const burn = fiveHourBurn(member.records);
6673
+ const burn = windowBurn(member.records, "fiveHour");
6674
+ const sevenRemaining = seven?.utilization === void 0 ? void 0 : Math.max(0, 1 - seven.utilization);
6675
+ const sevenUntil = seven?.resetsAtMs === void 0 ? SEVEN_DAY_WINDOW_MS : Math.max(seven.resetsAtMs - nowMs, MS_PER_SECOND4);
6676
+ const sevenDayBurn = windowBurn(member.records, "sevenDay");
6638
6677
  return {
6639
6678
  candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
6640
6679
  ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
6641
- ...burn === void 0 ? {} : { burn }
6680
+ ...burn === void 0 ? {} : { burn },
6681
+ ...sevenRemaining === void 0 ? {} : { sevenDay: { remaining: sevenRemaining, untilResetMs: sevenUntil } },
6682
+ ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
6642
6683
  };
6643
6684
  }
6644
6685
  function rankPool(input) {
@@ -6649,14 +6690,27 @@ function rankPool(input) {
6649
6690
  const candidates = assessments.map((assessment) => {
6650
6691
  const { candidate, fiveHour, burn } = assessment;
6651
6692
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6652
- if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
6693
+ const { sevenDay, sevenDayBurn } = assessment;
6694
+ if (candidate.class !== "scored") {
6653
6695
  return { ...candidate, feasible: true };
6654
6696
  }
6655
- const runsDryInMs = fiveHour.remaining / pace;
6656
- if (runsDryInMs >= fiveHour.untilResetMs) {
6697
+ const drySpots = [];
6698
+ if (fiveHour !== void 0 && pace !== void 0) {
6699
+ const runsDryInMs = fiveHour.remaining / pace;
6700
+ if (runsDryInMs < fiveHour.untilResetMs) {
6701
+ drySpots.push(`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`);
6702
+ }
6703
+ }
6704
+ if (sevenDay !== void 0 && sevenDayBurn !== void 0) {
6705
+ const runsDryInMs = sevenDay.remaining / sevenDayBurn;
6706
+ if (runsDryInMs < sevenDay.untilResetMs) {
6707
+ drySpots.push(`7d window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(sevenDay.untilResetMs)}`);
6708
+ }
6709
+ }
6710
+ if (drySpots.length === 0) {
6657
6711
  return { ...candidate, feasible: true };
6658
6712
  }
6659
- return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
6713
+ return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
6660
6714
  });
6661
6715
  const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6662
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)
@@ -6664,7 +6718,8 @@ function rankPool(input) {
6664
6718
  const sticky = input.sticky;
6665
6719
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
6666
6720
  const stickyCandidate = ordered[stickyIndex];
6667
- if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
6721
+ const keepSticky = stickyCandidate?.class === "scored" && stickyCandidate.feasible;
6722
+ if (sticky !== void 0 && keepSticky) {
6668
6723
  const ageMs = nowMs - Date.parse(sticky.at);
6669
6724
  const warm = ageMs <= PROMPT_CACHE_TTL_MS;
6670
6725
  if (input.resuming || warm) {
@@ -6673,12 +6728,14 @@ function rankPool(input) {
6673
6728
  ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
6674
6729
  }
6675
6730
  }
6731
+ const movedOff = sticky !== void 0 && input.resuming && !keepSticky ? { identity: sticky.identity, reason: stickyCandidate?.reasons[0] ?? "no longer among the pool's members" } : void 0;
6676
6732
  const pick = ordered.find((candidate) => candidate.class !== "ineligible");
6677
6733
  const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
6678
6734
  const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
6679
6735
  return {
6680
6736
  candidates: ordered,
6681
- ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
6737
+ ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick },
6738
+ ...movedOff === void 0 || pick === void 0 ? {} : { movedOff }
6682
6739
  };
6683
6740
  }
6684
6741
 
@@ -6936,6 +6993,7 @@ function collectPoolPick(params) {
6936
6993
  candidates: ranking.candidates.map(candidateView),
6937
6994
  missing: missing.map(({ identity }) => identity),
6938
6995
  ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6996
+ ...ranking.movedOff === void 0 ? {} : { movedOff: ranking.movedOff },
6939
6997
  ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
6940
6998
  };
6941
6999
  }
@@ -9207,6 +9265,8 @@ var PoolPickReportSchema = import_zod13.z.strictObject({
9207
9265
  missing: import_zod13.z.array(import_zod13.z.string()),
9208
9266
  /** When nothing can be picked, the soonest any member returns. */
9209
9267
  earliestReturn: import_zod13.z.strictObject({ identity: import_zod13.z.string(), at: import_zod13.z.iso.datetime() }).optional(),
9268
+ /** The member a resumed conversation started on but moved off, and why, when the ranking could not keep it. */
9269
+ movedOff: import_zod13.z.strictObject({ identity: import_zod13.z.string(), reason: import_zod13.z.string() }).optional(),
9210
9270
  /** Why the last-pick record could not be read, when that is the case. */
9211
9271
  stickyProblem: import_zod13.z.string().optional()
9212
9272
  });
@@ -12953,10 +13013,11 @@ function resolvePoolLaunch(params) {
12953
13013
  if (graph.ranking.candidates.length === 0) {
12954
13014
  return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
12955
13015
  }
12956
- const { pick, earliestReturn } = graph.ranking;
13016
+ const { pick, earliestReturn, movedOff } = graph.ranking;
12957
13017
  if (pick !== void 0) {
12958
13018
  recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
12959
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
13019
+ const move = movedOff === void 0 ? "" : `moved off ${movedOff.identity} (${movedOff.reason}); `;
13020
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${move}${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
12960
13021
  }
12961
13022
  if (earliestReturn === void 0) {
12962
13023
  return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };
package/dist/index.mjs CHANGED
@@ -56,6 +56,7 @@ var categories_default_default = {
56
56
  // src/config/schema.ts
57
57
  import path2 from "node:path";
58
58
  import { z } from "zod";
59
+ import { PredicateNodeSchema } from "trilean";
59
60
 
60
61
  // src/versionDiscovery.ts
61
62
  import os from "node:os";
@@ -186,13 +187,14 @@ var CategoryMapSchema = z.strictObject({
186
187
  }).transform((input) => expandAllCategoryKey(input));
187
188
  var DURATION_RE = /^(?:0|[1-9][0-9]*)(?:ms|s|m|h|d|w)$/;
188
189
  var DurationSchema = z.string().regex(DURATION_RE);
189
- var WhenSchema = z.strictObject({
190
+ var WhenConditionObjectSchema = z.strictObject({
190
191
  newerThan: DurationSchema.optional(),
191
192
  olderThan: DurationSchema.optional(),
192
193
  maxSizeBytes: z.int().positive().optional(),
193
194
  branch: z.string().min(1).optional(),
194
195
  env: z.record(z.string().min(1), z.string()).optional()
195
196
  });
197
+ var WhenSchema = z.union([WhenConditionObjectSchema, PredicateNodeSchema]);
196
198
  var EntryValueSchema = z.union([
197
199
  z.boolean(),
198
200
  z.strictObject({ value: z.boolean(), when: WhenSchema })
@@ -657,6 +659,7 @@ function classifyEntries(names, options) {
657
659
 
658
660
  // src/resolve/conditions.ts
659
661
  import picomatch2 from "picomatch";
662
+ import { createSyncEvaluator } from "trilean";
660
663
  var MILLISECONDS_PER_UNIT = Object.freeze({
661
664
  ms: 1,
662
665
  s: 1e3,
@@ -687,52 +690,81 @@ function matchBranch(pattern, branch, detached = false) {
687
690
  }
688
691
  return picomatch2(pattern, { dot: true, nocase: false })(branch);
689
692
  }
693
+ var evaluator = createSyncEvaluator({});
694
+ function resolversOf(context) {
695
+ const values = {
696
+ now: context.nowMs,
697
+ ...context.fact?.latestMtimeMs === void 0 ? {} : { "entry.latestMtimeMs": context.fact.latestMtimeMs },
698
+ ...context.fact?.totalSizeBytes === void 0 ? {} : { "entry.totalSizeBytes": context.fact.totalSizeBytes },
699
+ ...context.branch === void 0 ? {} : { "repo.branch": context.branch },
700
+ "repo.detached": context.branchDetached ?? false
701
+ };
702
+ return {
703
+ resolveValue: (key) => {
704
+ const wrap = (value2) => ({ found: true, value: typeof value2 === "number" ? { kind: "number", value: value2 } : typeof value2 === "string" ? { kind: "text", value: value2 } : { kind: "boolean", value: value2 } });
705
+ if (typeof key !== "string") {
706
+ return { found: false };
707
+ }
708
+ if (key.startsWith("env.")) {
709
+ const value2 = context.env[key.slice("env.".length)];
710
+ return value2 === void 0 ? { found: false } : wrap(value2);
711
+ }
712
+ const value = values[key];
713
+ return value === void 0 ? { found: false } : wrap(value);
714
+ },
715
+ // This embedding provides the reference namespace only: a tree that asks for a lookup or a collection finds neither, which the evaluator carries as indeterminate rather than letting the tree pretend an answer.
716
+ resolveLookup: () => ({ found: false }),
717
+ resolveCollection: () => []
718
+ };
719
+ }
720
+ function fieldPredicate(field, when, context) {
721
+ switch (field) {
722
+ case "newerThan": {
723
+ const window = parseDuration(when.newerThan ?? "");
724
+ return { kind: "compare", op: "gte", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
725
+ }
726
+ case "olderThan": {
727
+ const window = parseDuration(when.olderThan ?? "");
728
+ return { kind: "compare", op: "lt", left: { kind: "reference", key: "entry.latestMtimeMs" }, right: { kind: "numberLiteral", value: context.nowMs - window } };
729
+ }
730
+ case "maxSizeBytes":
731
+ return { kind: "compare", op: "lte", left: { kind: "reference", key: "entry.totalSizeBytes" }, right: { kind: "numberLiteral", value: when.maxSizeBytes ?? 0 } };
732
+ case "branch":
733
+ return { kind: "allOf", operands: [{ kind: "textCompare", op: "portableMatches", left: { kind: "reference", key: "repo.branch" }, right: { kind: "textLiteral", value: when.branch ?? "" } }, { kind: "compare", op: "eq", left: { kind: "reference", key: "repo.detached" }, right: { kind: "booleanLiteral", value: false } }] };
734
+ case "env": {
735
+ const entries = Object.entries(when.env ?? {});
736
+ return { kind: "allOf", operands: entries.map(([name, expected]) => ({ kind: "textCompare", op: "equals", left: { kind: "reference", key: `env.${name}` }, right: { kind: "textLiteral", value: expected } })) };
737
+ }
738
+ }
739
+ return { kind: "allOf", operands: [] };
740
+ }
690
741
  function evaluateWhen(when, context) {
691
742
  if (when === void 0) {
692
- return { passed: true, checked: [], failed: [] };
743
+ return { status: "definite", passed: true, checked: [], failed: [] };
744
+ }
745
+ if ("kind" in when) {
746
+ const result = evaluator.evaluatePredicate(when, void 0, resolversOf(context));
747
+ return result.status === "indeterminate" ? { status: "indeterminate", reason: result.reason.message, checked: ["predicate"] } : { status: "definite", passed: result.value, checked: ["predicate"], failed: result.value ? [] : ["predicate"] };
693
748
  }
694
749
  const checked = [];
695
750
  const failed = [];
696
- if (when.newerThan !== void 0) {
697
- checked.push("newerThan");
698
- const window = parseDuration(when.newerThan);
699
- const latest = context.fact?.latestMtimeMs;
700
- if (latest === void 0 || context.nowMs - latest > window) {
701
- failed.push("newerThan");
702
- }
703
- }
704
- if (when.olderThan !== void 0) {
705
- checked.push("olderThan");
706
- const window = parseDuration(when.olderThan);
707
- const latest = context.fact?.latestMtimeMs;
708
- if (latest === void 0 || context.nowMs - latest <= window) {
709
- failed.push("olderThan");
710
- }
711
- }
712
- if (when.maxSizeBytes !== void 0) {
713
- checked.push("maxSizeBytes");
714
- const total = context.fact?.totalSizeBytes;
715
- if (total === void 0 || total > when.maxSizeBytes) {
716
- failed.push("maxSizeBytes");
751
+ for (const field of ["newerThan", "olderThan", "maxSizeBytes", "branch", "env"]) {
752
+ if (when[field] === void 0) {
753
+ continue;
717
754
  }
718
- }
719
- if (when.branch !== void 0) {
720
- checked.push("branch");
721
- if (!matchBranch(when.branch, context.branch, context.branchDetached ?? false)) {
722
- failed.push("branch");
755
+ checked.push(field);
756
+ const result = evaluator.evaluatePredicate(fieldPredicate(field, when, context), void 0, resolversOf(context));
757
+ if (result.status === "indeterminate") {
758
+ return { status: "indeterminate", reason: result.reason.message, checked };
723
759
  }
724
- }
725
- if (when.env !== void 0) {
726
- checked.push("env");
727
- const mismatch = Object.entries(when.env).some(([name, expected]) => context.env[name] !== expected);
728
- if (mismatch) {
729
- failed.push("env");
760
+ if (!result.value) {
761
+ failed.push(field);
730
762
  }
731
763
  }
732
- return { passed: failed.length === 0, checked, failed };
764
+ return { status: "definite", passed: failed.length === 0, checked, failed };
733
765
  }
734
766
  function isVacuousWhen(when) {
735
- return when !== void 0 && Object.keys(when).length === 0;
767
+ return when !== void 0 && !("kind" in when) && Object.keys(when).length === 0;
736
768
  }
737
769
 
738
770
  // src/resolve/match.ts
@@ -1103,10 +1135,10 @@ function selectRule(relPath, flattened, context) {
1103
1135
  ...context.branchDetached === void 0 ? {} : { branchDetached: context.branchDetached },
1104
1136
  env: context.env
1105
1137
  });
1106
- if (evaluation.passed) {
1138
+ if (evaluation.status === "definite" && evaluation.passed) {
1107
1139
  return { rule, eliminated };
1108
1140
  }
1109
- eliminated.push({ rule, failed: evaluation.failed });
1141
+ eliminated.push({ rule, failed: evaluation.status === "definite" ? evaluation.failed : [`${evaluation.checked.join(", ")} could not be decided: ${evaluation.reason}`] });
1110
1142
  }
1111
1143
  return { eliminated };
1112
1144
  }
@@ -1340,7 +1372,10 @@ function planFarm(params) {
1340
1372
  var ENTER_EVERYTHING = () => true;
1341
1373
  function readsSubtreeAggregates(rule) {
1342
1374
  const when = rule.when;
1343
- return when !== void 0 && (when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0);
1375
+ if (when === void 0) {
1376
+ return false;
1377
+ }
1378
+ return "kind" in when || when.newerThan !== void 0 || when.olderThan !== void 0 || when.maxSizeBytes !== void 0;
1344
1379
  }
1345
1380
  function hasUnboundedDepth(pattern) {
1346
1381
  return pattern.includes("**") || /[{(]/.test(pattern);
@@ -5043,7 +5078,7 @@ import { Agent as Agent3, fetch as undiciFetch } from "undici";
5043
5078
  // package.json
5044
5079
  var package_default = {
5045
5080
  name: "agent-shim",
5046
- version: "8.31.0",
5081
+ version: "8.33.0",
5047
5082
  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.",
5048
5083
  license: "MIT",
5049
5084
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -5099,6 +5134,7 @@ var package_default = {
5099
5134
  commander: "^15.0.0",
5100
5135
  cosmiconfig: "^10.0.1",
5101
5136
  picomatch: "^4.0.7",
5137
+ trilean: "1.7.1",
5102
5138
  undici: "7.29.1",
5103
5139
  zod: "^4.6.1"
5104
5140
  },
@@ -6225,10 +6261,10 @@ function windowBlockedUntil(windows) {
6225
6261
  const resets = windows.flatMap((window) => window.status === "rejected" && window.resetsAtMs !== void 0 ? [window.resetsAtMs] : []);
6226
6262
  return resets.length === 0 ? void 0 : Math.max(...resets);
6227
6263
  }
6228
- function fiveHourBurn(records) {
6264
+ function windowBurn(records, which) {
6229
6265
  const points = [];
6230
6266
  for (const record of records) {
6231
- const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.fiveHour;
6267
+ const window = record.rateLimitHeaders === void 0 ? void 0 : parseUnifiedRateLimit(record.rateLimitHeaders)?.[which];
6232
6268
  if (window?.utilization !== void 0 && window.resetsAt !== void 0) {
6233
6269
  points.push({ resetsAt: window.resetsAt, point: { atMs: Date.parse(record.at), utilization: window.utilization } });
6234
6270
  }
@@ -6322,11 +6358,16 @@ function assessIdentity(member, nowMs) {
6322
6358
  const score = remaining * plan.capacity / (untilResetMs / MS_PER_HOUR);
6323
6359
  const fiveRemaining = five?.utilization === void 0 ? void 0 : Math.max(0, 1 - five.utilization);
6324
6360
  const fiveUntil = five?.resetsAtMs === void 0 ? FIVE_HOUR_WINDOW_MS : Math.max(five.resetsAtMs - nowMs, MS_PER_SECOND4);
6325
- const burn = fiveHourBurn(member.records);
6361
+ const burn = windowBurn(member.records, "fiveHour");
6362
+ const sevenRemaining = seven?.utilization === void 0 ? void 0 : Math.max(0, 1 - seven.utilization);
6363
+ const sevenUntil = seven?.resetsAtMs === void 0 ? SEVEN_DAY_WINDOW_MS : Math.max(seven.resetsAtMs - nowMs, MS_PER_SECOND4);
6364
+ const sevenDayBurn = windowBurn(member.records, "sevenDay");
6326
6365
  return {
6327
6366
  candidate: { ...base, class: "scored", score, reasons: [...windowReasons.map((reason, index) => index === windowReasons.length - 1 ? `${reason}${observedAgo}` : reason), ...planReasons] },
6328
6367
  ...fiveRemaining === void 0 ? {} : { fiveHour: { remaining: fiveRemaining, untilResetMs: fiveUntil } },
6329
- ...burn === void 0 ? {} : { burn }
6368
+ ...burn === void 0 ? {} : { burn },
6369
+ ...sevenRemaining === void 0 ? {} : { sevenDay: { remaining: sevenRemaining, untilResetMs: sevenUntil } },
6370
+ ...sevenDayBurn === void 0 ? {} : { sevenDayBurn }
6330
6371
  };
6331
6372
  }
6332
6373
  function rankPool(input) {
@@ -6337,14 +6378,27 @@ function rankPool(input) {
6337
6378
  const candidates = assessments.map((assessment) => {
6338
6379
  const { candidate, fiveHour, burn } = assessment;
6339
6380
  const pace = burn ?? (sharedDemand === void 0 ? void 0 : sharedDemand / capacityOf(candidate.plan));
6340
- if (candidate.class !== "scored" || fiveHour === void 0 || pace === void 0) {
6381
+ const { sevenDay, sevenDayBurn } = assessment;
6382
+ if (candidate.class !== "scored") {
6341
6383
  return { ...candidate, feasible: true };
6342
6384
  }
6343
- const runsDryInMs = fiveHour.remaining / pace;
6344
- if (runsDryInMs >= fiveHour.untilResetMs) {
6385
+ const drySpots = [];
6386
+ if (fiveHour !== void 0 && pace !== void 0) {
6387
+ const runsDryInMs = fiveHour.remaining / pace;
6388
+ if (runsDryInMs < fiveHour.untilResetMs) {
6389
+ drySpots.push(`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`);
6390
+ }
6391
+ }
6392
+ if (sevenDay !== void 0 && sevenDayBurn !== void 0) {
6393
+ const runsDryInMs = sevenDay.remaining / sevenDayBurn;
6394
+ if (runsDryInMs < sevenDay.untilResetMs) {
6395
+ drySpots.push(`7d window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(sevenDay.untilResetMs)}`);
6396
+ }
6397
+ }
6398
+ if (drySpots.length === 0) {
6345
6399
  return { ...candidate, feasible: true };
6346
6400
  }
6347
- return { ...candidate, feasible: false, reasons: [`5h window would run dry in ${formatAge(runsDryInMs)}, before it resets in ${formatAge(fiveHour.untilResetMs)}`, ...candidate.reasons] };
6401
+ return { ...candidate, feasible: false, reasons: [...drySpots, ...candidate.reasons] };
6348
6402
  });
6349
6403
  const ordered = input.preference === "listed" ? [...candidates] : [...candidates].sort(
6350
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)
@@ -6352,7 +6406,8 @@ function rankPool(input) {
6352
6406
  const sticky = input.sticky;
6353
6407
  const stickyIndex = sticky === void 0 ? -1 : ordered.findIndex((candidate) => candidate.identity === sticky.identity);
6354
6408
  const stickyCandidate = ordered[stickyIndex];
6355
- if (sticky !== void 0 && stickyCandidate?.class === "scored" && stickyCandidate.feasible) {
6409
+ const keepSticky = stickyCandidate?.class === "scored" && stickyCandidate.feasible;
6410
+ if (sticky !== void 0 && keepSticky) {
6356
6411
  const ageMs = nowMs - Date.parse(sticky.at);
6357
6412
  const warm = ageMs <= PROMPT_CACHE_TTL_MS;
6358
6413
  if (input.resuming || warm) {
@@ -6361,12 +6416,14 @@ function rankPool(input) {
6361
6416
  ordered.unshift({ ...stickyCandidate, reasons: [why, ...stickyCandidate.reasons] });
6362
6417
  }
6363
6418
  }
6419
+ const movedOff = sticky !== void 0 && input.resuming && !keepSticky ? { identity: sticky.identity, reason: stickyCandidate?.reasons[0] ?? "no longer among the pool's members" } : void 0;
6364
6420
  const pick = ordered.find((candidate) => candidate.class !== "ineligible");
6365
6421
  const returns = ordered.flatMap((candidate) => candidate.blockedUntilMs === void 0 ? [] : [{ identity: candidate.identity, atMs: candidate.blockedUntilMs }]);
6366
6422
  const earliestReturn = returns.length === 0 ? void 0 : returns.reduce((soonest, entry) => entry.atMs < soonest.atMs ? entry : soonest);
6367
6423
  return {
6368
6424
  candidates: ordered,
6369
- ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick }
6425
+ ...pick === void 0 ? { ...earliestReturn === void 0 ? {} : { earliestReturn } } : { pick },
6426
+ ...movedOff === void 0 || pick === void 0 ? {} : { movedOff }
6370
6427
  };
6371
6428
  }
6372
6429
 
@@ -6624,6 +6681,7 @@ function collectPoolPick(params) {
6624
6681
  candidates: ranking.candidates.map(candidateView),
6625
6682
  missing: missing.map(({ identity }) => identity),
6626
6683
  ...ranking.earliestReturn === void 0 ? {} : { earliestReturn: { identity: ranking.earliestReturn.identity, at: new Date(ranking.earliestReturn.atMs).toISOString() } },
6684
+ ...ranking.movedOff === void 0 ? {} : { movedOff: ranking.movedOff },
6627
6685
  ...sticky.problem === void 0 ? {} : { stickyProblem: sticky.problem }
6628
6686
  };
6629
6687
  }
@@ -8895,6 +8953,8 @@ var PoolPickReportSchema = z13.strictObject({
8895
8953
  missing: z13.array(z13.string()),
8896
8954
  /** When nothing can be picked, the soonest any member returns. */
8897
8955
  earliestReturn: z13.strictObject({ identity: z13.string(), at: z13.iso.datetime() }).optional(),
8956
+ /** The member a resumed conversation started on but moved off, and why, when the ranking could not keep it. */
8957
+ movedOff: z13.strictObject({ identity: z13.string(), reason: z13.string() }).optional(),
8898
8958
  /** Why the last-pick record could not be read, when that is the case. */
8899
8959
  stickyProblem: z13.string().optional()
8900
8960
  });
@@ -12641,10 +12701,11 @@ function resolvePoolLaunch(params) {
12641
12701
  if (graph.ranking.candidates.length === 0) {
12642
12702
  return { ok: false, message: `agent-shim: no member of pool "${poolName}" is an existing identity.` };
12643
12703
  }
12644
- const { pick, earliestReturn } = graph.ranking;
12704
+ const { pick, earliestReturn, movedOff } = graph.ranking;
12645
12705
  if (pick !== void 0) {
12646
12706
  recordStickyPick(params.usageFs, params.paths.usagePicksFile, params.cwd, pick.identity, nowMs);
12647
- return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
12707
+ const move = movedOff === void 0 ? "" : `moved off ${movedOff.identity} (${movedOff.reason}); `;
12708
+ return { ok: true, identity: pick.identity, explanation: `pool ${poolName}: ${move}${pick.reasons.slice(0, REASONS_LOGGED).join("; ")}` };
12648
12709
  }
12649
12710
  if (earliestReturn === void 0) {
12650
12711
  return { ok: false, message: `agent-shim: no member of pool "${poolName}" can be picked.` };